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