news 2026/2/18 6:39:37

ThreeJs场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ThreeJs场景

场景结构

场景在 3D 引擎是一个图中节点的层次结构,其中每个节点代表了一个局部空间(local space)。

示例

假设我们需要做一个太阳系的例子。对于太阳来说,它只需要保持自身不动,那么他的child有水金地火木土星……这些child可能还有自己的卫星,比如月球绕着地球转。但是对于月球来说,它需要关注太阳在哪吗?当然不用。月球只需要关注它的parent地球,绕着地球转动即可。同样的假设月球上有个宇航员,他不需要关注太阳和地球怎么动,他的移动只需要相对于月球即可。

我们可以使用一个场景Scene来模仿上述的例子。

  1. 创建中心的太阳

    // 要更新旋转角度的对象数组 const objects = []; // 一球多用 const radius = 1; // 设置球的半径 const widthSegments = 20; // 设置球的边数 const heightSegments = 20; // 设置球的边数 const sphereGeometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments ); const sunMaterial = new THREE.MeshPhongMaterial({ emissive: 0xffff00 }); const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial); sunMesh.scale.set(5, 5, 5); // 扩大太阳的大小 scene.add(sunMesh); objects.push(sunMesh);

    放射属性(emissive)是基本上不受其他光照影响的固有颜色。光照会被添加到该颜色上。

    要注意:要让太阳能够正确显示,我们需要按需设置camera,这决定了相机的可视范围。

    // 创建透视摄像机 const fov = 75; const aspect = 2; // 相机默认值 const near = 0.1; const far = 100; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(0, 50, 0); camera.up.set(0, 0, 1); camera.lookAt(0, 0, 0);

    我们要创建一个俯视图,所以把相机设置在了y轴+50的方向上,同时需要设置对于摄像机朝上的up位置(想象我们俯视(0,0,0)位置的太阳,我们相机的上方应该在z轴方向上),然后我们需要使用lookAt让其看向原点。

    相当于三步:确定相机位置上下聚焦点

    只有这三点都确定,我们才能完全确定一个相机的摆放方式。

    注意我们需要调整far,沿用之前的far=5会让离相机远的object显示不出来。

    创建完成后,我们想让太阳旋转起来,只需要调整objects数组,设定其旋转方式。

    objects.forEach((obj) => { obj.rotation.y = time; });

    这个object的坐标应该是对于创建的3dObject,它会随着时间绕y轴旋转time的角度。

  2. 创建地球

    // 创建地球 const earthMaterial = new THREE.MeshPhongMaterial({ color: 0x2233ff, emissive: 0x112244, }); const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial); earthMesh.position.x = 10; scene.add(earthMesh); objects.push(earthMesh);
    const widthSegments = 10; // 设置球的边数 const heightSegments = 10; // 设置球的边数

    我们可以适当设置球的边数,这样更容易看出来它在旋转。

    在这种情况下,我们虽然可以看到地球正确绘制并自转了,但它并没有绕着太阳公转。

    sunMesh.add(earthMesh);

    我们不应该把earth添加到场景scene中,而是直接添加给sun.

    但是地球居然变得和太阳一样大,而且会飞出视野范围。

    我们让earthMesh成为sunMesh的一个子节点。sunMesh.scale.set(5, 5, 5)将其比例设置为 5x。这意味着sunMeshs的局部空间是 5 倍大。这表示地球现在大了 5 倍,它与太阳的距离 (earthMesh.position.x = 10) 也是 5 倍。

    为了解决这个问题,我们添加一个空的场景图节点。我们将把太阳和地球都作为该节点的子节点。

    // 创建太阳系容器 const solarSystem = new THREE.Object3D(); scene.add(solarSystem); objects.push(solarSystem);

    这里我们创建了一个 [Object3D]。像 [Mesh]一样,它也是场景图中的一个节点,但与 [Mesh] 不同的是,它没有材质(material)和几何体(geometry)。它只是代表一个局部空间。新的场景图变为:

  3. 创建月球

延续同样的模式,我们再加一个月亮。

新的场景图应该像这样:

雨是我们再次添加了更多的隐形场景图节点。首先是一个名为earthOrbit的 [Object3D],并将新增earthMeshmoonOrbit都添加到其中。然后,我们把moonMesh添加到moonOrbit上。

轴可视化

[AxesHelper]。它画了 3 条线,分别代表本地的 X(红色), Y(绿色), 以及 Z(蓝色)轴。

