Three.js 简单碰撞检测教程
简单碰撞检测 ·Simple Coll· ▶ 在线运行案例
- 案例合集:三维可视化功能案例(threehub.cn)
- 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
- 400个案例代码:网盘链接
你将学到什么
- onBeforeCompile 注入 GLSL 改造内置材质
- OrbitControls 相机轨道交互
- 场景雾效增强纵深
requestAnimationFrame渲染循环与resize自适应
效果说明
本案例演示简单碰撞检测效果:Bounding volume hierarchy (BVH)即层次包围体,,在BVH中,所有的几何物体都会被包在bounding volume的叶子节点里面,;核心用到 onBeforeCompile、OrbitControls、场景雾效增强纵深。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。
核心概念
- Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开
logarithmicDepthBuffer缓解 Z-fighting。 - onBeforeCompile在 Three 拼好内置 shader 后替换
#include片段,适合在 PBR 材质上叠加大屏特效。 - OrbitControls提供轨道旋转/缩放;开启
enableDamping后需在 animate 中controls.update()。
实现步骤
- 搭建灯光与环境(如有)
- requestAnimationFrame 循环 update + render
代码要点
import { Scene, Fog, Color, PerspectiveCamera, WebGLRenderer, DirectionalLight, AmbientLight, PlaneGeometry, MeshLambertMaterial, Mesh, GridHelper, Vector2, Line3, MeshStandardMaterial, Vector3, Box3, Matrix4, Clock, CapsuleGeometry, Box3Helper, } from "three";import { OrbitControls } from "three/examples/jsm/Addons.js"; let scene, terrain, camera, controls, clock, renderer; // 碰撞参数/三维世界参数 const params = { firstPerson: false, displayCollider: false, displayBVH: false, visualizeDepth: 10, gravity: -30, playerSpeed: 10, // 步长 physicsSteps: 5, }; // 分数布朗运动 用于生成随机地形 let fbm =
// https://github.com/yiwenl/glsl-fbm/blob/master/3d.glsl #define NUM_OCTAVES 6float mod289(float x){return x - floor(x(1.0 / 289.0))289.0;} vec4 mod289(vec4 x){return x - floor(x(1.0 / 289.0))289.0;} vec4 perm(vec4 x){return mod289(((x34.0) + 1.0)x);}
float noise(vec3 p){ vec3 a = floor(p); vec3 d = p - a; d = dd(3.0 - 2.0 * d);
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); vec4 k1 = perm(b.xyxy); vec4 k2 = perm(k1.xyxy + b.zzww);
vec4 c = k2 + a.zzzz; vec4 k3 = perm(c); vec4 k4 = perm(c + 1.0);
vec4 o1 = fract(k3 * (1.0 / 41.0)); vec4 o2 = fract(k4 * (1.0 / 41.0));
vec4 o3 = o2d.z + o1(1.0 - d.z); vec2 o4 = o3.ywd.x + o3.xz(1.0 - d.x);
return o4.yd.y + o4.x(1.0 - d.y); }
float fbm(vec3 x) { float v = 0.0; float a = 0.5; vec3 shift = vec3(100); for (int i = 0; i < NUM_OCTAVES; ++i) { v += a * noise(x); x = x * 2.0 + shift; a *= 0.5; } return v; }; let globalUniforms = { time: { value: 0 }, }; // 监听键盘初始值 let fwdPressed = false, bkdPressed = false, lftPressed = false, rgtPressed = false; // player的速度 x,y,z三个方向上 let playerVelocity = new Vector3(); // 初始位置不在ground上 let playerIsOnGround = false; let upVector = new Vector3(0, 1, 0); let tempVector = new Vector3(); let tempVector2 = new Vector3(); let tempBox = new Box3(); let tempMat = new Matrix4(); let tempSegment = new Line3(); let init_scene = () => { scene = new Scene(); scene.background = new Color(0.5, 1, 0.875); scene.fog = new Fog(scene.background, 20, 45); camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000); let vHeight = 3; camera.position.set(30, vHeight + 2, 20).setLength(15); renderer = new WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); window.addEventListener("resize", () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }); controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, vHeight, 0); controls.update(); // controls.minPolarAngle = Math.PI * 0.4; // controls.maxPolarAngle = Math.PI * 0.5; // controls.minDistance = 10; // controls.maxDistance = 20; controls.enableDamping = true; // controls.enablePan = false; let light = new DirectionalLight(0xffffff, 0.25); light.position.setScalar(1); scene.add(light, new AmbientLight(0xffffff, 0.75)); clock = new Clock(); // 键盘监听 window.addEventListener("keydown", function (e) { switch (e.code) { case "KeyW": fwdPressed = true; break; case "KeyS": bkdPressed = true; break; case "KeyD": rgtPressed = true; break; case "KeyA": lftPressed = true; break; case "Space": if (playerIsOnGround) { playerVelocity.y = 10.0; playerIsOnGround = false; } break; } }); window.addEventListener("keyup", function (e) { switch (e.code) { case "KeyW": fwdPressed = false; break; case "KeyS": bkdPressed = false; break; case "KeyD": rgtPressed = false; break; case "KeyA": lftPressed = false; break; } }); }; import { computeBoundsTree, MeshBVHHelper } from "three-mesh-bvh";const add_helper = () => { const grid = new GridHelper(50, 50); scene.add(grid); const bvh_helper = new MeshBVHHelper(terrain, params.visualizeDepth); scene.add(bvh_helper); }; import { ImprovedNoise } from "three/examples/jsm/Addons.js"; import { MeshBVH, acceleratedRaycast } from "three-mesh-bvh"; // 启用 BVH 加速光线投射功能 Mesh.prototype.raycast = acceleratedRaycast; const load_collision_environment = () => { var _a, _b; let perlin = new ImprovedNoise(); let plane = new PlaneGeometry(50, 50, 500, 500); plane.rotateX(-Math.PI / 2); let { position } = plane.attributes; let uv = plane.attributes.uv; let v2 = new Vector2(); for (let i = 0; i < position.count; i++) { v2.fromBufferAttribute(uv, i).multiplyScalar(15); let n = perlin.noise(v2.x, v2.y, 0.314); n = Math.abs(n); n = Math.pow(n, 3); position.setY(i, Math.min(n * 35, 10)); } plane.computeVertexNormals(); let material = new MeshLambertMaterial({ color: 0xface8d, // wireframe:true }); material.onBeforeCompile = (shader) => { shader.uniforms.time = globalUniforms.time; shader.vertexShader =
varying vec3 vPos; ${shader.vertexShader}.replace(#include,#include vPos = position;); shader.fragmentShader =#define ss(a,b,c) smoothstep(a,b,c) uniform float time; varying vec3 vPos; ${fbm} ${shader.fragmentShader}.replace(vec4 diffuseColor = vec4( diffuse, opacity );,vec3 col = diffuse;float d = noise(vPos * vec3(0.05, 1, 0.05)); col = mix(col + 0.2, vec3(1, 0.2, 0.01), d);
vec3 strokePos = vPos * vec3(0.1, 3., 0.1); d = fbm(strokePos); float e = fwidth(strokePos.y); col = mix(col(0.5 + 0.5ss(2., 8., vPos.y)), col, ss(0.4 - e, 0.4, abs(d)));
col = mix(diffuse + 0.1, col, ss(0.5, 1.5, vPos.y));
// wind float dw = noise(vec3(vPos.x, vPos.y, vPos.z + time) * vec3(0.1, 10, 0.1)); d = ss(0.1, 0., abs(dw)); d = max(d, ss(1., 0., abs(dw))); d = max(d, pow(abs(noise(vPos - vec3(0, 0, time))), 1.)); d *= smoothstep(2., -0.5, abs(vPos.y)); col = mix(col, diffuse + 0.25, d);
vec4 diffuseColor = vec4( col, opacity );) .replace(#include,gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(0.5, 1, 0.875), pow(ss(7., 10., vPos.y), 0.5));); }; terrain = new Mesh(plane, material); terrain.geometry.computeBoundsTree = computeBoundsTree; (_b = (_a = terrain.geometry).computeBoundsTree) === null || _b === void 0 ? void 0 : _b.call(_a); // terrain.geometry.computeBoundingBox(); // if (terrain.geometry.boundingBox) { // addBoxHelper(terrain.geometry.boundingBox); // } scene.add(terrain); ready = true; }; let player; const add_player = () => { player = new Mesh(new CapsuleGeometry(0.3, 0.3, 4, 8), new MeshStandardMaterial()); // player.geometry.translate(0, 0, 0); // 胶囊体信息 player.userData.capsuleInfo = { radius: 0.5, segment: new Line3(new Vector3(), new Vector3(0, -0.5, 0.0)), }; player.castShadow = true; player.receiveShadow = true; player.userData.boundsTree = new MeshBVH(player.geometry); if (!Array.isArray(player.material)) { player.material.shadowSide = 2; } scene.add(player); }; // 地形碰撞环境 /**- Bounding volume hierarchy (BVH)即层次包围体,
- 在BVH中,所有的几何物体都会被包在bounding volume的叶子节点里面,
- bounding volume外面继续包着一个更大的bounding volume,
- 递归地包裹下去,最终形成的根节点会包裹着整个场景。
完整源码:GitHub
小结
- 本文提供简单碰撞检测完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
