roaming

preface

Recently, I have been busy with several project teams for some time. Participated in the secondary packaging of threejS engine of the company and gained a lot. This article mainly introduces the roaming scheme of realizing the switch between first person and third person and collision monitoring in 3D scenes.

The first effect

GIF is a little bit too big. Wait a minute

Analyze the

The functions that need to be implemented are as follows: - Loading model to generate collision surface - adding model robot and animation - manipulating movement and perspective following - of course, camera and lighting are also required. I will not repeat all the details here (relatively basic). By generating collision surface for collision monitoring, object movement and real-time update of camera position and Angle are realized through WASDCopy the code

Create collision surface

Collision surfaces are generated by model disassembly and calculation based on the principle of three-Glow-Mesh plug-in, which can be referred to open source projects https://gkjohnson.github.io/three-mesh-bvh/example/bundle/characterMovement.htmlCopy the code

// Core codeThrough deep traversal disassembly model calculation generated collision surface, want to know more about the author's source code, this piece of code I have made a little modification, is also a little knowledgeloadColliderEnvironment( scene, camera, model) {// Pass in the scene and camera and model
  const that = this
  const gltfScene = model
  new THREE.Box3().setFromObject(model)
  gltfScene.updateMatrixWorld(true)
  that.model=model
  // visual geometry setup
  const toMerge = {}
  gltfScene.traverse(c= > {
    if(c.isMesh && c.material.color ! = =undefined) {
      const hex = c.material.color.getHex()
      toMerge[hex] = toMerge[hex] || []
      toMerge[hex].push(c)
    }
  })

  that.environment = new THREE.Group()
  for (const hex in toMerge) {
    const arr = toMerge[hex]
    const visualGeometries = []
    arr.forEach(mesh= > {
      if(mesh.material.emissive && mesh.material.emissive.r ! = =0) {
        that.environment.attach(mesh)
      } else {
        const geom = mesh.geometry.clone()
        geom.applyMatrix4(mesh.matrixWorld)
        visualGeometries.push(geom)
      }
    })

    if (visualGeometries.length) {
      const newGeom = BufferGeometryUtils.mergeBufferGeometries(visualGeometries)
      const newMesh = new THREE.Mesh(newGeom, new THREE.MeshStandardMaterial({
        color: parseInt(hex),
        shadowSide: 2
      }))
      newMesh.castShadow = true
      newMesh.receiveShadow = true
      newMesh.material.shadowSide = 2
      newMesh.name = 'mool'
      that.environment.add(newMesh)
    }
  }

  // collect all geometries to merge
  const geometries = []
  that.environment.updateMatrixWorld(true)
  that.environment.traverse(c= > {
    if (c.geometry) {
      const cloned = c.geometry.clone()
      cloned.applyMatrix4(c.matrixWorld)
      for (const key in cloned.attributes) {
        if(key ! = ='position') {
          cloned.deleteAttribute(key)
        }
      }

      geometries.push(cloned)
    }
  })

  // create the merged geometry
  const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries, false)
  mergedGeometry.boundsTree = new MeshBVH(mergedGeometry, {lazyGeneration: false})

  that.collider = new THREE.Mesh(mergedGeometry)
  that.collider.material.wireframe = true
  that.collider.material.opacity = 0.5
  that.collider.material.transparent = true
  that.visualizer = new MeshBVHVisualizer(that.collider, that.params.visualizeDepth)
  that.visualizer.layers.set(that.currentlayers)
  that.collider.layers.set(that.currentlayers)
  scene.add(that.visualizer)
  scene.add(that.collider)
  scene.add(that.environment)
}
Copy the code

Load the robot model and animation