// 为每个节点添加一个AxesHelper objects.forEach((node) => { const axes = new THREE.AxesHelper(); axes.material.depthTest = false; axes.renderOrder = 1; node.add(axes); });

我们希望轴即使在球体内部也能出现。要做到这一点,我们将其材质(material)的depthTest属性设置为 false,这意味着它们不会检查其是否在其他东西后面进行绘制。我们还将它们的renderOrder属性设置为 1(默认值为 0),这样它们就会在所有球体之后被绘制。否则一个球体可能会画在它们上面,把它们遮住。

可能很难看到其中一些轴,因为有 2 对重叠的轴。sunMeshsolarSystem都在同一位置。同样地,earthMeshearthOrbit也在同一位置。让我们添加一些简单的控制方法,让我们可以为每个节点打开/关闭它们。同时,我们还可以添加另一个名为 [GridHelper]的帮助工具。它可以在 X,Z 平面上创建一个 2D 网格。默认情况下,网格是 10x10 单位。

使用[lil-gui],这是一个在 three.js 项目中非常流行的 UI 库。lil-gui 会获取一个对象和该对象上的属性名,并根据属性的类型自动生成一个 UI 来操作该属性。

我们要为每个节点制作一个 [GridHelper] 和一个 [AxesHelper]。我们需要为每个节点添加一个标签,所以我们将删除旧的循环,转而调用一些函数为每个节点添加帮助程序。

需要注意的是,我们将 [AxesHelper]的renderOrder设置为 2,将[GridHelper]的设置为 1,这样轴就会在网格之后绘制。否则网格可能会覆盖轴。

场景图示例

在建立场景图之前,我们需要明确每个场景的具体架构。

在一个简单的游戏世界中,一辆汽车可能有这样的场景图。

如果你移动车体,所有的轮子都会随之移动。如果你想让车身和轮子分开弹跳,你可以将车身和轮子作为代表汽车框架的框架(frame)节点的子节点。

对于游戏世界中的人类:

对于一辆简单的坦克:

