Game Development 22 min read

Creating a Naruto-Themed 3D World with three.js: Model Loading, Controls, Animation, and Octree Collision

This tutorial walks through building a simple Naruto-inspired 3D scene using three.js, covering model acquisition, character creation, keyboard controls, rotation handling, animation state management, camera integration, octree-based collision detection, and troubleshooting common issues such as camera‑character alignment and jump physics.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating a Naruto-Themed 3D World with three.js: Model Loading, Controls, Animation, and Octree Collision

The article begins by assuming familiarity with three.js basics (scene, camera, model loading) and introduces the goal of constructing a small Naruto world with a single character and interactive controls.

Resource acquisition – 3D models are downloaded from Sketchfab and stored in a repository; the code uses a custom this.game.resource.getModel('hatch_naruto') call that internally relies on GLTFLoader to load the GLTF model.

Character creation – A Naruto class extending EventEmitter is defined, loads the model, and adds it to the scene:

export class Naruto extends EventEmitter {
  game: Game;
  scene: Scene;
  model!: GLTF;
  constructor() {
    super();
    this.game = Game.getInstance();
    this.scene = this.game.gameScene.scene;
    this.init();
  }
  init() {
    this.model = this.game.resource.getModel('hatch_naruto') as GLTF;
    this.scene.add(this.model.scene);
  }
  update() {}
}

Keyboard handling – A Keypress interface tracks W/A/S/D keys. Event listeners for keydown and keyup update the keypress object, allowing the system to know which keys are currently pressed.

type Keypress = { w: boolean, a: boolean, s: boolean, d: boolean };
keypress: Keypress = { w: false, a: false, s: false, d: false };
initEvents() {
  document.addEventListener('keydown', evt => {
    const key = evt.key.toLocaleLowerCase();
    if (Reflect.ownKeys(this.keypress).includes(key)) {
      this.keypress[key as keyof Keypress] = true;
    }
  });
  document.addEventListener('keyup', evt => {
    const key = evt.key.toLocaleLowerCase();
    if (Reflect.ownKeys(this.keypress).includes(key)) {
      this.keypress[key as keyof Keypress] = false;
    }
  });
}

Rotation logic – The character rotates around the Y‑axis based on pressed keys. The method handleRotation() computes an offset radian from getOffsetRadian() , creates a quaternion with setFromAxisAngle , and applies it using rotateTowards . The rotation order is set to 'YXZ' to avoid gimbal lock.

this.model.scene.rotation.order = 'YXZ';
this.model.scene.rotation.y = Math.PI;
handleRotation() {
  const offsetRadian = this.getOffsetRadian();
  this.quaternion.setFromAxisAngle(new Vector3(0, 1, 0), offsetRadian);
  this.model.scene.quaternion.rotateTowards(this.quaternion, 0.2);
}

Camera‑character alignment – When OrbitControls rotates the camera, the character’s forward direction becomes inconsistent. The solution adds the camera‑to‑character angle (computed with Math.atan2 ) to the offset radian before applying the quaternion.

const angleYFromCamera = Math.atan2(camera.position.x - player.position.x, camera.position.z - player.position.z);
this.quaternion.setFromAxisAngle(new Vector3(0, 1, 0), offsetRadian + angleYFromCamera);

Animation states – An enum Status defines six states (IDLE, WALK, RUNNING, JUMP, FALLING, DANCING). An AnimationMixer loads all animation clips, maps them by name, and cross‑fades between actions based on the current state.

enum Status { IDLE = 'mixamo.com', WALK = 'Walking', RUNNING = 'Running', JUMP = 'Male Dynamic Pose', FALLING = 'Falling Idle', DANCING = 'Hip Hop Dancing' }
// Mixer setup omitted for brevity

Movement and speed – Walking and running speeds are scaled by 0.001 . The direction vector is derived from the camera’s forward direction, normalized, and then rotated according to the key‑derived radian. The final movement vector updates the character’s position.

const speed = (this.status === Status.WALK ? this.walkSpeed : this.runSpeed) * 0.001;
const direction = new Vector3();
this.game.gameCamera.camera.getWorldDirection(direction);
// apply rotation and move

Camera follow – After moving the character, the camera’s position and target are updated to keep the player centered.

controls.object.position.x += moveX;
controls.object.position.z += moveZ;
controls.target = new Vector3(player.position.x, player.position.y + 1, player.position.z);

Octree collision detection – A singleton SceneOctree builds an Octree from the scene graph and uses a Capsule to represent the player’s collision volume. Each frame the capsule is translated by gravity, intersected with the octree, and corrected by moving it out of intersecting geometry. The capsule’s position is then copied back to the character mesh.

export class SceneOctree extends EventEmitter {
  octree = new Octree();
  capsule = new Capsule(new Vector3(0, 0.3, 0), new Vector3(0, 1.3, 0), 0.3);
  capsuleOnFloor = false;
  fallingSpeed = 0;
  init(scene: Group) { this.octree.fromGraphNode(scene); }
  update() {
    if (!this.capsuleOnFloor) {
      const ratio = 0.00001;
      this.fallingSpeed += -this.game.gameWorld.grivity * ratio * this.game.time.delta;
      this.capsule.translate(new Vector3(0, this.fallingSpeed, 0));
    }
    this.capsuleOnFloor = false;
    const result = this.octree.capsuleIntersect(this.capsule);
    if (result) {
      const { depth, normal } = result;
      this.capsuleOnFloor = normal.y > 0;
      if (this.capsuleOnFloor) this.fallingSpeed = 0;
      this.capsule.translate(normal.multiplyScalar(depth));
    }
  }
}

Remaining challenges – The author notes occasional jump failures and jitter when colliding with irregular geometry, indicating that further tuning of gravity and capsule resolution is needed.

Overall, the guide demonstrates a complete pipeline from model loading to interactive character control, animation blending, camera management, and basic physics using three.js, making it a practical reference for developers building browser‑based 3D games.

animationJavaScriptGame developmentThree.jsWebGLcollision detection
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.