There is a special point on the side of the core code. Due to the Angle of view, it is easy to adjust the height of the Angle of view of the robot by following the position of the hidden geometry. In fact, WASD and jump operation are geometric cylindersCopy the code
Here is more basic, do not make too many notes, do not understand can see my first animation articleloadplayer(scene, camera) {
  const that = this
  // character model reference geometry
  that.player = new THREE.Mesh(
    new RoundedBoxGeometry(0.5.1.7.0.5.10.0.5),
    new THREE.MeshStandardMaterial()
  )
  that.player.geometry.translate(0, -0.5.0)
  that.player.capsuleInfo = {
    radius: 0.5.segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -1.0.0.0))
  }
  that.player.name = 'player'
  that.player.castShadow = true
  that.player.receiveShadow = true
  that.player.material.shadowSide = 2
  that.player.visible = false
  scene.add(that.player)
  const loader = new GLTFLoader()
  loader.load('/static/public/RobotExpressive.glb'.(gltf) = > {
    gltf.scene.scale.set(0.3.0.3.0.3)
    that.robot = gltf.scene
    that.robot.capsuleInfo = {
      radius: 0.5.segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -1.0))
    }
    that.robot.castShadow = true
    that.robot.receiveShadow = true
    that.robot.visible = true
    that.robot.traverse(c= > {
      c.layers.set(that.currentlayers)
    })
    const animations = gltf.animations / / animation
    that.mixer = new THREE.AnimationMixer(gltf.scene)
    var action = that.mixer.clipAction(animations[6])
    action.play()
    scene.add(that.robot)
    that.reset(camera)
  })
}
Copy the code

Operational events

This includes WASD movement as well as jumping and person switchingCopy the code
this.params = { This is the object that is initialized to configure the GUI
  firstPerson: false.displayCollider: false.displayBVH: false.visualizeDepth: 10.gravity: -30.playerSpeed: 5.physicsSteps: 5.reset: that.reset
}
windowEvent(camera, renderer) {
  const that = this
  window.addEventListener('resize'.function () {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()

    renderer.setSize(window.innerWidth, window.innerHeight)
  }, false)

  window.addEventListener('keydown'.function (e) {
    switch (e.code) {
      case 'KeyW':
        that.fwdPressed = true
        break
      case 'KeyS':
        that.bkdPressed = true
        break
      case 'KeyD':
        that.rgtPressed = true
        break
      case 'KeyA':
        that.lftPressed = true
        break
      case 'Space':
        if (that.playerIsOnGround) {
          that.playerVelocity.y = 10.0
        }
        break
      case 'KeyV': that.params.firstPerson = ! that.params.firstPersonif(! that.params.firstPerson) {// Person switch
          camera
            .position
            .sub(that.controls.target)
            .normalize()
            .multiplyScalar(10)
            .add(that.controls.target)
          that.robot.visible = true
        } else {
          that.robot.visible = false
        }
        break}})window.addEventListener('keyup'.function (e) {
    switch (e.code) {
      case 'KeyW':
        that.fwdPressed = false
        break
      case 'KeyS':
        that.bkdPressed = false
        break
      case 'KeyD':
        that.rgtPressed = false
        break
      case 'KeyA':
        that.lftPressed = false
        break}})}Copy the code

Model camera position updated

