前言
为了满足工作需求,我已着手学习Three.js,并决定详细记录这一学习过程。在此旅程中,如果出现理解偏差或有其他更佳的学习方法,请大家不吝赐教,在评论区给予指正或分享您的宝贵建议,我将不胜感激。
搭建一个threejs项目
请参考hello,正方体!创建threejs项目部分。
1. 规划项目组织结构
模块化软件设计
模块化软件设计是一种软件开发方法,其核心思想是将一个复杂的系统分解为多个相互独立、功能单一的模块。每个模块负责完成特定的功能,这样可以使得软件的开发、维护、测试和重用变得更加容易和高效。模块化设计遵循“高内聚,低耦合”的原则:
- 高内聚:意味着每个模块内部的元素(函数、类等)紧密相关,共同完成一个明确的任务。这样可以确保模块内部逻辑清晰,易于理解和修改。
- 低耦合:表示不同模块之间的依赖关系和交互尽可能减少,每个模块对外部的依赖仅限于必要的接口。这样做的好处是可以独立地开发、测试每个模块,以及在不影响其他模块的情况下修改或替换某个模块。
- 在根目录下创建World文件夹并在该文件夹内创建World.js。
World.js:添加如下
class World {#scene; // 场景#camera; // 相机#renderer;// 渲染/*** @param {Element} container - 容器*/constructor(container) {}/*** 渲染函数*/render() {}
}
export { World };
小结
设置私有属性,仅使用设计的接口进行交互,并且隐藏其他所有内容。
- 在src/main.js中引入World类
main.js:添加如下
import { World } from "../World/World";
// 主函数
function main() {// 获取容器const container = document.querySelector("#scene-container");// 创建一个World类的实例const world = new World(container);// 渲染场景world.render();
}
// 调用主函数
main();
- 创建文件夹
- 在World文件内新建components文件夹存放在组件,如立方体、相机和场景本身。
- 在World文件内新建systems文件夹存放在组件或其他系统上运行的东西
- 创建渲染器模块
在systems文件夹内新建renderer.js
renderer.js:添加如下
import { WebGLRenderer } from "three";/*** @description - 创建渲染器* @returns {WebGLRenderer} - 渲染器实例*/
export const createRenderder = () => {// 创建WebGLRenderer类的一个实例const renderer = new WebGLRenderer();return renderer;
};
- 创建场景模块
在components内新建scene.js
scene.js:添加如下
import { Scene, Color } from "three";/*** @description - 创建场景* @returns {Scene} - 场景实例*/
export const createScene = () => {// 创建WebGLRenderer类的一个实例const scene = new Scene();// 设置场景背景颜色为天蓝色scene.background = new Color("skyblue");return scene;
};
- 创建相机模块
在components文件夹内创建camera.js文件
camera.js:添加如下
import { PerspectiveCamera } from "three";
/*** @description - 创建相机* @returns {PerspectiveCamera} - 透视相机实例*/
export const createCamera = () => {// 创建一个PerspectiveCamera类实例 并设置初始值const camera = new PerspectiveCamera(35, 1, 0.1, 100);// 设置相机位置camera.position.set(0, 0, 10);return camera;
};
小结
使用了一个虚拟值1作为纵横比(aspect),因为它依赖于container的尺寸。避免不必要地传递东西,将推迟设置纵横比。如有更好的想法,请在评论区留言,谢谢。
- 创建立方体模块
在components文件夹内创建cube.js文件
cube.js:添加如下
import { BoxGeometry, Mesh, MeshBasicMaterial } from "three";
/*** @description - 创建立方体* @returns {Mesh} - 网格实例*/
export const createCude = () => {// 创建边长为2的几何体(就是边长2米)const geometry = new BoxGeometry(2, 2, 2);// 创建一个默认基础材质(白色)const material = new MeshBasicMaterial();// 创建一个网格添加几何体和材质const cube = new Mesh(geometry, material);return cube;
};
- 创建大小模块
在systems文件夹内创建Resizer.js(文件名以大写 R 开头表示它是一个类)
Resizer.js:添加如下
class Resizer {constructor() {}
}
export { Resizer };
- 设置World类
- 1.World.js中引入刚刚创建的五个模块。
- 2.设置场景,相机,渲染。
- 3.将画布添加到容器中。
- 4.渲染场景。
- 5.创建立方体并添加如场景中。
World.js:添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createRenderder } from "./systems/renderer";
import { Resizer } from "./systems/Resizer";
class World {#scene; // 场景#camera; // 相机#renderer;// 渲染/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();container.append(this.#renderer.domElement);const cube = createCude();this.#scene.add(cube);}/*** 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}
}
export { World };
- 设置Resizer类
Resizer.js:添加如下
import { PerspectiveCamera, WebGLRenderer } from "three";
class Resizer {/*** @param {Element} container - 容器* @param {PerspectiveCamera} camera - 相机* @param {WebGLRenderer} renderer - 渲染器*/constructor(container, camera, renderer) {// 设置纵横比camera.aspect = container.clientWidth / container.clientHeight;// 更新平截头体camera.updateProjectionMatrix();// 设置渲染器大小renderer.setSize(container.clientWidth, container.clientHeight);// 设置设备像素大小 这是防止 HiDPI 显示器模糊所必需的 (也称为视网膜显示器)。renderer.setPixelRatio(window.devicePixelRatio);}
}
export { Resizer };
小结
平截头体不会自动重新计算,因此当我们更改存储在camera.aspect、camera.fov、camera.near和camera.far中的任何这些设置时,我们还需要更新平截头体。
- 在World类构造函数中创建一个Resizer实例
World.js:添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createRenderder } from "./systems/renderer";
import { Resizer } from "./systems/Resizer";
class World {#scene; // 场景#camera; // 相机#renderer;// 渲染/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();container.append(this.#renderer.domElement);const cube = createCude();this.#scene.add(cube);new Resizer(container, this.#camera, this.#renderer);}/*** 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}
}
export { World };
小结
至此规划项目组织结构已经结束了,现在运行项目看看吧。
2. 基于物理的渲染和照明
基于物理的渲染 (PBR)已成为渲染实时和电影 3D 场景的行业标准方法。顾名思义,这种渲染技术使用真实世界的物理学来计算表面对光的反应方式,从而避免在场景中设置材质和照明时进行猜测。
- 启用物理上正确的光照
renderer.js:启用物理正确的照明
import { WebGLRenderer } from "three";/*** @description - 创建渲染器* @returns {WebGLRenderer} - 渲染器实例*/
export const createRenderder = () => {// 创建WebGLRenderer类的一个实例const renderer = new WebGLRenderer();// 启用物理上正确的光照renderer.physicallyCorrectLights = true;return renderer;
};
- 添加一个DirectionalLight到我们的场景
在components目录下创建light.js文件
light.js:添加如下
import { DirectionalLight } from "three";
/*** @description - 直接照明 (阳光)* @returns {DirectionalLight} - 直照光照实例*/
export const createLights = () => {// 创建一个直照光照实例并设置颜色是白色强度为8const light = new DirectionalLight("white", 8);// 设置光源位置 现在灯光从(10,10,10)照向(0,0,0)。light.position.set(10, 10, 10);return light;
};
小结
DirectionalLight设计的目的是模仿遥远的光源,例如太阳。来自DirectionalLight的光线不会随着距离而消失。场景中的所有对象都将被同样明亮地照亮,无论它们放在哪里——即使是在灯光后面。DirectionalLight的光线是平行的,从一个位置照向一个目标。默认情况下,目标放置在我们场景的中心(点(0,0,0)),所以当我们移动周围的光线时,它总是会向中心照射。
- 在World.js中,导入新模块并使用
World.js:添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createRenderder } from "./systems/renderer";
import { createLights } from "./components/lights";
import { Resizer } from "./systems/Resizer";
class World {#scene; // 场景#camera; // 相机#renderer;// 渲染/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();container.append(this.#renderer.domElement);const cube = createCude();const light = createLights();this.#scene.add(cube, light);new Resizer(container, this.#camera, this.#renderer);}/*** 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}
}
export { World };
- 切换材质MeshStandardMaterial
cube.js:添加如下
import { BoxGeometry, Mesh, MeshStandardMaterial } from "three";
/*** @description - 创建立方体* @returns {Mesh} - 网格实例*/
export const createCude = () => {// 创建边长为2的几何体(就是边长2米)const geometry = new BoxGeometry(2, 2, 2);// 创建一个高质量、通用、物理精确的材料 设置颜色为紫色const material = new MeshStandardMaterial({ color: "purple" }); // 创建一个网格添加几何体和材质const cube = new Mesh(geometry, material);// 旋转立方体cube.rotation.set(-0.5, -0.1, 0.8);return cube;
};
小结
用 MeshStandardMaterial代替基本材料MeshBasicMaterial。这是一种高质量、通用、物理精确的材料,可以使用真实世界的物理方程对光做出反应。
总结
在向场景添加灯光之前,我们将切换到使用物理上正确的光照强度计算。
创建物理大小的场景,为了使物理上正确的照明准确,如果你的房间有 1000 公里宽,那么使用真实灯泡的数据是没有意义的!
three.js 中的大小单位是米。我们之前创建的2×2×2的立方体每边长为两米。camera.far = 100意味着我们可以看到一百米的距离。camera.near = 0.1意味着距离相机不到十厘米的物体将不可见。使用米为单位是一种约定,而不是规则。如果不遵循它,那么除了物理上精确的照明之外的一切都仍然有效。但是,如果想要物理上准确的照明,那么必须使用以下公式将场景构建到真实世界的规模:1单位 = 1米。
即使我们使用 PBR,现实世界和 three.js 之间的一个区别是默认情况下对象不会阻挡光线。光路径中的每个物体都会收到照明,即使路上有一堵墙。落在物体上的光会照亮它,但也会直接穿过并照亮后面的物体。物理正确性就这么多!
至此已经全部完成。你好,正方体2!如果出现理解偏差或有其他更佳的学习方法,请大家不吝赐教,在评论区给予指正或分享您的宝贵建议,我将不胜感激。
主要文献
three.js官网
《discoverthreejs》