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

【初体验 threejs】【学习】【笔记】hello,正方体 3!

前言

为了满足工作需求,我已着手学习 Three.js,并决定详细记录这一学习过程。在此旅程中,如果出现理解偏差或有其他更佳的学习方法,请大家不吝赐教,在评论区给予指正或分享您的宝贵建议,我将不胜感激。

项目基础

请参考hello,正方体 2!。

1. 抗锯齿

除非直线完全水平或垂直,否则使用方形像素绘制直线是很困难的。我们将使用一种称为抗锯齿(AA) 的技术来解决这个问题。

1.1 多重采样抗锯齿 (MSAA)

抗锯齿是使用内置的 WebGL 方法执行的,即 多重采样抗锯齿 (MSAA)。

1.2 启用抗锯齿

renderer.js: 添加如下

import { WebGLRenderer } from "three";/*** @description - 创建渲染器* @returns {WebGLRenderer} - 渲染器实例*/
export const createRenderder = () => {// 创建WebGLRenderer类的一个实例// 打开抗锯齿antialiasconst renderer = new WebGLRenderer({ antialias: true });// 启用物理上正确的光照renderer.physicallyCorrectLights = true;return renderer;
};

注意
一旦创建了渲染器,就无法更改此设置。

2. 浏览器窗口大小变化

目的是为了用户调整预览窗口的大小,场景适应新的大小。

2.1 扩展 Resizer 类
  1. 因为初始化时要设置初始大小,页面变化时也要重新设置大小,所有要把这部分设置代码移入到一个函数里。
  2. 将使用 ResizeObserver 来监听容器的大小变化。
  3. 因为我们只调用了.render 一次,它在画布中绘制了一个帧。当画布被调整大小时,这个框架被拉伸以适应新的大小。所以要创建一个 onResize 钩子,每次调整大小事件触发时生成一个新帧。
  4. 在 World 中使用 onResize 函数。

Resizer.js:添加如下

import { PerspectiveCamera, WebGLRenderer } from "three";
class Resizer {#container; // 容器#camera; // 相机#renderer; // 渲染器onResize = () => {}; // 页面大小变化钩子函数/*** @param {Element} container - 容器* @param {PerspectiveCamera} camera - 相机* @param {WebGLRenderer} renderer - 渲染器*/constructor(container, camera, renderer) {this.#container = container;this.#camera = camera;this.#renderer = renderer;// 初始化sizethis.#setSize();// 监听容器大小变化const resizeObserver = new ResizeObserver(() => {this.#setSize();this.onResize();});resizeObserver.observe(container);}#setSize() {this.#camera.aspect =this.#container.clientWidth / this.#container.clientHeight;this.#camera.updateProjectionMatrix();this.#renderer.setSize(this.#container.clientWidth,this.#container.clientHeight);this.#renderer.setPixelRatio(window.devicePixelRatio);}
}
export { Resizer };

World.js:添加如下

import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createLights } from "./components/lights";
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();const light = createLights();this.#scene.add(cube, light);const resizer = new Resizer(container, this.#camera, this.#renderer);resizer.onResize = () => {this.render();};}/*** @description - 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}
}export { World };

3. 动画循环(运动)

为立方体添加一个简单的旋转动画。

  1. 创建一个动画循环
    创建 systems/Loop.js 模块并在其中创建一个新 Loop 类
import { PerspectiveCamera, Scene, WebGLRenderer } from "three";
class Loop {#camera; // 相机#scene; // 场景#renderer; // 渲染/*** @param {PerspectiveCamera} camera - 相机* @param {Scene} scene - 场景* @param {WebGLRenderer} renderer - 渲染*/constructor(camera, scene, renderer) {this.#camera = camera;this.#scene = scene;this.#renderer = renderer;}/*** @description - 动画循环开始*/start() {}/*** @description - 动画循环结束*/stop() {}
}export { Loop };
  1. 在 World 中,将这个新类添加到导入列表中:
    World.js: 添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createLights } from "./components/lights";