In addition to collision monitoring, the most important thing for so-called roaming is movement and camera following. Here we need to understand that in addition to the object's own coordinate system, there is also a world coordinate system. When we modify the object, we need to update its vertex coordinate position in the world coordinate systemCopy the code
Initialization parametersconst upVector = new THREE.Vector3(0.1.0)
const tempVector = new THREE.Vector3()
const tempVector2 = new THREE.Vector3()
const tempBox = new THREE.Box3()
const tempMat = new THREE.Matrix4()
const tempSegment = new THREE.Line3()
Copy the code
updatePlayer(delta, params, fwdPressed, tempVector, upVector, bkdPressed, lftPressed, rgtPressed, tempBox, tempMat, tempSegment, tempVector2, camera) {
  const that = this
  that.playerVelocity.y += that.playerIsOnGround ? 0 : delta * params.gravity
  that.player.position.addScaledVector(that.playerVelocity, delta)
  // move the player
  const angle = that.controls.getAzimuthalAngle()
  //WASD
  if (fwdPressed) {
    tempVector.set(0.0, -1).applyAxisAngle(upVector, angle)
    that.player.position.addScaledVector(tempVector, params.playerSpeed * delta)
  }

  if (bkdPressed) {
    tempVector.set(0.0.1).applyAxisAngle(upVector, angle)
    that.player.position.addScaledVector(tempVector, params.playerSpeed * delta)
  }

  if (lftPressed) {
    tempVector.set(-1.0.0).applyAxisAngle(upVector, angle)
    that.player.position.addScaledVector(tempVector, params.playerSpeed * delta)
  }

  if (rgtPressed) {
    tempVector.set(1.0.0).applyAxisAngle(upVector, angle)
    that.player.position.addScaledVector(tempVector, params.playerSpeed * delta)
  }
  // Update model world coordinates
  that.player.updateMatrixWorld()

  // adjust player position based on collisions
  const capsuleInfo = that.player.capsuleInfo
  tempBox.makeEmpty()
  tempMat.copy(that.collider.matrixWorld).invert()
  tempSegment.copy(capsuleInfo.segment)

  // get the position of the capsule in the local space of the collider
  tempSegment.start.applyMatrix4(that.player.matrixWorld).applyMatrix4(tempMat)
  tempSegment.end.applyMatrix4(that.player.matrixWorld).applyMatrix4(tempMat)

  // get the axis aligned bounding box of the capsule
  tempBox.expandByPoint(tempSegment.start)
  tempBox.expandByPoint(tempSegment.end)

  tempBox.min.addScalar(-capsuleInfo.radius)
  tempBox.max.addScalar(capsuleInfo.radius)

  that.collider.geometry.boundsTree.shapecast({

    intersectsBounds: box= > box.intersectsBox(tempBox),

    intersectsTriangle: tri= > {
      // check if the triangle is intersecting the capsule and adjust the
      // capsule position if it is.
      const triPoint = tempVector
      const capsulePoint = tempVector2

      const distance = tri.closestPointToSegment(tempSegment, triPoint, capsulePoint)
      if (distance < capsuleInfo.radius) {
        const depth = capsuleInfo.radius - distance
        const direction = capsulePoint.sub(triPoint).normalize()

        tempSegment.start.addScaledVector(direction, depth)
        tempSegment.end.addScaledVector(direction, depth)
      }
    }

  })

  // get the adjusted position of the capsule collider in world space after checking
  // triangle collisions and moving it. capsuleInfo.segment.start is assumed to be
  // the origin of the player model.
  const newPosition = tempVector
  newPosition.copy(tempSegment.start).applyMatrix4(that.collider.matrixWorld)

  // check how much the collider was moved
  const deltaVector = tempVector2
  deltaVector.subVectors(newPosition, that.player.position)
  // if the player was primarily adjusted vertically we assume it's on something we should consider ground
  that.playerIsOnGround = deltaVector.y > Math.abs(delta * that.playerVelocity.y * 0.25)

  const offset = Math.max(0.0, deltaVector.length() - 1e-5)
  deltaVector.normalize().multiplyScalar(offset)

  // adjust the player model
  that.player.position.add(deltaVector)
  if(! that.playerIsOnGround) { deltaVector.normalize() that.playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(that.playerVelocity)) }else {
    that.playerVelocity.set(0.0.0)}// adjust the camera
  camera.position.sub(that.controls.target)
  that.controls.target.copy(that.player.position)
  camera.position.add(that.player.position)
  that.player.rotation.y = that.controls.getAzimuthalAngle() + 3
  if (that.robot) {
    that.robot.rotation.y = that.controls.getAzimuthalAngle() + 3
    that.robot.position.set(that.player.position.clone().x, that.player.position.clone().y, that.player.position.clone().z)
    that.robot.position.y -= 1.5
  }
  // if the player has fallen too far below the level reset their position to the start
  if (that.player.position.y < -25) {
    that.reset(camera)
  }
}
Copy the code

Click floor displacement

By two-dimensional coordinates into three-dimensional coordinates and custom shaders to achieve the functionCopy the code

