当前位置: 首页 > news >正文

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 6

    float 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,
    • 递归地包裹下去,最终形成的根节点会包裹着整个场景。
    */ const addBoxHelper = (box) => { const helper = new Box3Helper(box); scene.add(helper); }; // 检测碰撞 let ready, bvh_tree; const detect_collision = () => { bvh_tree = terrain.geometry.boundsTree; player.geometry.computeBoundingBox(); // adjust player position based on collisions const capsuleInfo = player.userData.capsuleInfo; tempBox.makeEmpty(); tempMat.copy(terrain.matrixWorld).invert(); tempSegment.copy(capsuleInfo.segment); // get the position of the capsule in the local space of the collider tempSegment.start.applyMatrix4(player.matrixWorld).applyMatrix4(tempMat); tempSegment.end.applyMatrix4(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); addBoxHelper(tempBox); bvh_tree.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); } }, }); }; function reset() { playerVelocity.set(0, 0, 0); player.position.set(0, 15, 0); camera.position.sub(controls.target); controls.target.copy(player.position); camera.position.add(player.position); controls.update(); } // 渲染器 const render = () => { requestAnimationFrame(render); const delta = Math.min(clock.getDelta(), 0.1); const physicsSteps = params.physicsSteps; if (ready) { for (let i = 0; i < physicsSteps; i++) { update_player(delta / physicsSteps); } } controls.update(); renderer.render(scene, camera); }; // 控制器 function update_player(delta) { // player是否在地面上 if (playerIsOnGround) { // 在 y方向速度 playerVelocity.y = delta * params.gravity; } else { // 不在 y方向速度 playerVelocity.y += delta * params.gravity; } // 更新位置 player.position.addScaledVector(playerVelocity, delta); // move the player // 当前的水平旋转角度 const angle = controls.getAzimuthalAngle(); if (fwdPressed) { // tempVector:行进方向 tempVector.set(0, 0, -1).applyAxisAngle(upVector, angle); player.position.addScaledVector(tempVector, params.playerSpeed * delta); } if (bkdPressed) { tempVector.set(0, 0, 1).applyAxisAngle(upVector, angle); player.position.addScaledVector(tempVector, params.playerSpeed * delta); } if (lftPressed) { tempVector.set(-1, 0, 0).applyAxisAngle(upVector, angle); player.position.addScaledVector(tempVector, params.playerSpeed * delta); } if (rgtPressed) { tempVector.set(1, 0, 0).applyAxisAngle(upVector, angle); player.position.addScaledVector(tempVector, params.playerSpeed * delta); } player.updateMatrixWorld(); detect_collision(); // 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(terrain.matrixWorld); // check how much the collider was moved const deltaVector = tempVector2; deltaVector.subVectors(newPosition, player.position); // if the player was primarily adjusted vertically we assume it's on something we should consider ground playerIsOnGround = deltaVector.y > Math.abs(deltaplayerVelocity.y0.25); const offset = Math.max(0.0, deltaVector.length() - 1e-5); deltaVector.normalize().multiplyScalar(offset); // adjust the player model player.position.add(deltaVector); if (!playerIsOnGround) { deltaVector.normalize(); playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(playerVelocity)); } else { playerVelocity.set(0, 0, 0); } // 掉下去了 if (player.position.y < -25) { reset(); } } init_scene(); load_collision_environment(); add_player(); add_helper(); render();

    完整源码:GitHub

    小结

    • 本文提供简单碰撞检测完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
http://www.jsqmd.com/news/1096951/

相关文章:

  • 告别安卓模拟器:3分钟学会在Windows上直接安装APK应用
  • 3分钟掌握Resemble Enhance:终极AI语音降噪增强神器
  • Spring Boot 与 Solon 比较,相互迁移实战指南
  • Cadence Allegro PCB Designer实战:从零到一绘制标准PCB封装
  • qrcode.vue:Vue生态中的专业二维码生成解决方案
  • Parsedown终极指南:3步打造高效Markdown解析工作流
  • 杨洋亮相青岛啤酒“白啤更懂夏的嗨”派对 共赴夏日之约
  • Kazumi番剧播放器:如何通过插件扩展实现全网动漫自由观看
  • 【全网最详细】Sucrose Wallpaper Engine下载免费版 动态桌面壁纸软件安装图解(2026最新)
  • 从Wireshark到NpCap:动手构建网络协议解析与流量监控工具
  • ArkTS常用组件知识点整理
  • AGGrid自定义cellRenderer下tooltipShowMode不生效如何处理?
  • 3步搞定艾尔登法环存档管理:终极角色迁移方案
  • Multisim14丨界面布局异常恢复丨实战排查指南
  • 从零到一:基于STM8的125KHz RFID读卡器实现与曼彻斯特码解析实战
  • ORBSLAM3实战:手把手教你将KITTI数据集适配VIO/IMU模式,并完成精度评估
  • OpenAI API 0613更新深度解析:从GPT-3.5-turbo-16k到函数调用的实战指南
  • 红帽 Linux 零基础完整学习笔记 5
  • 从跑分到洞察:CPU性能评估工具全解析与实战指南
  • Yahoo Finance API:.NET开发者的金融数据革命性解决方案
  • 从编译产物到智能索引:详解gen_compile_commands.py生成compile_commands.json的实战路径
  • 从理论到实践:积极心理学与情绪智慧如何赋能研究生科研与生活
  • 深度解析Untrunc:开源视频修复工具的技术实现与实战应用
  • Python量化交易数据获取的终极解决方案:efinance免费金融数据库完全指南
  • AI智能审核技术架构解析:规则引擎与大模型协同的双重拦截
  • MCP 会取代 API 吗?普通开发者应该怎么理解它?
  • 20美元革命性突破:打造你的专属超声波定向音响系统
  • 深圳亚马逊卖家做GEO,哪家能提升站外AI流量?
  • STM32F407硬件SPI驱动GD25Q32闪存,从接线到读写数据的保姆级教程
  • 通用大模型 vs 行业垂类 vs 自建小模型:差 3 个点,和差23 个点