import { createRenderder } from "./systems/renderer";
import { Resizer } from "./systems/Resizer";
import { Loop } from "./systems/Loop";
class World {#scene; // 场景#camera; // 相机#renderer; // 渲染#loop; // 动画循环/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();this.#loop = new Loop(this.#camera, this.#scene, this.#renderer);container.append(this.#renderer.domElement);const cube = createCude();const light = createLights();this.#scene.add(cube, light);const resizer = new Resizer(container, this.#camera, this.#renderer);resizer.onResize = () => {this.render();};}/*** @description - 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}/*** @description - 动画循环开始*/start() {this.#loop.start();}/*** @description - 动画循环结束*/stop() {this.#loop.stop();}
}export { World };
  1. 启动动画循环
    main.js:添加如下
import { World } from "../World/World";function main() {// 获取容器const container = document.querySelector("#scene-container");// 创建一个world类实例const world = new World(container);// 开始动画循环world.start();
}main();
  1. 使用.setAnimationLoop 创建循环
    在内部,循环是使用 .requestAnimationFrame。这种内置的浏览器方法可以智能地安排帧与显示器的刷新率同步,如果您的硬件跟不上,它会平滑地降低帧率。然而,.setAnimationLoop 还有一点额外的魔力可以确保循环在虚拟现实和增强现实环境中工作。
    Loop.js:添加如下
import { PerspectiveCamera, Scene, WebGLRenderer } from "three";
class Loop {#camera; // 相机#scene; // 场景#renderer; // 渲染/*** @param {PerspectiveCamera} camera - 相机* @param {Scene} scene - 场景* @param {WebGLRenderer} renderer - 渲染*/constructor(camera, scene, renderer) {this.#camera = camera;this.#scene = scene;this.#renderer = renderer;}/*** @description - 动画循环开始*/start() {this.#renderer.setAnimationLoop(() => {this.#renderer.render(this.#scene, this.#camera);});}/*** @description - 动画循环结束*/stop() {this.#renderer.setAnimationLoop(null);}
}export { Loop };
  1. 移除 onResize 钩子
    现在循环正在运行,每当我们调整窗口大小时,都会在循环的下一次迭代中生成一个新帧。这足够快,不会注意到任何延迟,因此我们不再需要在调整大小时手动重绘场景。从 World 中移除 resizer.onResize 钩子
    World.js:添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createLights } from "./components/lights";
import { createRenderder } from "./systems/renderer";
import { Resizer } from "./systems/Resizer";
import { Loop } from "./systems/Loop";
class World {#scene; // 场景#camera; // 相机#renderer; // 渲染#loop; // 动画循环/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();this.#loop = new Loop(this.#camera, this.#scene, this.#renderer);container.append(this.#renderer.domElement);const cube = createCude();const light = createLights();this.#scene.add(cube, light);new Resizer(container, this.#camera, this.#renderer);}/*** @description - 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}/*** @description - 动画循环开始*/start() {this.#loop.start();}/*** @description - 动画循环结束*/stop() {this.#loop.stop();}
}export { World };

4. 动画系统

每次循环运行时,希望通过将它们向前移动一帧来更新所有这些动画。就在我们渲染每一帧之前,我们会让立方体旋转一点点, 几乎是肉眼无法看到的微小量,但随着时间的推移会产生流畅的动画效果。

  1. Loop.tick 方法
    为了处理所有这些,我们需要一个更新所有动画的函数,并且这个函数应该在每一帧开始时运行一次。
    Loop.js:添加如下
import { PerspectiveCamera, Scene, WebGLRenderer } from "three";
class Loop {#camera; // 相机#scene; // 场景#renderer; // 渲染/*** @param {PerspectiveCamera} camera - 相机* @param {Scene} scene - 场景* @param {WebGLRenderer} renderer - 渲染*/constructor(camera, scene, renderer) {this.#camera = camera;this.#scene = scene;this.#renderer = renderer;}/*** @description - 动画循环开始*/start() {this.#renderer.setAnimationLoop(() => {this.tick();this.#renderer.render(this.#scene, this.#camera);});}/*** @description - 动画循环结束*/stop() {this.#renderer.setAnimationLoop(null);}/*** @description - 更新动画*/tick() {}
}export { Loop };
  1. cube.tick 方法
    为动画对象创建一个.tick 方法,更新自身。
    cube.js: 添加如下