/ / shader
scatterCircle(r, init, ring, color, speed) {
   var uniform = {
     u_color: {value: color},
     u_r: {value: init},
     u_ring: {
       value: ring
     }
   }

   var vs = ` varying vec3 vPosition; void main(){ vPosition=position; Gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `
   var fs = ` varying vec3 vPosition; uniform vec3 u_color; uniform float u_r; uniform float u_ring; Void main () {float PCT = short (vec2 (vPosition. X, vPosition. Y), vec2 (0.0)); If (PCT > u_r | | PCT < (u_r - u_ring)) {gl_FragColor = vec4 (1.0, 0.0, 0.0, 0); }else{ float dis=(pct-(u_r-u_ring))/(u_r-u_ring); gl_FragColor = vec4(u_color,dis); }} `
   const geometry = new THREE.CircleGeometry(r, 120)
   var material = new THREE.ShaderMaterial({
     vertexShader: vs,
     fragmentShader: fs,
     side: THREE.DoubleSide,
     uniforms: uniform,
     transparent: true.depthWrite: false
   })
   const circle = new THREE.Mesh(geometry, material)
   circle.layers.set(this.currentlayers)

   function render() {
     uniform.u_r.value += speed || 0.1
     if (uniform.u_r.value >= r) {
       uniform.u_r.value = init
     }
     requestAnimationFrame(render)
   }

   render()
   return circle
 }
Copy the code
// Click the event
clickMobile(camera, scene) {
  const raycaster = new THREE.Raycaster()
  const mouse = new THREE.Vector2()

  const that = this
  document.addEventListener('dblclick'.function (ev) {
    mouse.x = (ev.clientX / window.innerWidth) * 2 - 1
    mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1
    // Here we only check the selected model
    raycaster.setFromCamera(mouse, camera)
    const intersects = raycaster.intersectObjects(scene.children, true)
    if (intersects.length > 0) {
      var selected
      let ok = false
      intersects.map(child= > {
        if (child.object.name === 'mool' && !ok) {
          selected = child// take the first object
          ok = true}})if (selected) {
        that.walking = true
        clearTimeout(that.timer)
        if(! that.circle) { that.circle = that.scatterCircle(1.0.1.0.3.new THREE.Vector3(0.1.1), 0.1)
          scene.add(that.circle)
        }
        const d1 = that.player.position.clone()
        const d2 = new THREE.Vector3(selected.point.x, that.player.position.y, selected.point.z)
        const distance = d1.distanceTo(d2)
        that.circle.position.set(selected.point.x, 2.5, selected.point.z)
        that.circle.rotation.x = Math.PI / 2
        that.setTweens(that.player.position, {
          x: selected.point.x,
          y: that.player.position.y,
          z: selected.point.z
        }, distance * 222)
        that.timer = setTimeout(() = > {
          that.walking = false
          that.circle.visible = false
        }, distance * 222)
        that.circle.visible = true}}},false)}Copy the code

render

The Render function mainly updates some of the animation positions in the sceneCopy the code
function render() {
  // stats.update()
  that.timeIndex = requestAnimationFrame(render)
  TWEEN.update()
  const delta = Math.min(clock.getDelta(), 0.1)
  if(that.mixer && (that.rgtPressed || that.lftPressed || that.bkdPressed || that.fwdPressed || that.walking) && ! that.params.firstPerson) { that.mixer.update(delta) }if (that.params.firstPerson) {
    that.controls.maxPolarAngle = Math.PI / 2
    that.controls.minDistance = 1e-4
    that.controls.maxDistance = 1e-4
  } else {
    that.controls.maxPolarAngle = Math.PI / 2
    that.controls.minDistance = 10
    that.controls.maxDistance = 20
  }

  if (that.collider && that.player) {
    that.collider.visible = that.params.displayCollider
    that.visualizer.visible = that.params.displayBVH

    const physicsSteps = that.params.physicsSteps
    for (let i = 0; i < physicsSteps; i++) {
      that.updatePlayer(delta / physicsSteps, that.params, that.fwdPressed, tempVector, upVector, that.bkdPressed, that.lftPressed, that.rgtPressed, tempBox, tempMat, tempSegment, tempVector2, camera)
    }
  }
}
Copy the code

conclusion

Due to time reasons, this article refers to a specific modification made by an open source project. The open source link has been put in the article, and many details cannot be annotated, partly because I do not particularly understand them, partly because I am pressed for time. As the code is too long to post, please refer to the open source demo for detailsCopy the code