用JavaScript构建3D程序
用JavaScript构建3D程序
概述
使用 JavaScript 构建 3D 程序通常依赖于现成的库或框架来实现高效开发。JavaScript可用的主流 3D 库有Three.js、Babylon.js等
两者对比
特性 | Three.js | Babylon.js |
核心功能 | 专注于渲染 | 渲染 + 内置物理引擎 |
物理引擎支持 | 需要集成第三方库(如 Cannon.js) | 内置 Cannon.js / Oimo.js / Ammo.js |
灵活性 | 高(开发者可以自由选择物理引擎) | 较低(依赖内置的物理引擎) |
学习曲线 | 较低(API 简洁) | 较高(API 更复杂,功能更多) |
适用场景 | WebGL/WebXR、轻量级项目 | WebGL/WebXR、游戏开发 |
学习路径的差异
- Three.js 的学习路径更线性:从基础几何体、材质、光照到高级功能(如着色器、后期处理)。
- Babylon.js 的学习路径更广泛:除了渲染功能,还需要学习物理引擎、动画系统、GUI 系统等
Three.js 和 Babylon.js的坐标系比较
Three.js 坐标系
Three.js 使用的是 左手坐标系(Left-Handed Coordinate System)。具体特点如下:
- X轴:水平方向,指向右侧。
- Y轴:垂直方向,指向上方。
- Z轴:深度方向,指向屏幕外(从相机的角度来看,即远离你)。
在 Three.js 中,原点 (0, 0, 0) 通常位于场景的中心,物体的位置是相对于这个原点的坐标来设定的。
Babylon.js 坐标系
Babylon.js 使用的是 右手坐标系(Right-Handed Coordinate System)。其特点与 Three.js 有一些不同,具体如下:
- X轴:水平方向,指向右侧。
- Y轴:垂直方向,指向上方。
- Z轴:深度方向,指向屏幕内(从相机的角度来看,即靠近你)。
在 Babylon.js 中,原点(0, 0, 0)也通常位于场景的中心,物体的坐标是相对于这个原点来设定的。与 Three.js 不同的是,Babylon.js 的 Z 轴朝向相机方向。
说明:大多数图形硬件(如 OpenGL)使用的是左手坐标系,因此 Three.js 选择使用左手坐标系。而 Babylon.js 是为了与 DirectX 兼容,DirectX 使用的是右手坐标系。
Three.js
Three.js是最流行的 Web 3D 库,文档丰富,适合快速原型开发。
英文文档 https://threejs.org/docs/
中文文档 https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
核心概念
场景 (Scene): 场景是 Three.js 中所有对象的容器。你可以将物体、光源、相机等添加到场景中。
相机 (Camera): 相机决定了场景中的哪些部分会被渲染。常用的相机类型包括透视相机 (PerspectiveCamera) 和正交相机 (OrthographicCamera)。
渲染器 (Renderer): 渲染器负责将场景和相机的内容渲染到网页上。常用的渲染器是 WebGLRenderer。
几何体 (Geometry): 几何体定义了物体的形状,例如立方体 (BoxGeometry)、球体 (SphereGeometry) 等。
材质 (Material): 材质定义了物体的外观,例如颜色、纹理、光照效果等。常用的材质包括 MeshBasicMaterial、MeshPhongMaterial 等。
网格 (Mesh): 网格是几何体和材质的组合,表示一个可渲染的物体。
环境搭建
<!-- 通过 CDN 引入 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
CDN原理与应用简要介绍 https://blog.csdn.net/cnds123/article/details/126268941
JavaScript 3D动画库three.js入门篇 https://blog.csdn.net/cnds123/article/details/121060565
以下是一个简单的 Three.js 示例代码,用于创建一个旋转的立方体:
效果图:
源码如下:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Three.js 示例:旋转的立方体</title><style>body {margin: 0;padding: 0;width: 100%;height: 100%;display: flex;justify-content: center; /* 水平居中 */align-items: center; /* 垂直居中 */}canvas {display: block; }</style>
</head>
<body><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script><script>// 创建场景const scene = new THREE.Scene();// 创建相机const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);camera.position.z = 5;// 创建渲染器const renderer = new THREE.WebGLRenderer();renderer.setSize(window.innerWidth, window.innerHeight);document.body.appendChild(renderer.domElement);// 创建立方体材质(6种不同颜色)const materials = [new THREE.MeshBasicMaterial({ color: 0xff0000 }), // 右面 - 红new THREE.MeshBasicMaterial({ color: 0x00ff00 }), // 左面 - 绿new THREE.MeshBasicMaterial({ color: 0x0000ff }), // 上面 - 蓝new THREE.MeshBasicMaterial({ color: 0xffff00 }), // 下面 - 黄new THREE.MeshBasicMaterial({ color: 0xff00ff }), // 前面 - 品红new THREE.MeshBasicMaterial({ color: 0x00ffff }) // 后面 - 青];// 创建立方体并应用多材质const geometry = new THREE.BoxGeometry();const cube = new THREE.Mesh(geometry, materials);scene.add(cube);// 动画循环function animate() {requestAnimationFrame(animate);cube.rotation.x += 0.01;cube.rotation.y += 0.01;renderer.render(scene, camera);}animate();</script>
</body>
</html>
修改上面的Three.js示例,给立方体加上纹理,并且能用鼠标拖动立方体的位置。
效果图:
使用 TextureLoader 加载棋盘格纹理(示例 URL 来自 Three.js 官方资源)
鼠标拖拽,事件绑定:mousedown 开始拖拽,mousemove 更新位置,mouseup 结束拖拽。
源码如下:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Three.js 纹理 + 拖拽立方体</title><style>body { margin: 0; }canvas { display: block; cursor: grab; }canvas:active { cursor: grabbing; }</style>
</head>
<body><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script><script>// 初始化场景、相机、渲染器const scene = new THREE.Scene();const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setSize(window.innerWidth, window.innerHeight);document.body.appendChild(renderer.domElement);// 加载纹理贴图(这里使用棋盘格纹理示例)const textureLoader = new THREE.TextureLoader();const texture = textureLoader.load('https://threejs.org/examples/textures/checker.png');texture.wrapS = THREE.RepeatWrapping;texture.wrapT = THREE.RepeatWrapping;texture.repeat.set(4, 4); // 重复纹理4x4次// 创建立方体(应用纹理)const geometry = new THREE.BoxGeometry();const material = new THREE.MeshPhongMaterial({ map: texture, // 使用纹理贴图color: 0xffffff });const cube = new THREE.Mesh(geometry, material);scene.add(cube);// 添加光源const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);scene.add(ambientLight);const pointLight = new THREE.PointLight(0xffffff, 1, 100);pointLight.position.set(10, 10, 10);scene.add(pointLight);camera.position.z = 5;// --- 拖拽逻辑 ---let isDragging = false;let previousMousePosition = { x: 0, y: 0 };const raycaster = new THREE.Raycaster();const mouse = new THREE.Vector2();// 鼠标事件处理function onMouseDown(event) {// 计算鼠标归一化坐标mouse.x = (event.clientX / window.innerWidth) * 2 - 1;mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;// 检测点击物体raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObject(cube);if (intersects.length > 0) {isDragging = true;previousMousePosition = {x: event.clientX,y: event.clientY};}}function onMouseMove(event) {if (!isDragging) return;// 计算移动增量const deltaMove = {x: event.clientX - previousMousePosition.x,y: event.clientY - previousMousePosition.y};// 将屏幕移动转换为3D空间移动(基于相机视角)const deltaX = deltaMove.x * 0.01;const deltaY = -deltaMove.y * 0.01; // Y轴方向反转cube.position.x += deltaX;cube.position.y += deltaY;previousMousePosition = {x: event.clientX,y: event.clientY};}function onMouseUp() {isDragging = false;}// 绑定事件window.addEventListener('mousedown', onMouseDown);window.addEventListener('mousemove', onMouseMove);window.addEventListener('mouseup', onMouseUp);// 动画循环function animate() {requestAnimationFrame(animate);cube.rotation.x += 0.005;cube.rotation.y += 0.005;renderer.render(scene, camera);}animate();// 窗口大小调整window.addEventListener('resize', () => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);});</script>
</body>
</html>
Babylon.js
Babylon.js 内置物理引擎(Cannon.js / Oimo.js / Ammo.js),更适合游戏开发。
英文文档 https://doc.babylonjs.com/
中文文档 https://shawn0326.github.io/babylon-doc-cn/#/
Babylonjs中文网 https://doc.cnbabylon.com/
核心概念
Babylon.js 是一个功能强大的 WebGL 3D 引擎,支持从简单的场景到复杂的游戏开发。以下是其核心概念:
场景 (Scene): 场景是所有对象的容器,包括网格、光源、相机等。
网格 (Mesh): 网格是几何体和材质的组合,表示一个可渲染的物体。
材质 (Material): 材质定义了物体的外观,例如颜色、纹理、光照效果等。
光源 (Light): Babylon.js 支持多种光源类型,如点光源 (PointLight)、平行光 (DirectionalLight)、聚光灯 (SpotLight) 等。
相机 (Camera): 相机决定了场景中的哪些部分会被渲染。常用的相机类型包括自由相机 (FreeCamera)、弧形旋转相机 (ArcRotateCamera) 等。
物理引擎 (Physics Engine): Babylon.js 内置了物理引擎支持(如 Cannon.js、Oimo.js),可以轻松实现碰撞检测、重力效果等。
环境搭建
<!-- 通过 CDN 引入 -->
<script src="https://cdn.babylonjs.com/babylon.js"></script>
地面上静止的小球
效果图:
源码如下:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Babylon.js 示例:地面上的小球</title><style>html, body {width: 100%;height: 100%;margin: 0;padding: 0;overflow: hidden; /* 防止页面滚动 */}#renderCanvas {width: 100%;height: 100%;touch-action: none; /* 禁用触摸默认行为,避免与 3D 交互冲突 */}</style>
</head>
<body><!-- 用于渲染 3D 场景的画布 --><canvas id="renderCanvas"></canvas><!-- 引入 Babylon.js 库 --><script src="https://cdn.babylonjs.com/babylon.js"></script><script>// 获取画布元素const canvas = document.getElementById("renderCanvas");// 初始化 Babylon.js 引擎,传入画布和是否启用抗锯齿const engine = new BABYLON.Engine(canvas, true);// 创建场景的函数const createScene = function () {// 创建一个新的场景const scene = new BABYLON.Scene(engine);// 创建一个自由相机,设置其初始位置为 (0, 5, -10)const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);// 设置相机的目标点为场景原点 (0, 0, 0)camera.setTarget(BABYLON.Vector3.Zero());// 将相机绑定到画布,允许用户通过鼠标/触摸控制相机camera.attachControl(canvas, true);// 创建一个半球光,设置其方向为 (0, 1, 0),即从上方照射const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);// 设置光的强度为 0.7light.intensity = 0.7;// 创建一个球体,直径为 2,分段数为 32(更高的分段数会使球体更平滑)const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2, segments: 32 }, scene);// 将球体的 Y 轴位置设置为 1,使其悬浮在地面上方sphere.position.y = 1;// 创建一个地面,宽度和高度均为 6const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 6, height: 6 }, scene);// 返回创建好的场景return scene;};// 调用 createScene 函数创建场景const scene = createScene();// 启动渲染循环,每帧调用 scene.render() 渲染场景engine.runRenderLoop(function () {scene.render();});// 监听窗口大小变化事件,调整引擎的画布大小window.addEventListener("resize", function () {engine.resize();});</script>
</body>
</html>
修改:单击球体可演示弹跳——模拟重力下的地面上的小球弹跳情况,单击球体弹跳。
效果图:
源码如下:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Babylon.js 示例:单击球体可演示弹跳</title><style>html, body {width: 100%;height: 100%;margin: 0;padding: 0;overflow: hidden;}#renderCanvas {width: 100%;height: 100%;touch-action: none;}</style>
</head>
<body><canvas id="renderCanvas"></canvas><script src="https://cdn.babylonjs.com/babylon.js"></script><script>const canvas = document.getElementById("renderCanvas");const engine = new BABYLON.Engine(canvas, true);// --- 全局变量声明 ---let sphere; // 将 sphere 提升到全局作用域let isBouncing = false;let verticalSpeed = 0;const gravity = -9.8; // 使用更真实的重力值const damping = 0.7;const initialJumpForce = 5; // 调整初始弹跳力const createScene = function () {const scene = new BABYLON.Scene(engine);// 相机设置const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);camera.setTarget(BABYLON.Vector3.Zero());camera.attachControl(canvas, true);// 灯光设置const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);light.intensity = 0.7;// 创建球体(确保 sphere 是全局变量)sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2, segments: 32 }, scene);sphere.position.y = 1; // 初始位置高于地面// 创建地面const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 6, height: 6 }, scene);// --- 点击事件绑定 ---sphere.actionManager = new BABYLON.ActionManager(scene);sphere.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPickTrigger,() => {if (!isBouncing) {verticalSpeed = initialJumpForce;isBouncing = true;}}));return scene;};const scene = createScene();// --- 渲染循环修复 ---engine.runRenderLoop(() => {if (isBouncing) {// 使用更精确的时间计算(deltaTime 单位为毫秒)verticalSpeed += gravity * scene.deltaTime / 1000;sphere.position.y += verticalSpeed * scene.deltaTime / 1000;// 地面碰撞检测(地面Y坐标为0,球体半径为1)if (sphere.position.y <= 1) {sphere.position.y = 1; // 防止穿透地面verticalSpeed = -verticalSpeed * damping;// 停止条件if (Math.abs(verticalSpeed) < 0.5) {isBouncing = false;verticalSpeed = 0;}}}scene.render();});window.addEventListener("resize", () => {engine.resize();});</script>
</body>
</html>
Babylon.js 实现滚球吃金币游戏
游戏说明
☆操作方式
方向键 或 WASD:控制球体滚动,注意控制不要让球在场地边缘跌落。60秒后,可用空格键重新开始游戏。
☆规则
每收集一个金币(球体碰到金币)得10分并移除金币;
60秒内尽可能获得更高分数。
运行效果:
源码如下:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>滚球吃金币 - 修复版</title><style>html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }#renderCanvas { width: 100%; height: 100%; touch-action: none; }#ui { position: fixed; top: 20px; left: 20px; color: white; font-family: Arial; }</style>
</head>
<body><canvas id="renderCanvas"></canvas><div id="ui"><div>分方向键 或 WASD:控制球体滚动,注意控制不要让球在场地边缘跌落。60秒后,可用空格键重新开始游戏。</div> <div>分数: <span id="score">0</span></div><div>时间: <span id="timer">30</span>秒</div><div id="gameOver" style="display: none;">游戏结束! 最终分数: <span id="finalScore">0</span></div></div><script src="https://cdn.babylonjs.com/babylon.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script><script>const canvas = document.getElementById("renderCanvas");const engine = new BABYLON.Engine(canvas, true);const scene = new BABYLON.Scene(engine);let score = 0, timeLeft = 30, isGameActive = false, sphere;//确保输入焦点在 Canvas 上window.addEventListener("load", () => {canvas.focus();});// 物理引擎初始化scene.enablePhysics(new BABYLON.Vector3(0, -9.81, 0), new BABYLON.CannonJSPlugin());// 光源const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);light.intensity = 0.7;// 相机(彻底禁用鼠标控制)const camera = new BABYLON.ArcRotateCamera("camera",Math.PI / 2,Math.PI / 4,10,new BABYLON.Vector3(0, 2, 0),scene);camera.inputs.clear(); // 清除所有输入camera.attachControl(canvas, false); // 禁用默认控制// 地面const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 10, height: 10 }, scene);ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground,BABYLON.PhysicsImpostor.BoxImpostor,{ mass: 0, friction: 0.5, restitution: 0.3 },scene);// 玩家球体const createPlayer = () => {sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 0.8 }, scene);sphere.position.y = 1;sphere.physicsImpostor = new BABYLON.PhysicsImpostor(sphere,BABYLON.PhysicsImpostor.SphereImpostor,{ mass: 1, friction: 0.2, restitution: 0.3 },scene);};// 金币生成const coins = [];const generateCoin = () => {const coin = BABYLON.MeshBuilder.CreateCylinder("coin", { diameter: 0.5, height: 0.1 }, scene);coin.position.set((Math.random() - 0.5) * 8, 0.5, (Math.random() - 0.5) * 8);coin.material = new BABYLON.StandardMaterial("coinMaterial", scene);coin.material.diffuseColor = new BABYLON.Color3(1, 0.84, 0);coin.physicsImpostor = new BABYLON.PhysicsImpostor(coin, BABYLON.PhysicsImpostor.CylinderImpostor, { mass: 0 }, scene);coins.push(coin);};// 输入控制(修复后的统一处理)const setupControls = () => {const inputMap = {};scene.actionManager = new BABYLON.ActionManager(scene);scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyDownTrigger, (evt) => {inputMap[evt.sourceEvent.key.toLowerCase()] = true;}));scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyUpTrigger, (evt) => {inputMap[evt.sourceEvent.key.toLowerCase()] = false;}));scene.onBeforeRenderObservable.add(() => {if (!isGameActive) return;const force = 5, forceVector = new BABYLON.Vector3(0, 0, 0);if (inputMap["arrowup"] || inputMap["w"]) forceVector.z -= force;if (inputMap["arrowdown"] || inputMap["s"]) forceVector.z += force;if (inputMap["arrowleft"] || inputMap["a"]) forceVector.x += force;if (inputMap["arrowright"] || inputMap["d"]) forceVector.x -= force;if (forceVector.length() > 0) {sphere.physicsImpostor.applyForce(forceVector, sphere.getAbsolutePosition());}});};// 碰撞检测const checkCollisions = () => {scene.onBeforeRenderObservable.add(() => {if (!isGameActive) return;coins.forEach((coin, index) => {if (coin.intersectsMesh(sphere)) {coin.dispose();coins.splice(index, 1);score += 10;document.getElementById("score").textContent = score;generateCoin();}});});};// 游戏逻辑const startGame = () => {isGameActive = true;score = 0; timeLeft = 60; //document.getElementById("score").textContent = "0";document.getElementById("timer").textContent = "60"; //document.getElementById("gameOver").style.display = "none";if (sphere) sphere.dispose();coins.forEach(coin => coin.dispose());coins.length = 0;createPlayer();for (let i = 0; i < 5; i++) generateCoin();const timer = setInterval(() => {timeLeft--;document.getElementById("timer").textContent = timeLeft;if (timeLeft <= 0) {clearInterval(timer);isGameActive = false;document.getElementById("gameOver").style.display = "block";document.getElementById("finalScore").textContent = score;}}, 500); //};// 初始化setupControls();checkCollisions();startGame();window.addEventListener("keydown", (e) => {if (e.code === "Space" && !isGameActive) startGame();});engine.runRenderLoop(() => scene.render());window.addEventListener("resize", () => engine.resize());</script>
</body>
</html>