import { BoxGeometry, Mesh, MeshStandardMaterial, MathUtils } 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);// 添加更新动画方法tickcube.tick = () => {cube.rotation.z += 0.01;cube.rotation.x += 0.01;cube.rotation.y += 0.01;};return cube;
};

注意
像这样在运行时向现有类添加属性称为 猴子补丁。

  1. Loop.#updatables
    循环类中的动画对象列表。
    Loop.js:添加如下
import { PerspectiveCamera, Scene, WebGLRenderer, Object3D } from "three";
class Loop {#camera; // 相机#scene; // 场景#renderer; // 渲染#updatables = []; // 循环里的动画对象列表/*** @param {PerspectiveCamera} camera - 相机* @param {Scene} scene - 场景* @param {WebGLRenderer} renderer - 渲染* @param {Object3D[]} updatables - 循环里的动画对象列表*/constructor(camera, scene, renderer, updatables = []) {this.#camera = camera;this.#scene = scene;this.#renderer = renderer;this.#updatables = updatables;}/*** @description - 动画循环开始*/start() {this.#renderer.setAnimationLoop(() => {this.tick();this.#renderer.render(this.#scene, this.#camera);});}/*** @description - 动画循环结束*/stop() {this.#renderer.setAnimationLoop(null);}/*** @description - 更新动画*/tick() {for (const object of this.#updatables) {object.tick();}}/*** @description - 添加动画对象* @param  {...Object3D} object*/addObjectToUpdatables(...object) {this.#updatables.push(...object);}/*** @description - 删除动画对象* @param  {...Object3D} object*/removeObjectFromUpdatables(...object) {this.#updatables = this.#updatables.filter((obj) => !object.includes(obj));}
}export { Loop };
  1. 添加 cube 到 Loop.#updatables
    World.js:添加如下
import { createScene } from "./components/scene";
import { createCamera } from "./components/camera";
import { createCude } from "./components/cube";
import { createLights } from "./components/lights";
import { createRenderder } from "./systems/renderer";
import { Resizer } from "./systems/Resizer";
import { Loop } from "./systems/Loop";
class World {#scene; // 场景#camera; // 相机#renderer; // 渲染#loop; // 动画循环/*** @param {Element} container - 容器*/constructor(container) {this.#scene = createScene();this.#camera = createCamera();this.#renderer = createRenderder();this.#loop = new Loop(this.#camera, this.#scene, this.#renderer);container.append(this.#renderer.domElement);const cube = createCude();this.#loop.addObjectToUpdatables(cube);const light = createLights();this.#scene.add(cube, light);new Resizer(container, this.#camera, this.#renderer);}/*** @description - 渲染函数*/render() {this.#renderer.render(this.#scene, this.#camera);}/*** @description - 动画循环开始*/start() {this.#loop.start();}/*** @description - 动画循环结束*/stop() {this.#loop.stop();}
}export { World };

小结
立方体应该立即开始旋转。

5. 将动画速度与帧速率解耦

我们动画的速度取决于观看它的设备。我们的动画循环不会以固定速率生成帧。这意味着,在 60Hz 屏幕上,目标帧率为 60FPS,在 90Hz 屏幕上,目标帧率为 90FPS,以此类推。
在每一种情况下,动画循环都会以较低的速率生成帧,并且这个速率可能会因为许多因素从一个时刻到下一个时刻波动。这称为可变帧速率
为了防止这种情况,我们需要将动画速度与帧速率解耦。我们将这样做:当我们告诉一个对象.tick 前进一帧时,我们将根据前一帧花费的时间来缩放移动的大小。

  1. 使用 Clock 类
    用 Clock.getDelta 来衡量前一帧花了多长时间。.getDelta 告诉我们自上次调用.getDelta 以来已经过去了多少时间。
    将结果保存在一个名为 delta 的变量中,然后我们将其传递给每个动画对象的.tick 方法。
    Loop.js:添加如下