日地月代码示例

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <style> /* 居中显示并留白,背景和卡片式样式 */ html, body { height: 100%; margin: 0; background: #000000; display: flex; align-items: center; justify-content: center; } /* 画布占满容器 */ canvas { display: block; width: 98vw; height: 98vh; border-radius: 4px; background: #000; /* 可选,避免未渲染时看起来空白 */ } </style> <body> <canvas id="c"></canvas> <script type="module"> // 引入ThreeJs import * as THREE from 'three'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; // 渲染(WebGL渲染器) // 自己创建的canvas在使用时候会更灵活(直接对canvas进行修改) const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({antialias: true, canvas}); const gui = new GUI(); // 创建透视摄像机 const fov = 75; const aspect = 2; // 相机默认值 const near = 0.1; const far = 100; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(0, 50, 0); camera.up.set(0, 0, 1); camera.lookAt(0, 0, 0); // camera.position.z = 4; // 设置相机位置 // 创建场景 const scene = new THREE.Scene(); scene.background = new THREE.Color(0xAAAAAA); // 创建灯光(三维效果会更明显) const color = 0xFFFFFF; const intensity = 3; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); // 要更新旋转角度的对象数组 const objects = []; // 一球多用 const radius = 1; // 设置球的半径 const widthSegments = 10; // 设置球的边数 const heightSegments = 10; // 设置球的边数 const sphereGeometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments ); // 创建太阳系容器 const solarSystem = new THREE.Object3D(); scene.add(solarSystem); objects.push(solarSystem); // 创建太阳 const sunMaterial = new THREE.MeshPhongMaterial({ emissive: 0xffff00 }); const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial); sunMesh.scale.set(5, 5, 5); // 扩大太阳的大小 solarSystem.add(sunMesh); objects.push(sunMesh); // 创建地球轨道容器 const earthOrbit = new THREE.Object3D(); earthOrbit.position.x = 10; solarSystem.add(earthOrbit); objects.push(earthOrbit); // 创建地球 const earthMaterial = new THREE.MeshPhongMaterial({ color: 0x2233ff, emissive: 0x112244, }); const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial); earthOrbit.add(earthMesh); objects.push(earthMesh); // 创建月球轨道容器 const moonOrbit = new THREE.Object3D(); moonOrbit.position.x = 2; earthOrbit.add(moonOrbit); // 创建月球 const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222}); const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial); moonMesh.scale.set(.5, .5, .5); moonOrbit.add(moonMesh); objects.push(moonMesh); // 打开/关闭轴和网格的可见性 // lil-gui 要求一个返回类型为bool型的属性来创建一个复选框,所以我们为 visible属性 // 绑定了一个setter 和 getter。 从而让lil-gui去操作该属性. class AxisGridHelper { constructor(node, units = 10) { const axes = new THREE.AxesHelper(); axes.material.depthTest = false; axes.renderOrder = 2; // 在网格渲染之后再渲染 node.add(axes); const grid = new THREE.GridHelper(units, units); grid.material.depthTest = false; grid.renderOrder = 1; node.add(grid); this.grid = grid; this.axes = axes; this.visible = false; } get visible() { return this._visible; } set visible(v) { this._visible = v; this.grid.visible = v; this.axes.visible = v; } } function makeAxisGrid(node, label, units) { const helper = new AxisGridHelper(node, units); gui.add(helper, 'visible').name(label); } makeAxisGrid(solarSystem, 'solarSystem', 25); makeAxisGrid(sunMesh, 'sunMesh'); makeAxisGrid(earthOrbit, 'earthOrbit'); makeAxisGrid(earthMesh, 'earthMesh'); makeAxisGrid(moonOrbit, 'moonOrbit'); makeAxisGrid(moonMesh, 'moonMesh'); // 是否需要调整渲染器大小以适应显示尺寸 function resizeRendererToDisplaySize(renderer) { const canvas = renderer.domElement; const pixelRatio = window.devicePixelRatio; const width = Math.floor( canvas.clientWidth * pixelRatio ); const height = Math.floor( canvas.clientHeight * pixelRatio ); const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); } return needResize; } function render(time) { time *= 0.001; // 将时间单位变为秒 // 调整渲染器大小以适应显示尺寸 if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; // 更新相机宽高比 camera.updateProjectionMatrix(); // 更新投影矩阵 } objects.forEach((obj) => { obj.rotation.y = time; }); renderer.render(scene, camera); requestAnimationFrame(render); } requestAnimationFrame(render); </script> </body> </html>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/17 9:07:59

wazero在物联网嵌入式设备中的创新部署实践指南

wazero在物联网嵌入式设备中的创新部署实践指南 【免费下载链接】AI_NovelGenerator 使用ai生成多章节的长篇小说&#xff0c;自动衔接上下文、伏笔 项目地址: https://gitcode.com/GitHub_Trending/ai/AI_NovelGenerator wazero作为零依赖的WebAssembly运行时&#xff…

作者头像 李华
网站建设 2026/2/14 12:30:48

ARMv8-A权威指南:掌握下一代处理器核心技术

ARMv8-A权威指南&#xff1a;掌握下一代处理器核心技术 【免费下载链接】ARMv8架构参考手册下载分享 ARMv8架构参考手册下载 项目地址: https://gitcode.com/Open-source-documentation-tutorial/1df35 开启ARM架构学习之旅 你是否曾经为理解ARM处理器底层原理而苦恼&a…

作者头像 李华
网站建设 2026/2/6 20:44:46

打造完美智能家居:5个Home Assistant入门必知要点

打造完美智能家居&#xff1a;5个Home Assistant入门必知要点 【免费下载链接】awesome-home-assistant A curated list of amazingly awesome Home Assistant resources. 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-home-assistant Home Assistant作为开源智…

作者头像 李华
网站建设 2026/2/16 23:51:06

K-Diffusion扩散模型终极指南:从快速上手到实战精通

K-Diffusion扩散模型终极指南&#xff1a;从快速上手到实战精通 【免费下载链接】k-diffusion Karras et al. (2022) diffusion models for PyTorch 项目地址: https://gitcode.com/gh_mirrors/kd/k-diffusion K-Diffusion是基于PyTorch实现的先进扩散模型库&#xff0c…

作者头像 李华
网站建设 2026/2/17 21:17:45

解锁GloVe词向量的实战指南:从零构建语义理解引擎

嘿&#xff0c;朋友们&#xff01;今天咱们来聊聊那个让自然语言处理变得简单高效的神器——GloVe词向量。如果你曾经为理解文本语义而头疼&#xff0c;或者想在项目中快速集成词向量功能&#xff0c;那么这篇文章就是为你量身打造的。 【免费下载链接】GloVe Software in C an…

作者头像 李华