import {PerspectiveCamera,Scene,WebGLRenderer,Object3D,Clock,
} from "three";
class Loop {#camera; // 相机#scene; // 场景#renderer; // 渲染#updatables = []; // 循环里的动画对象列表#clock; // 秒表/*** @param {PerspectiveCamera} camera - 相机* @param {Scene} scene - 场景* @param {WebGLRenderer} renderer - 渲染* @param {Object3D[]} updatables - 循环里的动画对象列表*/constructor(camera, scene, renderer, updatables = []) {this.#camera = camera;this.#scene = scene;this.#renderer = renderer;this.#updatables = updatables;this.#clock = new Clock();}/*** @description - 动画循环开始*/start() {this.#renderer.setAnimationLoop(() => {this.tick();this.#renderer.render(this.#scene, this.#camera);});}/*** @description - 动画循环结束*/stop() {this.#renderer.setAnimationLoop(null);}/*** @description - 更新动画*/tick() {// .getDelta告诉我们自上次调用.getDelta以来已经过去了多少时间。const delta = this.#clock.getDelta();for (const object of this.#updatables) {object.tick(delta);}}/*** @description - 添加动画对象* @param  {...Object3D} object*/addObjectToUpdatables(...object) {this.#updatables.push(...object);}/*** @description - 删除动画对象* @param  {...Object3D} object*/removeObjectFromUpdatables(...object) {this.#updatables = this.#updatables.filter((obj) => !object.includes(obj));}
}export { Loop };

cube.js:添加如下

import { BoxGeometry, Mesh, MeshStandardMaterial, MathUtils } 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);// 度数转弧度const radiansPerSecond = MathUtils.degToRad(30);// 添加更新动画方法tickcube.tick = (delta) => {const radian = radiansPerSecond * delta;cube.rotation.z += radian;cube.rotation.x += radian;cube.rotation.y += radian;};return cube;
};

6. 总结

至此已经全部完成。你好,正方体 3!如果出现理解偏差或有其他更佳的学习方法,请大家不吝赐教,在评论区给予指正或分享您的宝贵建议,我将不胜感激。

主要文献

three.js 官网
《discoverthreejs》

http://www.lryc.cn/news/374694.html

相关文章:

  • 第04章:IDEA的安装与使用
  • [原创][Delphi多线程]使用TMonitor, TEvent和TQueue配合实现TThreadQueue的经典使用案例.
  • 6.12ctf练习
  • 海豚调度异常处理: 使用 arthas 在内存中删除启动失败的工作流
  • 在Qt中,QSerialPort::write(data) 和 readAll() 有什么关联和联系
  • 第 2 章:Spring Framework 中的 IoC 容器
  • 构造函数、实例、原型对象三者之间的关系
  • 人工智能抢走了他们的工作。现在他们得到报酬,让它听起来像人类
  • 大模型微调出错的解决方案(持续更新)
  • 企业多云策略的优势与实施指南
  • vue分页
  • 服务器上设置pnpm环境变量
  • Java中BIO、NIO、AIO详解
  • cloud_enum:一款针对不同平台云环境安全的OSINT工具
  • 图像的对比度和亮度
  • 手撕设计模式——计划生育之单例模式
  • Mac M3 Pro 部署Flink-1.16.3
  • Mysql 的分布式策略
  • 记录一个利用winhex进行图片隐写分离的
  • 压缩映射定理证明
  • Ubuntu20.04.6操作系统安装教程
  • (分治算法3)leecode 53 最大子数组和(最大子段和)
  • 【C++】模板初级
  • eslint 使用单引号,Prettier使用双引号冲突
  • 进化生物学的数学原理 知识点总结
  • 如何挑到高质量的静态IP代理?
  • vagrant putty错误的解决
  • 图像分割——U-Net论文介绍+代码(PyTorch)
  • C#进阶-ASP.NET的WebService跨域CORS问题解决方案
  • 如何利用TikTok矩阵源码实现自动定时发布和高效多账号管理