![]() |
卖萌的水桶 · 如何将rtf文件合并_百度知道· 2 月前 · |
![]() |
听话的牛肉面 · Analyze pySCENIC cli ...· 2 月前 · |
![]() |
强悍的钥匙 · flutter打印 List元素内容 - ...· 3 月前 · |
![]() |
爱运动的投影仪 · public class ...· 4 月前 · |
![]() |
逼格高的凉面 · 使用OpenSSL从PKCS#12文件导出证 ...· 4 月前 · |
创建您的第一个场景并了解场景、摄影机、几何体、材质、纹理等基础知识。添加 debug 面板以调整环境,并对内容进行动画处理。
1.1 什么是 Three.js
Three.js 是一个开源的应用级 3D JavaScript 库,可以让开发者在网页上创建 3D 体验。
什么是应用级?Three.js 屏蔽了 WebGL 的底层调用细节,大多数时候,你编写的代码更专注于“场景”中的“三维物体”的位置,加载“模型文件”,高级点的会写漂亮的“材质”。这些带引号的概念,在 WebGL 底层接口都是没有的,是实时渲染技术的一些概念在 JavaScript 中的实现。在 WebGL 中,我们需要装备顶点着色器和片元着色器。而在 Three.js 中需要准备的是相机、场景、物体和光源。
Three.js 应用示例:
● https://www.oculus.com/medal-of-honor/
1.2 什么是 WebGL
WebGL是一种 JavaScript API ,用于在不使用插件的情况下在任何兼容的网页浏览器中呈现 交互式2D和3D图形 。WebGL完全集成到浏览器的所有网页标准中,可将影像处理和效果的 GPU加速 使用方式当做网页 <canvas> 的一部分。WebGL元素可以加入其他HTML元素之中并与网页或网页背景的其他部分混合。WebGL程序由JavaScript编写的句柄和OpenGL Shading Language(GLSL)编写的着色器(shader)代码组成,该语言类似于C或C++,并在电脑的图形处理器(GPU)上执行。
● JavaScript API
● 以非常快的速度渲染三角形(模型是由一个个三角形组成的)
● 结果可以绘制在 <canvas> 上
● 与大多数现代浏览器兼容
● 使用图形处理器(GPU)
绘制像素和放置坐标点的指令(instruction)就是 shader
创建 index.html
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>03 - Basic Scene</title>
</head>
<canvas class="webgl"></canvas>
<script src="./three.min.js"></script>
<script src="./script.js"></script>
</body>
</html>
引入 three.js
我们需要在 threejs.org 中点击 download 按钮下载 three.js 源码 zip 文件,将 build/three.min.js 保存到当前文件夹下。在 index.html 中,首先加载 three.min.js 再加载 script.js。
在 script.js 中,我们现在可以访问一个全局变量 THREE,
console.log(THREE)
THREE 变量包含了大多数 Three.js 项目中所需的属性和类,比如创建场景和创建球体几何,
const scene = new THREE.Scene()
const sphereGeometry = new THREE.SphereGemetry(1.5, 32, 32)
First Scene
为了显示一个基本的场景,我们需要:
● scene 场景
● objects 物体
● camera 相机
● renderer 渲染器
Scene
场景像一个 容器(container) ,你可以将物体,模型,粒子,光源,相机等加入其中。
为了创建场景,我们需要使用 Scene 类
// Scene
const scene = new THREE.Scene()
Objects
物体可以有很多种,比如原始的几何体,导入的模型 ,粒子,光等。
我们将从简单的红色立方体开始。
要创建该红色立方体,我们需要创建一种名为 Mesh 的对象。Mesh 是几何(形状)和材料(外观如何)的组合。
有许多几何形状和许多材料,但是我们现在将保持简单,创建一个 BoxGeometry 和一个 MeshBasicMaterial。
为了创建几何体,我们将使用 BoxGeometry 类,前三个参数表示其大小
// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)
现在可以将 mesh 加入到场景中,
scene.add(mesh)
Camera
相机是不可见的。这更像是理论上的视角。当我们渲染一个场景时,这将是从那个相机的角度来看。
您可以像电影一样拥有多个摄像头,并且可以随意地在这些摄像机之间切换。但是通常,我们只使用一台相机。
相机有不同的类型,我们将在以后的课程中谈论这些。就目前而言,我们只需要一个透视相机(使近距离对象看起来比远的对象更为突出)。
要创建相机,我们使用最常用的 PerspectiveCamera,并且我们需要提供两个基本参数。
视野 (The field of view)
视野是您的视觉角度的大小。如果您使用一个非常大的角度,则可以立即朝各个方向看到,但变形很大,因为结果将在一个小矩形上绘制。如果您使用一个小角度,则看起来会放大。视野(FOV)以度为单位表示,与垂直视觉角度相对应。对于此练习,我们将使用75度角。
长宽比(The aspect ratio)
在大多数情况下,纵横比是画布的宽度除以其高度。我们暂时尚未指定任何宽度或高度,但是我们需要以后进行。同时,我们将创建一个具有临时值的对象,我们可以重复使用。
不要忘记将相机添加到场景中,不将相机添加到场景中可能会导致一些 bugs
// Sizes
const sizes = {
width: 800,
height: 600
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)
Renderer 渲染器
我们将简单地要求渲染器从相机的角度渲染我们的场景,结果将被绘制到 canvas 中。您可以自己创建 canvas,或者让渲染器生成它,然后将其添加到您的页面中。对于此练习,我们将将 canvas 添加到HTML中,并将其发送给渲染器。
<canvas class="webgl"></canvas>
要创建渲染器,我们使用 WebGLRenderer 类,和一个包含所有选项的 {} 对象作为参数,
// Canvas
const canvas = document.querySelector('canvas.webgl')
// ...
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas
renderer.setSize(sizes.width, sizes.height)
First render
调用 renderer 的 render 方法渲染场景,
renderer.render(scene, camera)
依然没有看见场景?这是因为,我们尚未指定对象的位置,也没有指定相机的位置。
两者都处于默认位置,即场景的中心,我们无法从物体内部看到物体。
我们需要移动相机位置。
为此,我们可以访问每个对象上的多个属性,例如位置 position,旋转 rotation 和比例 scale。现在,使用 position 属性向后移动相机。
位置属性是具有三个相关属性的对象:x,y和z。默认情况下,Three.js 将 forward/backward 轴视为 z 轴
为了向后移动相机,我们需要为该属性提供正值。创建相机变量后,您可以在任何地方执行此操作,但是必须在进行渲染之前就发生:
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
camera.position.z = 3
scene.add(camera)
渲染结果:
恭喜,您应该看到您的第一个渲染。
它看起来像一个正方形,这是因为相机与立方体对齐了,您只能看到它的前侧。
不必担心渲染的尺寸,我们将在之后学习如何使 canvas 拟合 viewport。
在接下来的课程中,您将更多地了解有关位置 position,旋转 rotation 和缩放 scale 属性,如何更改它们,并为场景添加动画。
我们在上一课中加载 Three.js 的方式是最简单的。不幸的是,它有一些局限性。它不包含 Three.js 中的一些类,因为这会使得文件太重了。但是,我们需要一种加载这些隐藏类的方法。我们还需要运行服务器来加载和操纵图像。为此,由于安全原因,我们需要运行本地服务器。
处理这些问题有很多方法,但最简单的是使用打包器(module bundler)。
什么是打包器?
Bundler 是一种工具,您可以在其中发送 JavaScript,CSS,HTML,Images,Typescript, Stylus和其他语言等 assets。Bundler 将处理这些 assets,应用潜在的修改并输出由HTML,CSS,Images,JavaScript 等 web-friendly 文件组成的 bundle
您会看到,就像一条管道一样,您在该管道中发送了 non-web-friendly 的 assets,并且在管子的尽头,您可以获得 web-friendly 的 assets。
Bundler 可以做更多的事情。您可以使用 Bundler 创建本地服务器,管理依赖项,提高兼容性,支持模块,优化图像,在服务器上部署,缩小代码等。
Webpack
Webpack 是目前最受欢迎的 Bundler。它可以满足您的大部分需求,并提供广泛的文档和建设性的社区。
不幸的是,WebPack 配置非常具有挑战性。不用担心,我将为您提供一个简单的模板,我将解释它的作用以及如何使用它。
虽然您可能将其视为一个限制,但 WebPack 是如此受欢迎和有用,以至于对于Web开发人员来说,它是必不可少的。而且,如果您不知道 WebPack,那是学习新工具的理想场合。
如何使用模板
下载模板文件,文件包含:
安装依赖
npm install
启动本地服务器
npm run dev
你将看到空白页,
Get our scene back
在 src/index.html 文件中添加 <canvas>
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>04 - Webpack</title>
</head>
<canvas class="webgl"></canvas>
</body>
</html>
注意我们没有添加 <script> 标签,Webpack 会处理这个部分。
现在,您需要将 JavaScript 代码添加到/src/script.js文件中。唯一的区别是前两行。
● import './style.css'将导入CSS并将其应用于页面(CSS文件当前为空)。
● import * as THREE from “three” 将导入 THREE 变量所有的默认类, three 模块位于 / node_modules/ 文件夹中,但您无需触摸它。
import './style.css'
import * as THREE from 'three'
// Scene
const scene = new THREE.Scene()
// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
// Sizes
const sizes = {
width: 800,
height: 600
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
camera.position.z = 3
scene.add(camera)
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl')
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)
如果服务器以及在运行中,打开页面(无需刷新)
Webpack 在最开始有点困难,但是一旦您习惯了,它就非常强大,它会开发环境提供可自动刷新的开发服务器。
在 Basic scene 中,我们将 camera 向后移动了一点点,使用 camera.position.z = 3,本节课将学习如何转换物体。
有4个属性可以在我们的场景中转换对象
● position(移动对象)
● scale(调整对象的大小)
● rotation(旋转物体)
● quaternion(还是旋转对象;稍后再详细介绍)
所有继承 Object3D 的类都具有这些属性,比如 PerspectiveCamera 和 Mesh
这些属性将被编译为矩阵。矩阵在 Three.js,WebGL和GPU内部使用。幸运的是,您不必自己处理矩阵,只需修改这些属性即可。
参考阅读: https://hrcak.srce.hr/file/297879
移动对象
position 具有3个基本属性,即x,y和z。这些是将某物放在3D空间中的3个必要轴。
每个轴的方向是任意的,并且可以根据环境而变化。在Three.js中,我们通常认为Y轴向上移动,Z轴向后移动,并且X轴向右移动。而在 Unity 和 Blender 中,这可能会有所不同。
至于1 unit的含义,这取决于您。1可以是1厘米,1米甚至1公里。我建议您将 unit 适应您要构建的内容。如果您要创建房屋,则可能应该将1个单位视为1米。
您可以使用 mesh 的 position 属性来玩耍,并尝试猜测立方体将在哪里(尝试将其保存在屏幕上)。
在调用 render 方法之前对 mesh 位置的修改都是有效的。
mesh.position.x = 0.7
mesh.position.y = - 0.6
mesh.position.z = 1
position 属性不是普通的对象,它是 Vector3 类的实例(3D 向量)。这个类不仅具有 x,y 和 z 属性,也具有许多有用的方法。
Vector3 中的部分方法:
mesh.position.length() // 返回(0,0,0)到(x,y,z)的欧氏距离mesh.position.distanceTo(camera.position) // 返回当前Vector3到另一个Vector3的距离mesh.position.normalize() // 归一化, 保持方向不变mesh.position.set(0.7, -0.6, 1) // 同时设置x,y,z
Axes helper
在空间中放置对象是困难的,一个好的解决方案是使用 AxesHelper 类来为了每个轴展示一条彩色的线。
例子:
// Axes helper
const axesHelper = new THREE.AxesHelper()scene.add(axesHelper)
由于我们的 camera 处于 z 轴,所以我们看不到蓝色的z轴。
调整对象大小 scale
使用 scale 属性来调整对象大小, scale 也是 Vector3 的实例,这意味着它有 x,y,z属性,以及前面提到的方法。
mesh.scale.x = 2
同时设置 xyz 的 scale
mesh.scale.set(2, 0.5, 0.5)
旋转对象 rotation
我们可以使用 rotation 属性或 quaternion 属性,更新一个属性会自动更新另一个属性。
rotation 也有 x,y,z 属性,但是它是一个 Euler,Euler 是用于做旋转变换的类。
当你在修改 x,y,z 属性时,你可以想象在轴的方向上向物体中心插入一根棍子,然后沿着这根棍子进行旋转。
mesh.rotation.y = 0.5 // 沿y轴旋转0.5弧度
角度与弧度的关系:
mesh.rotation.y = 3.1415 // 旋转3.14弧度(旋转180度)
mesh.rotation.y = Math.PI // 更好的写法
需要小心的是,当你旋转一个轴时,你可能也在旋转其他轴,默认情况下,旋转是按照x,y,z顺序的。
在有些情况下,某些轴的旋转可能并不会生效,这被称为万向锁问题(gimbal lock problem)。
参考阅读: https://en.wikipedia.org/wiki/Gimbal_lock
我们可以更改旋转顺序,使用
// 先更改旋转顺序
mesh.rotation.reorder("YXZ")
// 再进行旋转
mesh.rotation.x = Math.PI/4
mesh.rotation.x = Math.PI/4
Euler 更容易理解,但是轴的顺序是一个问题,这就是为什么大多数游戏引擎和3D软件使用 Quaternion 的原因。
Quaternion 是一种旋转的表示,但是以一种更加数学的方式。
Look at this
Object3D 实例有 lookAt() 方法,它会旋转物体使得 -z 面对 target,target 需要是一个 Vector3
camera.lookAt(new THREE.Vector3(3, 0, 0))
让相机看向物体位置,
camera.lookAt(mesh.position)
分组 Group
你可以将物体分组,然后对整个组使用 position,rotation,scale。为了实现这个需求,我们需要使用 Group 类。
/**
* Objects
const group = new THREE.Group()
scene.add(group)
const cube1 = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshBasicMaterial({color: 0xff0000}))
group.add(cube1)
const cube2 = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshBasicMaterial({color: 0x00ff00}))
cube2.position.x = -2
group.add(cube2)
const cube3 = new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshBasicMaterial({color: 0x0000ff}))
cube3.position.x = 2
group.add(cube3)
group.position.y = 1
group.scale.y = 2
我们创建了一个在代码结束后渲染一次的场景。这已经是不错的进步,但是大多数时候,您都需要为您的创作做动画。
动画,使用 Three.js 时,就像停止运动一样工作(stop motion)。您移动对象,然后进行渲染。然后,您再移动对象一些,然后再进行另一个渲染。您在渲染间隔之间移动对象距离越多,它们似乎移动的速度就越快。
屏幕以特定的频率运行,我们称之为帧率(frame rate)。帧率主要取决于屏幕,但是计算机本身有局限性。大多数屏幕以每秒60帧的速度运行。如果您进行数学计算,那意味着每16ms大约一帧。有些屏幕的帧率可以更高,当计算机难以处理内容时,它的运行速度会变慢。
我们想执行一个函数,这个函数将移动对象然后每个帧上执行渲染,而不管帧率如何。
原生 JavaScript 的方式是使用 window.requestAnimationFrame 方法。
Setup
使用 requestAnimationFrame
有些人可能认为 requestAnimationFrame 是用来做动画的,这其实是错误的观点。RAF 主要目的是在下一帧调用一个函数而已。
但是,如果此这个函数还调用 requestAnimationFrame 在下一个帧上执行相同的函数,则将永远在每个帧上执行您的函数,这正是我们想要的。
创建一个名为tick的函数,然后调用此函数一次。在此函数中,使用window.requestanimationframe 在下一个帧上调用此相同的函数:
/**
* Animate
const tick = () =>
console.log('tick')
window.requestAnimationFrame(tick)
tick()
对,就是这样,你现在有了一个无限循环。
正如您在控制台上看到的那样,每个帧都在调用“ tick”。如果您在具有较高帧速率的屏幕上测试此代码,则“ tick”将以较高的频率出现。
现在,你可以将 renderer.render() 调用移到这个函数中去,然后旋转这个立方体
/**
* Animate
const tick = () =>
// Update objects
mesh.rotation.y += 0.01
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
tick()
恭喜,你现在有了第一个 Three.js 动画。
问题是,如果您在具有高帧率的计算机上测试此代码,则立方体会更快地旋转,如果您以较低的帧率进行测试,则立方体将旋转较慢。这是因为,在每秒60帧的计算机,我们将每秒执行该函数60次,而在 144Hz 的计算机上,每秒将执行函数144次。
接下来将介绍三个解决方案,
自适应帧率
为了使动画适应帧速率,我们需要知道自上次tick以来已经有多少时间了。
首先,我们需要一种测量时间的方法。在原生 JavaScript 中,您可以使用 Date.now() 获取当前时间戳:
const time = Date.now()
时间戳对应于自1970年1月1日(UNIX的开始)以来已经过去了多少时间。在JavaScript中,其单位为毫秒。
您现在需要的是将当前的时间戳减去上一个帧的时间戳,以得到 deltaTime,并在对物体进行动画时使用此值:
/**
* Animate
let time = Date.now()
const tick = () =>
// Time
const currentTime = Date.now()
const deltaTime = currentTime - time
time = currentTime
// Update objects
mesh.rotation.y += 0.01 * deltaTime
// ...
tick()
该立方体应更快地旋转,因为如果您的屏幕以60fps的速度运行,则 deltaTime 约为16,因此请随意通过乘以该值来减少它。
现在,我们旋转是基于自上次帧以来花费了多少时间,无论帧率如何,每个屏幕和每个计算机都将显示相同速率的旋转效果。
使用 Clock
Three.js 有一个内置的解决方案叫做时钟 Clock
我们可以实例化 Clock 然后使用它的 getElapsedTime() 方法
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime() // 返回已经过去的时间, 以秒为单位
// Upadate objects
mesh.rotation.y = elapsedTime * Math.PI * 2 // 一秒自传一圈
mesh.position.y = Math.sin(elapsedTime) // 立方体以sin函数上下移动
mesh.position.x = Math.cos(elapsedTime) // 立方体以cos函数左右移动, x,y组合起来呈圆形环绕
// 移动相机
camera.position.y = Math.sin(elapsedTime)
camera.position.x = Math.cos(elapsedTime)
camera.lookAt(mesh.position)
// ...
tick()
使用 GASP 动画库
如果你想要有更多的控制,创建动画渐变,创建时间线等等,你可以使用像 GSAP (Green Sock) 的库
npm i [email protected]
import gsap from "gsap"
//...
gsap.to(mesh.position, { duration: 1, delay: 1, x: 2}) // gsap是自带tick的
const tick = () =>
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
tick()
我们已经使用过了透视相机 PerspectiveCamera,但是还有许多其他种类的相机,正如你在文档中见到的。
Camera
Camera 类是我们所说的抽象类(abstract class)。您不应该直接使用它,但是您可以从中继承它来访问共有的属性和方法。下面将介绍一些 继承自 Camera 的相机类.
● ArrayCamera
ArrayCamera 用于使用多个相机渲染多次你的场景。每个相机将渲染画布的特定区域。您可以想象这种看起来像 old-school 游戏机多人游戏,玩家必须共享一个分屏。
● StereoCamera
StereoCamera 用于通过两个模仿眼睛的摄像机来渲染场景,以创建我们所谓的视差效应(parallax effect),这会吸引您的大脑以为有深度。您必须拥有足够的设备,例如VR耳机或红蓝光眼镜,才能查看结果。
● CubeCamera
CubeCamera 用于使渲染效果面向每个方向(向前,向后,向左,向右,向上和向下),以创建周围环境的渲染。您可以使用它来创建一个环境图进行反射或阴影。我们稍后再讨论。
● OrthographicCamera
OrthographicCamera 用于创建场景的没有透视的正交渲染。如果您制作像帝国时代这样的RTS游戏,这将很有用。不管离相机多元,元素将保持相同大小。
● PerspectiveCamera
Perspective 用于模拟现实中具有透视的相机,我们将主要聚焦在正交相机和投影相机。
PerspectiveCamera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)
第一个参数 field of view 视野,单位为角度 degree,而不是经常使用的弧度 radius,它是垂直 vertical 的角度,通常设置在 45-75 之间。
当 FOV 逐渐增加至 180 度,会出现 distorsion 现象。
第二个参数为 aspect ratio ,对应于宽度除以高度。虽然您可能会认为这显然是 canvas 高度的 canvas 宽度,而 Three.js 应该自己计算出来,但如果您以非常特定的方式开始使用Three.js,则并非总是如此。但是就我们而言,您可以简单地使用 canvas 宽度和 canvas 高度。
我建议将这些值保存在对象中,因为我们需要多次需要它们:
const sizes = {
width: 800,
height: 600
不同的宽高比示意图:
第三个和第四个参数为 near 和 far,对应于相机可以看多近和多远。物体或物体的一部分在相机的 near 值之外或这 far 值之外将不会被渲染到屏幕上。您可以看到就像在那些旧的赛车游戏中看到的那样,您可以在远处看到树木出现。我们可以把 near 设置为 0.1,将 far 设置为 100。
避免将 near 和 far 设置为极端值,如 0.0001 和 999999 来防止 z-fighting 问题,z-fighting 是对于深度相差非常小的情况下,会显示交叉闪烁的前后两个画面。
https://twitter.com/Snapman_I_Am/status/800567120765616128
https://twitter.com/FreyaHolmer/status/799602767081848832
OrthographicCamera
正交相机和透视相机不同的地方在于,正交相机没有透视效果。无论距离多远,物体在正交相机中总会呈现相同的大小。
当我们的 camera 在做正方形渲染时,而我们 renderer 并不是正方形时,就会出现 stretch 的现象,这是因为 renderer 和 camera 的渲染宽高比不一致造成的。
const sizes = {
width: 800,
height: 300
//...
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100)
所以,为了解决这个问题,我们需要定义 aspectRatio
const aspectRatio = sizes.width / sizes.height
const camera = new THREE.OrthographicCamera(-1 * aspectRatio, 1 * aspectRatio, 1, -1, 0.1, 100)
自定义相机控制
我们想通过鼠标控制相机位置。
首先,我们需要鼠标在网页上的坐标,使用 addEventListener 监听 mousemove 事件然后取出 event.clientX 和 event.clientY
window.addEventListener("mousemove", (event) => {
console.log(event.clientX, event.clientY)
})
clientX 和 clientY 的单位为 pixel,由于在电脑和手机上像素会有较大差异,我们需要调整坐标值到 [-0.5, 0.5] 范围区间。
const cursor = {
x: 0,
window.addEventListener("mousemove", (event) => {
cursor.x = event.clientX / sizes.width - 0.5
cursor.y = event.clientY / sizes.height - 0.5
然后,就可以在 tick 函数中使用 cursor 坐标更新 camera 的位置了,
const tick = () => {
// ...
// update camera
camera.position.x = cursor.x
camera.position.y = cursor.y
// ...
现在,我们的相机就可以跟随我们的鼠标进行移动了,但是有一个小问题,当我们的鼠标在上下移动时,cube 移动方向和鼠标方向相同,而我们的鼠标在左右移动时,cube 的移动方向和鼠标方向相反,这是因为浏览器 y 轴向下为正,而在 three.js 中 y轴向上为正,所以,我们可以通过修改 cursor.y 的方向来解决这个问题。
window.addEventListener("mousemove", (event) => {
cursor.x = event.clientX / sizes.width - 0.5
cursor.y = - (event.clientY / sizes.height - 0.5)
})
更进一步,在移动相机后,让相机看向原点。
const tick = () =>
// ...
camera.position.x = cursor.x * 3
camera.position.y = cursor.y * 3
camera.lookAt(new Vector3()) // mesh.position is also ok.
// ...
}
如果我们想让相机沿着 x-z 平面环绕检视正方体
const tick = () =>
// ...
camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 2 // 由于x在[-0.5, 0.5], 所以移动x只会旋转一圈
camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 2
camera.position.y = cursor.y * 3 // 上下移动相机保持不变
camera.lookAt(new Vector3())
// ...
}
Three.js 内置相机控制
Three.js 内置了多个 controls 类来帮助你做很多事情,我们可以在文档中查看 controls 示例,以便于理解这些 controls。
● DeviceOrientationControls (removed)
移动设备陀螺仪控制,由于无法在所有设备上进行可靠的实现,因此删除了该 class。
● FlyControls (SpaceshipControls)
● FirstPersonControls (BirdControls)
第一视角控制和 FPS 游戏无关,它更像 BirdControls,和 FlyConrols 类似。
● PointerLockControls
● OrbitControls
● TrackballControls
● TransformControls
● DragControls
使用 DragControls,我们可以拖动场景中的方块
使用 OrbitControls
注释掉我们在 tick 函数中更新 camera 位置的代码,让 camera 的位置完全交给 OrbitControls 处理。
OrbitControls 类不能直接使用 THREE.OrbitControls 访问,这个类位于 /node_modules/three/examples/jsm/controls/OrbitControls.js
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
// ..
const controls = new OrbitControls(camera, canvas)
现在我们可以使用鼠标控制 camera 的运动,但是我们的移动并不光滑,有点生硬,我们可以使用 enableDamping 阻尼属性。
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true // damping will smooth camera move by adding acceleration and friction
const tick = () =>
// ...
controls.update() // 在每一帧更新controls
// ...
}
何时使用自定义 controls 何时使用内置 controls?
当内置 controls 不能满足你所需要的功能时,我们需要自己实现 controls。
我们的 canvas 目前的固定分辨率为 800x600。您不一定需要WebGL来适合整个屏幕,但是如果您想要沉浸式体验(immersive experience),那可能会更好。
首先,我们想让 canvas 占用所有可用的空间。然后,我们需要确保如果用户调整其窗口大小,仍匹配。最后,我们需要为用户提供一种尝试全屏中体验的方法。
Fit in the viewport 匹配视口
为了使 canvas 完美地拟合在视口中,而不是使用 sizes 变量中的固定数字,使用 window.innerwidth和 window.innerheight:innerheight:
// ...
// Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight
// ...
您可以看到 canvas 现在具有视口的宽度和高度。不幸的是,它有一个白色的边缘和滚动条。
问题在于,浏览器都具有默认样式,例如重要的标题,带下划线的链接,段落之间的空格和页面上的 padding。有很多解决方法,这可能取决于您网站的其余部分。如果您还有其他内容,请尽量不要在执行此操作时破坏任何内容。
我们将保持简单,并使用CSS修复 canvas 的位置。
我们的模板已经链接到 src/style.css 中的CSS文件。如果您不习惯 WebPack,看起来可能很奇怪,但是CSS文件是从 script.js 的第一行直接导入:
import './style.css'
您可以像习惯一样编写标准CSS,并且页面将自动重新加载。
首先要做的一件好事是使用通配符 *删除所有元素上的任何类型的边距或填充。
*
margin: 0;
padding: 0;
}
然后,我们将 canvas 固定在左上角,使用 .webgl 类选择器
.webgl
position: fixed;
top: 0;
left: 0;
}
针对不同浏览器和 MacOS(现代版IE),我们还需要添加一些样式,最终的样式如下。
*
margin: 0;
padding: 0;
html, body
overflow: hidden;
.webgl
position: fixed;
top: 0;
left: 0;
outline: none;
}
现在,白色边缘和滚动条都消失了,您可以开心的享受 WebGL。不幸的是,如果您调整了窗口大小,那么 canvas 就不会跟随。
我们需要处理调整大小。
Handle resize 处理调整大小
通过监听 resize 事件,我们可以知道窗口是否在被调整大小。
window.addEventListener("resize", () => {
console.log("window has been resized")
})
在 resize 事件中,更新 sizes 变量,更新 camera,更新 renderer
window.addEventListener("resize", () => {
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update renderer
renderer.setSize(sizes.width, sizes.height)
现在,当调整窗口大小时,渲染窗口也会正确变化!
Handle pixel ratio 处理像素密度
有时我们会在边缘上看到 blurry render 和 stair effect(锯齿),如果出现了这个现象,这是因为你在一块像素密度大于1的屏幕上测试。
很多年前,所有的屏幕的像素密度都是1,像 Apple 公司这样的生产商看到了机会,开始制造像素密度等于2的屏幕,并称为 retina 视网膜屏幕,现在许多生产商在制造像素密度大于3或者更高的屏幕。
像素密度为 2 意味着要渲染的像素多 4 倍,像素密度为 3 意味着要渲染的像素多了 9 倍,更高的像素密度通常出现在移动设备上,通常 pixel ratio 为 2 就足够了,更高的 pixel ratio 会加重 GPU 的负担。
为了获得当前的像素密度,我们可以使用 window.devicePixelRatio,在浏览器控制台查看当前像素密度
要更新 renderer 的像素密度,我们可以使用 renderer.setPixelRatio() 方法
renderer.setPixelRatio(window.devicePixelRatio)
为了避免像素密度过大造成性能问题,我们可以加上一个限制,
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
同时,将它加入到 resize 事件中,这是为了处理用户将窗口移动到另一块屏幕上的情况,
window.addEventListener("resize", () => { renderer.setPixelRatio(Math.min(indow.devicePixelRatio, 2))})
Handle fullscreen 处理全屏
双击窗口打开全屏模式,监听 dblclick 事件,
window.addEventListener("dblclick", () => {
console.log("double click")
})
为了知道我们是否已经处于全屏模式,我们可以使用 document.fullscreenElement,如果用户双击屏幕,并且还没有进入全屏,则进入全屏。
window.addEventListener("dblclick", () => {
if(!document.fullscreenElement)
canvas.requestFullscreen()
document.exitFullscreen()
})
不幸的是,这种写法不能在 Safari 中运行,我们需要使用兼容版本,
window.addEventListener("dblclick", () => {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement
if(!fullscreenElement) {
if(canvas.requestFullscreen) {
canvas.requestFullscreen()
else if(canvas.webkitRequestFullscreen) {
canvas.webkitRequestFullscreen()
if(document.exitFullscreen) {
document.exitFullscreen()
else if(document.webkitExitFullscreen) {
document.webkitExitFullscreen()
})
到目前为止,我们仅使用 BoxGeometry 类来创建我们的立方体。在本课程中,我们将探索其他各种几何形状,但首先,我们需要了解真正的几何形状。
什么是 Geometry?
在 Three.js 中,geometry 由顶点 vertices(3D空间中的点坐标)和面 faces(将这些顶点连接以创建表面)组成。
我们使用 geometry 来创建网格 meshes,但是您也可以使用 geometry 来形成颗粒 particles。每个顶点将于粒子相对应,但这是将来的课程。
我们可以存储除顶点位置之外的更多信息。一个很好的例子是 UV 坐标,normal 和颜色。
内置几何体
所有的几何体都继承自 Geometry 类,这个类拥有 translate(),rotateX(),normalize() 等方法。
● BoxGeometry To create a box.
● PlaneGeometry To create a rectangle plane.
● CircleGeometry To create a disc or a portion of a disc (like a pie chart).
● ConeGeometry To create a cone or a portion of a cone. You can open or close the base of the cone.
● CylinderGeometry To create a cylinder. You can open or close the ends of the cylinder and you can change the radius of each end.
● RingGeometry To create a flat ring or portion of a flat circle.
● TorusGeometry To create a ring that has a thickness (like a donut) or portion of a ring.
● TorusKnotGeometry To create some sort of knot geometry.
● DodecahedronGeometry To create a 12 faces sphere. You can add details for a rounder sphere.
● OctahedronGeometry To create a 8 faces sphere. You can add details for a rounder sphere.
● TetrahedronGeometry To create a 4 faces sphere (it won't be much of a sphere if you don't increase details). You can add details for a rounder sphere.
● IcosahedronGeometry To create a sphere composed of triangles that have roughly the same size.
● SphereGeometry To create the most popular type of sphere where faces looks like quads (quads are just a combination of two triangles).
● ShapeGeometry To create a shape based on a path.
● TubeGeometry To create a tube following a path.
● ExtrudeGeometry To create an extrusion based on a path. You can add and control the bevel.
● LatheGeometry To create a vase or portion of a vase (more like a revolution).
● TextGeometry To create a 3D text. You'll have to provide the font in typeface json format.
通过组合这些几何体,我们可以创建复杂的形状,如果还需要更复杂的模型,可以使用如 Blender 这样的 3D 软件。
BoxGeometry
我们已经制作了一个立方体,但我们没有谈论参数。大多数几何体都有参数,并且在使用几何体之前,你需要查看文档。
BoxGeometry 有6个参数:
● width:X轴上的大小
● height:Y轴上的大小
● depth:Z轴上的大小
● widthSegments:x轴中有多少个网格
● heightSegments:Y轴中有多少个网格
● depthSegments:Z轴中有多少个网格
Subdivisions 对应与多少个三角形组成平面。默认情况下是1,这意味着每个面只有2个三角形。如果将 subdivisions 设置为2,则每个平面将获得8个三角形:
const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
创建自己的 BufferGeometry
我们可以创建自己的 geometry,但是如果 geometry 十分复杂和具体,我们也可以使用 3D 软件。
在创建 geometry 之前,我们需要理解怎么存储缓存几何数据,我们将使用 Float32Array,
● Typed array
● Can only store floats
● Fixed length
● Easier to handle for the computer
// 确定长度然后填充数组
const positionsArray = new Float32Array(9)
positionsArray[0] = -1
positionsArray[1] = -1
positionsArray[2] = 0
positionsArray[3] = 1
positionsArray[4] = -1
positionsArray[5] = 0
positionsArray[6] = 1
positionsArray[7] = 1
positionsArray[8] = 0
创建一个三角形,
const geometry = new THREE.BufferGeometry();
// 直接传入数组
const vertices = new Float32Array( [
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
// we can convert Float32Array to a BufferAttribute
// 3 corresponds to how much values compose one vertex
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
const mesh = new THREE.Mesh( geometry, material );
我们也可以创建大量随机的三角形,
const geometry = new THREE.BufferGeometry()
const count = 50
const positionsArray = new Float32Array(count * 3 * 3) //一个vertex有三个坐标值, 一个三角形有三个vertex
for(let i = 0; i < count * 3 * 3; i++) {
positionsArray[i] = (Math.random() - 0.5) * 4
geometry.setAttribute('position', new THREE.BufferAttribute( positionsArray, 3 ) );
有时,我们的三角形会共享同一个顶点,当我们创建 BufferGeometry 时,我们指定了一系列顶点,然后 indices 用于创建面和复用顶点,这对性能优化有帮助,但是我们这里不会讲这个。
每个创意项目的一个重要方面是使调试(debug)变得容易并调整(tweak)您的代码。开发人员(您)和其他从事该项目的参与者(例如设计师甚至客户)必须能够更改尽可能多的参数。
您必须考虑到它们,以找到完美的颜色,速度,数量等,以获得最佳体验。您甚至可能会得到看起来不错的意外结果。
首先,我们需要一个调试UI。
虽然您可以使用HTML / CSS / JS创建自己的调试UI,但已经有多个库:
● dat.GUI
● Uil
● Guify
● Oui
所有这些都可以做我们想做的事,但是我们将使用最受欢迎的 dat.gui。Feel free to try the other ones.
Dat.GUI vulnerabilities
dat.gui很长一段时间没有更新,如果我们将它添加到项目中,可能会出现一些漏洞警告(vulnerabilities warning)。
幸运的是,有一个名为 lil-gui 的替代库,可以用作 “dat.gui 替代品”。这意味着我们可以像使用 dat.gui 一样使用它。
Example
https://bruno-simon.com/#debug
如何添加 Dat.GUI
要将 dat.gui 添加到我们的WebPack项目中,我们可以使用Node.js提供的依赖项管理器,称为 npm。
在您的终端(服务器未运行或使用同一文件夹上的另一个终端窗口)中运行
npm install dat-gui
dat.gui现在可以在 /node_modules /文件夹中可用,我们可以在 script.js 中导入它。不要忘记重新启动服务器:
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import gsap from 'gsap'
import * as dat from 'lil-gui'
// ...
你现在可以实例化 Dat.GUI
/** * Debug */
const gui = new dat.GUI()
这将导致屏幕右上角的一个空面板。
您可以将不同类型的元素添加到该面板中:
● Range —for numbers with minimum and maximum value
● Color —for colors with various formats
● Text —for simple texts
● Checkbox —for booleans (true or false)
● Select —for a choice from a list of values
● Button —to trigger functions
● Folder —to organize your panel if you have too many elements
让我们看看其中的一些。
添加元素
要向面板添加一个元素,您必须使用 gui.add()。
第一个参数是一个对象,第二个参数是要调整的该对象的属性(string 类型)。创建相关对象后,您需要设置它:
gui.add(mesh.position, 'y')
面板中应出现一个范围。尝试更改它,并观察立方体移动。
要指定最小值,最大值和精度,您可以在参数中设置它们:
gui.add(mesh.position, 'y', - 3, 3, 0.01)
或者,你可以在 add()方法后链式调用 min(),max(),step() 方法
gui.add(mesh.position, 'y').min(- 3).max(3).step(0.01)
如果您不喜欢在一行中链式调用太多方法,则只需添加换号符:
gui
.add(mesh.position, 'y')
.min(- 3)
.max(3)
.step(0.01)
要改变标签值(label),使用 name() 方法
gui
.add(mesh.position, 'y')
.min(- 3)
.max(3)
.step(0.01)
.name('elevation')
Dat.GUI 会根据属性值的类型选择可以调整的类型,
// mesh.visible = false gui.add(mesh, "visible")
我们可以在 material 的 wireframe 做相同的事情,
gui.add(material, "wireframe")
调整颜色
我们需要一个临时对象来保存可调整的属性,然后传入 material 中,由于我们的 material.color 保存的并不是一个数字 0xff0000,而是一个对象,所以我们不能直接使用 add 方法。
const parameters = {
color: 0xff0000
}
然后使用 addColor 方法,
gui
.addColor(parameters, "color")
.onChange(() => {
material.color.set(parameters.color)
})
最后,将实例化 material 时的参数替换为 paramter.color
const material = new THREE.MeshBasicMaterial({ color: parameters.color })
函数
为了触发一个函数,我们需要在对象中保存函数,我们可以再使用 parameters 对象,创建一个 spin 旋转函数,
const parameters = {
color: 0xff0000,
spin: () => {
gsap.to(mesh.rotation, { duration:1, y: mesh.rotation.y + Math.PI * 2})
}
然后,
gui.add(parameters, "spin")
点击 spin 触发旋转,
TIPS
按 H 隐藏面板,
如果你想让面板一开始就隐藏,使用 gui.hide() 隐藏面板。
你可以改变默认面板的宽度,使用
const gui = new dat.GUI({ width: 400})
什么是纹理?
纹理是覆盖几何体表面的图片。有很多种类的纹理图,不同纹理图拥有不同的效果。我们可以在材质(material)上使用纹理贴图(texture maps)。
颜色纹理
颜色纹理(color texture or albedo texture)是最简单的。它只会将纹理的像素应用在几何体上。它等同于直接在 material 上设置 color 属性,但是 color 属性只能设置一种颜色,而 colorMap 可以为每一个位置设置颜色。
透明度纹理
透明度纹理(alpha texture)是灰度(grayscale)图像,白色为可见区域,黑色为不可见区域,它通常可以对纹理进行剪裁,将多余的部分去掉,使用透明度纹理需要开启 material 的 transparent 属性。
高度纹理
高度纹理(height/displacement texture)是灰度图像,它将移动顶点以创造一些高度。但是,使用高度纹理,你要添加更多的细分网格(subdivision)才能看到效果。
法线纹理
法线纹理(normal texture / bump texture)将增加细节。它不会移动顶点,但是它会让光线认为面的朝向不同。
法线纹理对于添加良好性能的细节非常有用,因为您不需要添加细分几何体。
更多: https://docs.unity3d.com/cn/2021.1/Manual/StandardShaderMaterialParameterNormalMap.html
环境光遮蔽纹理
环境遮挡纹理(ambient occlusion texture)是灰度图像,它将在表面缝隙中添加假的阴影。虽然它在物理上不准确,但它肯定有助于创建对比度。
金属纹理
金属纹理(metalness texture)是灰度图像,将指定哪个部分是金属(白色)和非金属(黑色)。此信息将有助于创造反射,给 material 增加金属质感。
粗糙度纹理
粗糙度(rougness texture)经常和金属度纹理一起使用,它也是灰度图,它将指定哪个部分是粗糙的(白色),哪一部分是光滑的(黑色)。此信息将有助于消散光线。地毯非常崎岖不平,您看不到它的光反射,而水的表面非常光滑,您可以看到在其上反射的光。
使用粗糙度纹理 + 金属纹理的反射的效果
PBR 基于物理渲染
这些纹理(尤其是金属性和粗糙度)遵循我们所谓的PBR原理。PBR代表基于物理的渲染(Physically Based Rendering)。它重新分组了许多倾向于遵循现实生活方向以获得现实结果的技术。
尽管还有许多其他技术,但PBR已成为真实渲染(realistic renders)的标准,许多软件,引擎和库正在使用它。
目前,我们将仅专注于如何加载纹理,如何使用它们,我们可以应用什么转换以及如何优化它们。我们将在以后的课程中看到有关PBR的更多信息,但是如果您很好奇,您可以在这里了解更多信息:
● https://marmoset.co/posts/basic-theory-of-physically-based-rendering/
● https://marmoset.co/posts/physically-based-rendering-and-you-can-too/
如何加载纹理
获取图像的URL
要加载纹理,我们需要图像文件的URL。
因为我们使用的是 WebPack,所以有两种获取它的方法。
您可以将图像纹理放在 /src/ 文件夹中,并像导入JavaScript 依赖一样导入它
import imageSource from './image.png' // 从src目录下导入
console.log(imageSource)
或者,您可以将该图像放入 /static/ 文件夹中,并仅通过将图像的路径添加到URL中来访问它:
const imageSource = '/image.png'console.log(imageSource)
在接下来的课程中,我们将使用 static 文件夹存放静态资源。
加载图片
● 使用原生 JavaScript
const image = new Image() // 创建 Image实例
// 监听onload事件
image.onload = () =>
console.log('image loaded')
// 修改src属性加载图片
image.src = '/textures/door/color.jpg'
您应该在控制台中看到“image loaded”。如您所见,我们将 src 设置为“/textures/door/color.jpg”,而没有路径中的 /static。
我们不能直接使用该图像。我们需要首先从该图像创建 Texture 。
这是因为WebGL需要一种非常具体的格式,该格式可以通过GPU访问,还需要一些更改将应用于纹理,如MipMapping,但我们将稍后再说。
用 Texture 类创建纹理:
const image = new Image()
image.addEventListener('load', () =>
const texture = new THREE.Texture(image)
image.src = '/textures/door/color.jpg'
// console.log(texture) Error: texture is not defined
我们现在需要做的是在 material 中使用该 texture。不幸的是,texture 变量已在函数中声明,我们无法在此 function 之外访问它。这是一个称为作用域scope的 JavaScript 限制。
我们可以在函数内部创建 mesh,但是有一个更好的解决方案,可以在 function 之外创建纹理,一旦图片加载时,将纹理的 needsUpdate 属性设置为 true。
const image = new Image()
const texture = new THREE.Texture(image)
image.onload = () => {
texture.needsUpdate = true
image.src = '/textures/door/color.jpg'
要在立方体上看到纹理,将 material 的 color 替换为 map,并使用 texture 作为值,
const material = new THREE.MeshBasicMaterial({ map: texture })
● 使用 TextureLoader 加载纹理
原生加载纹理并不复杂,但是还有一种更简单直接的方式,就是使用 TextureLoader。
使用 TextureLoader 类实例化变量,然后使用 load() 方法来创建纹理,
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
在内部,Three.js 将加载图像并在纹理准备就绪后更新纹理。
您可以使用一个纹理加载器实例加载任意数量的纹理。
在 path 之后,还可以传入三个事件函数:
● load: 当图片加载成功时调用
● progress: 当正在加载时调用
● error:当加载出错时调用
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
'/textures/door/color.jpg',
console.log('loading finished')
console.log('loading progressing')
console.log('loading error')
)
如果纹理不起作用,则添加这些回调函数查看错误可能会有帮助。
● 使用 LoadingManager 加载纹理
创建 LoadingManager 类实例然后将其传入 TextureLoader 中
const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)
现在你可以监听许多加载事件,
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () =>
console.log('loading started')
loadingManager.onLoad = () =>
console.log('loading finished')
loadingManager.onProgress = () =>
console.log('loading progressing')
loadingManager.onError = () =>
console.log('loading error')
const textureLoader = new THREE.TextureLoader(loadingManager)
您现在可以开始加载所需的所有图像:
// ...
const colorTexture = textureLoader.load('/textures/door/color.jpg')
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const heightTexture = textureLoader.load('/textures/door/height.jpg')
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
UV unwrapping
将 BoxGeometry 替换为其他几何体
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Or
const geometry = new THREE.SphereGeometry(1, 32, 32)
// Or
const geometry = new THREE.ConeGeometry(1, 1, 32)
// Or
const geometry = new THREE.TorusGeometry(1, 0.35, 32, 100)
纹理被以不同方式拉升或挤压来覆盖几何体,这被称为 UV unwrapping (UV展开)。
有点像将糖果包装纸展平,每个3D顶点将在平面( 通常是正方形平面 )上拥有一个2D坐标。
我们在 geometry.attributes.uv 查看 UV 坐标,
console.log(geometry.attributes)
这些 UV 坐标是 Three.js 产生的。如果你要创建自己的几何体,你必须指定 UV 坐标。如果你要使用 3D 软件如 Blender 制作几何体,你也需要做 UV 展开。
变换纹理 Transform Texture
我们可以 repeat 纹理,使用 repeat 属性,这是一个 Vector2 实例,所以可以指定 x 和 y属性
const texture = textureLoader.load('/textures/door/color.jpg')
texture.repeat.x = 2
texture.repeat.y = 3
默认情况下,纹理不会重复,并且最后一个像素还被拉升了,我们可以设置 wrapS 和 wrapT
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
我们可以使用 MirrorRepeatWrapping 修改重复方向,下面的例子以这个为基础
texture.wrapS = THREE.MirroredRepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
offset
texture.offset.x = 0.5
rotation
texture.rotation = Math.PI / 4
旋转是基于弧度的,
我们可以指定旋转中心,默认为左下角 (0, 0)
texture.center.x = 0.5
texture.center.y = 0.5
Filtering and Mipmapping
https://www.gamedevelopment.blog/texture-filter/
当你看立方体的上面时,当这个面几乎隐藏起来,你将会看到一个模糊的纹理,这是因为 filtering 和 mipmapping 的原因。
Mipmaping 是一种加快渲染速度和减少图像锯齿的技术,通过不断包含一半大小的纹理,直到1x1纹理,所有这些纹理变种被发送至 GPU,GPU 会选择最合适的纹理。物体远离摄像机时,可以直接使用较小的纹理(纹理质量稍差,模糊感较强),当物体靠近时适当替换较大纹理(纹理质量好,模糊感较少),以便给玩家呈现更精致的画面。但是,使用 mipmapping 会增加 33% 的纹理内存大小占用。
我们可以修改纹理图的 minification 缩小过滤器,使用 minFilter 属性,
● THREE.NearestFilter
● THREE.NearestMipmapNearestFilter
● THREE.NearestMipmapLinearFilter
● THREE.LinearFilter
● THREE.LinearMipmapNearestFilter
● THREE.LinearMipmapLinearFilter
如果使用 NearestFilter,你会得到一个很锐利(sharp)的纹理
texture.minFilter = THREE.NearestFilter
让我们切换一个西洋跳棋盘 checkerboard-1024x1024 像素纹理,此时会出现摩尔纹,
const texture = textureLoader.load('/textures/checkerboard-1024x1024.png')
Magnification filter
当纹理的像素大于渲染像素时,此时,纹理对于其覆盖的表面太小,会产生 blurry 的现象,我们需要将纹理放大。
使用 checkboard-8x8.png 纹理,
const texture = textureLoader.load('/textures/checkerboard-8x8.png')
我们可以修改纹理的 magnification 过滤器,使用 magFilter 属性
texture.magFilter = THREE.NearestFilter
如果你想做 Minecraft 风格,NearestFilter 很有用,
const texture = textureLoader.load('/textures/minecraft.png')
此外,使用 NearestFilter 的性能比线性Filter好,使用 NearestFilter 我们可以关闭 mipmaps
纹理格式与优化
当我们在创建纹理时,需要记住三个核心元素:
● 文件大小 weight
● 分辨率 size(resolution)
● 数据 data
weight:
用户将下载纹理,选择正确的文件格式,
● jpg - 有损压缩,但通常更小
● png - 无损压缩,但通常更大
不管文件多大,纹理的每个像素都必须存储在 GPU里,但是GPU 的存储是有限的。此外,mipmapping 会增加需要存储的像素,所以,尝试尽量减少图片的大小。
size:
由于 mipmapping 每次产生一半的纹理大小,所以纹理的高度和宽度需要是2的幂次方,比如512x512,1024x1024,512x2048
data:
纹理支持透明,但是 jpg 格式不能存储透明度,如果需要透明度,我们就只能选择同时支持 alpha 和颜色的png 文件
有时我们可以将不同的数据整合到一个纹理中,通过分别使用 RGBA ,这对性能优化有好处。
Where to find texture?
● 3dtextures.me
● poliigon.com
● arroway-textures.ch
确保你有权使用纹理,如果不是个人使用的情况下。
你可以创建自己的纹理,使用 2D 软件比如 Photoshop,或者程序化纹理比如Substance Designer
材质(material)用于在几何体的每个可见像素上涂上颜色。
决定每个像素的颜色的算法是用名为着色器(shader)的程序编写的。写下色器是WebGL和Three.js中最具挑战性的部分之一,但请不要担心。Three.js具有许多带有预制着色器的内置材料,比如我们使用过的 MeshBasicMaterial 。
我们将在以后的课程中发现如何创建自己的着色器。现在,让我们使用Three.js材质。
准备场景
为了测试材料,我们应该准备一个可爱的(lovely)场景并加载一些纹理。
创建由三个由不同几何体(球体,平面和圆环)组成的网格,在网格中使用 MeshBasicMaterial,你可以在多个网格中使用相同 material。移动球体到左边,移动圆环到右边。使用 scene.add() 方法一次性将多个物体添加到场景中。
/**
* Objects
const material = new THREE.MeshBasicMaterial()
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
material
sphere.position.x = - 1.5
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(1, 1),
material
const torus = new THREE.Mesh(
new THREE.TorusGeometry(0.3, 0.2, 16, 32),
material
torus.position.x = 1.5
scene.add(sphere, plane, torus)
我们现在可以在 tick 函数中旋转我们的物体,
/**
* Animate
const clock = new THREE.Clock()
const tick = () =>
const elapsedTime = clock.getElapsedTime()
// Update objects
sphere.rotation.y = 0.1 * elapsedTime
plane.rotation.y = 0.1 * elapsedTime
torus.rotation.y = 0.1 * elapsedTime
sphere.rotation.x = 0.15 * elapsedTime
plane.rotation.x = 0.15 * elapsedTime
torus.rotation.x = 0.15 * elapsedTime
// ...
tick()
你应该可以看到三个几何体正在旋转!它们的默认颜色为白色
我们将用不同方式使用纹理来探索材料,让我们先使用 TextureLoader 加载一些纹理
/**
* Textures
const textureLoader = new THREE.TextureLoader()
const doorColorTexture = textureLoader.load('/textures/door/color.jpg')
const doorAlphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const doorAmbientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const doorHeightTexture = textureLoader.load('/textures/door/height.jpg')
const doorNormalTexture = textureLoader.load('/textures/door/normal.jpg')
const doorMetalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const doorRoughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
const matcapTexture = textureLoader.load('/textures/matcaps/1.png')
const gradientTexture = textureLoader.load('/textures/gradients/3.jpg')
你可以在材料中使用 map 属性使用你的纹理,
const material = new THREE.MeshBasicMaterial({ map: doorColorTexture })
截止到现在,我们使用的是 MeshBasicMaterial。如果在 Three.js 文档中进行搜索,这里还有许多其他的材质,让我们来尝试一下。
MeshBasicMaterial
MeshBasicMaterial 是最基础的材质,它不会受到光照的影响。
我们可以在实例化材料时设置许多属性,你也可以在实例上直接改变这些属性,通常这些属性在其他材质上也可用。
const material = new THREE.MeshBasicMaterial({
map: doorColorTexture
// Equals
const material = new THREE.MeshBasicMaterial()
material.map = doorColorTexture
map 属性
material.map = doorColorTexture
color 属性
color 属性在几何体的表面应用一个相同的颜色,我们必须传入一个 Color 类给 material 的 color 属性才能修改。
material.color = new THREE.Color('#ff0000')
material.color = new THREE.Color('#f00')
material.color = new THREE.Color('red')
material.color = new THREE.Color('rgb(255, 0, 0)')
material.color = new THREE.Color(0xff0000)
将 color 和 map 结合起来给纹理染色(tint),
wireframe 属性
wireframe 属性将显示组成几何体的三角形,使用 1px 的细线。
material.wireframe = true
opacity 属性
我们需要先将 transparent 属性设置为 true 让 Three.js 直到这个材料支持透明度
material.transparent = truematerial.opacity = 0.5
alphamap 属性
使用 alphaMap 属性控制纹理的透明度,
material.transparent = true
material.alphaMap = doorAlphaTexture
side 属性
side 属性可以让你觉得哪一面是可见的,默认情况下,前面是可见的(THREE.FrontSide),但是你可以展示后面和两面(BackSide 和 DoubleSide)
material.side = THREE.DoubleSide
现在你可以看到平面的前面和背面了,但是,尽量避免使用 DoubleSide 因为你需要渲染多一倍的三角形。
MeshNormalMaterial
const material = new THREE.MeshNormalMaterial()
法线网格材质,可以展示漂亮的紫色,它将法线向量转换成RGB颜色展示。
法线(normal)指向平面的外侧,法线被用于光照,反射和折射(lighting,reflection,refraction)
MeshNormalMaterial 和 MeshBasicMaterial 共享相同的属性,比如 wireframe,transparent,opacity 和 side,但有一个 flatShading 属性,flatShading 会展平平面,这意味着在顶点间不会有插值。
material.flatShading = true
MeshNormalMaterial 常被用于调试法线,但是其好看的颜色也可以用在你的项目中。
MeshMatcapMaterial
MeshMatcapMaterial 会使用法线作为参考,选择一个合适的颜色,模拟灯光效果。
const material = new THREE.MeshMatcapMaterial()material.matcap = matcapTexture
你可以在 https://github.com/nidorx/matcaps 找到 matcaps 纹理,你可以在 3D 软件创建自己的 matcaps
MeshDepthMaterial
const material = new THREE.MeshDepthMaterial()
如果几何体靠近摄像机的 near 则会变白色,如果靠近摄像机的 far 则会变成黑色。
添加一些灯光
创建环境光,
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5) // 0xffffff white light, 0.5 intensity
scene.add(ambientLight)
const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.set(2,3,4)
scene.add(pointLight)
MeshLambertMaterial
使用 MeshDepthMaterial 不受光线影响,MeshLambertMaterial 而会响应光线,但并没有光的高光。
MeshPhongMaterial
const material = new THREE.MeshPhongMaterial()
使用 shininess 控制反光部分的强度
material.shininess = 1000
使用 specular 控制反光的颜色
material.specular = new THREE.Color(0x1188ff)
MeshToonMaterial
MeshToonMaterial 和 MeshLambertMaterial 相似,但是拥有卡通风格。
为了给颜色添加更多的 steps 分层,你可以使用 gradientMap 属性和使用 gradientTexture
material.gradientMap = gradientTexture
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false // 因为我们使用了NearestFilter, 我们可以关闭Mipmaps以节省性能
MeshStandardMaterial
MeshStandardMaterial 使用基于物理渲染(PBR),和 MeshLambertMaterial 以及 MeshPhongMaterial 类似,它支持光照,但是拥有更真实的算法以及更好的参数,如 roughness 属性和 metalness 属性
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.65
material.metalness = 0.65
我们可以使用 debug 工具 dat.gui 来调试参数,
npm i dat.gui
import * as dat from "dat.gui"
const gui = new dat.GUI()
// ..
gui.add(material, "metalness").min(0).max(1).step(0.0001)
gui.add(material, "roughness").min(0).max(1).step(0.0001)
使用 map 属性添加纹理,
material.map = doorColorTexture
aoMap属性(ambient occlusion 环境光遮罩)可以给纹理的黑色部分添加阴影,不过我们需要设置第二套 UV 值才能生效。
// mesh.geometry 访问几何体, 为几何体设置uv2属性, uv2属性和几何体uv保持一致。
sphere.geometry.setAttribute("uv2", new THREE.BufferAttribute(sphere.geometry.attributes.uv.array, 2))
plane.geometry.setAttribute("uv2", new THREE.BufferAttribute(plane.geometry.attributes.uv.array, 2))
torus.geometry.setAttribute("uv2", new THREE.BufferAttribute(torus.geometry.attributes.uv.array, 2))
material.aoMap = doorAmbientOcclusionTexture
我们可以使用 aoMapIntensity 改变 aoMap的强度
material.aoMapIntensity = 10
将其加入 gui debug,
gui.add(material, "aoMapIntensity").min(0).max(10).step(0.0001)
displacementMap 位移贴图
material.displacementMap = doorHeightTexture
它看起来糟糕极了,这是因为它们的顶点较少以及位移太强了。
material.displacementScale = 0.5new THREE.SphereGeometry(0.5, 64, 64)new THREE.PlaneGeometry(1, 1, 100, 100)new THREE.TorusGeometry(0.3, 0.2, 64, 128)
gui.add(material, "displacementScale").min(0).max(1).step(0.0001)
metalnessMap and roughnessMap 金属度提额度和粗糙度贴图
// material.roughness = 0.65// material.metalness = 0.65material.metalnessMap = doorMetalnessTexturematerial.roughnessMap = doorRoughnessTexture
normalMap 法线贴图
material.normalMap = doorNormalTexture
法线贴图会改变光线反射的方向,增加细节。
without normalMap
alphaMap 透明度贴图
material.alphaMap = doorAlphaTexture
material.transparent = true
MeshPhysicalMaterial
和 MeshStandardMaterial 一样,但是支持 clear coat effect 看起来更真实,下图来自官方示例。
其他材料:
● PointsMaterial - 创建粒子效果
● ShaderMaterial 和 RawShaderMaterial 可以用于创建自己的材料
EnvironmentMap 环境贴图
Three.js 仅支持 cube environment map,我们需要使用 CubeTextureLoader而不是TextureLoader 加载它。
const cubeTextureLoader = new THREE.CubeTextureLoader()
// 立方体的六个面
const environmentMapTexture = cubeTextureLoader.load([
"/textures/environmentMaps/0/px.jpg",
"/textures/environmentMaps/0/nx.jpg",
"/textures/environmentMaps/0/py.jpg",
"/textures/environmentMaps/0/ny.jpg",
"/textures/environmentMaps/0/pz.jpg",
"/textures/environmentMaps/0/nz.jpg",
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.7
material.metalness = 0.2
material.envMap = environmentMapTexture
scene.background = environmentMapTexture
我们可以在 HDRIHaven (High Dynamic Range Imaging)网站中找到更多 hdr 图片,然后将其转换成 cube map https://matheowis.github.io/HDRI-to-CubeMap/
我们已经知道足够的基础知识来创建一些内容。对于我们的第一个项目,我们将重新创建Ilithya 的 https://www.ilithya.rocks/ ,在场景的中间有一个大3D文本,周围有物体。
这个 portfolio 是您在早期学习 Three.js 时一个很好的例子。它简单,高效,看起来很棒。
Three.js 已经支持 3D 文本几何(TextGeometry)。问题在于您必须指定字体,并且该字体必须采用 JSON 格式,称为 typeface 。
如何得到一个 typeface 字体?
使用 https://gero3.github.io/facetype.js/ 将你的字体转换成 typeface,然后放入 /static/ 文件夹中导入
你也可以在 /node_modules/three/examples/fonts/ 文件夹下找到 Three.js 提供的字体,直接导入它们。
import typefaceFont from 'three/examples/fonts/helvetiker_regular.typeface.json'
我们也可以将自带的字体复制到 /static/fonts 文件夹中,然后导入它们。
import typefaceFont from '/fonts/helvetiker_regular.typeface.json'
加载字体
使用 FontLoader 加载字体,
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
/**
* Fonts
const fontLoader = new FontLoader()
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
console.log('loaded')
)
当在控制台中看到 loaded 时,此时字体就加载成功了,我们可以在成功的回调函数中使用 font
创建几何体
Bevel Modifier 倒角修改器 — Blender Manual
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"
const fontLoader = new FontLoader()
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
const textGeometry = new TextGeometry(
"Hello Three.js",
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
const textMaterial = new THREE.MeshBasicMaterial()
const text = new THREE.Mesh(textGeometry, textMaterial)
scene.add(text)
)
const textMaterial = new THREE.MeshBasicMaterial({wireframe: true})
创建文本几何体对于计算机来说是昂贵的,我们可以通过减少 curveSegments 和 bevalSegments 降低三角形的数量
居中文本
// Axes helperconst
axesHelper = new THREE.AxesHelper()scene.add(axesHelper)
1. 使用 bounding box
默认情况下,Three.js 使用 sphere bounding,使用 box bounding 需要调用 computeBoundingBox() 方法,
textGeometry.computeBoundingBox()console.log(textGeometry.boundingBox)
有了 bounding box 的位置信息,使用 translate 平移几何体,
textGeometry.translate(
- textGeometry.boundingBox.max.x * 0.5,
- textGeometry.boundingBox.max.y * 0.5,
- textGeometry.boundingBox.max.z * 0.5,
)
由于 bevelThickness 和 bevelSize 的原因,文本并不是完全居中的,
textGeometry.translate(
- (textGeometry.boundingBox.max.x - 0.02) * 0.5,
- (textGeometry.boundingBox.max.y - 0.02) * 0.5,
- (textGeometry.boundingBox.max.z - 0.03) * 0.5,
)
使用 center()
一个更简单的方式是使用 center()
textGeometry.center()
添加 matcap 材质
https://github.com/nidorx/matcaps
const matcapTexutre = textureLoader.load("/textures/matcaps/1.png")
const textMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexutre})
添加更多物体
for(let i = 0; i < 100; i++) {
const donutGeometry = new THREE.TorusBufferGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexutre })
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
donut.position.x = (Math.random() - 0.5) * 10
donut.position.y = (Math.random() - 0.5) * 10
donut.position.z = (Math.random() - 0.5) * 10
donut.rotation.x = Math.random() * Math.PI
donut.rotation.y = Math.random() * Math.PI
const scale = Math.random()
donut.scale.x = scale
donut.scale.y= scale
donut.scale.z = scale
scene.add(donut)
}
优化
console.time("donuts")
for(let i = 0; i < 100; i++) {
// ...
console.timeEnd("donuts")
console.time("donuts")
const donutGeometry = new THREE.TorusBufferGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexutre })
for(let i = 0; i < 100; i++) {
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
// ...
console.timeEnd("donuts")
https://threejs-journey.com/lessons/go-live
传统的部署(hosting)方案使用 FTP client 手动上传文件,现代的 hosting 方案是使用如 Vercel,Netlify,Github Pages 的平台。
首先,我们在本地需要构建项目,
● Run npm run build in the terminal
● Upload all files from the /dist/
使用 Vercel 部署项目
Github,GitLab 和 Bitbucket 是Git仓库的托管解决方案,大多数开发者使用它们来保存代码。
Vercel 可以自动 fetch 你的仓库,并且在你向仓库推送新版本时自动更新。
你可以在 Vercel 网页版部署项目,也可以使用 vercel 的 npm module。
npm i vercel
{
"scripts": {
// ...
"deploy": "vercel --prod"
}
npm run deploy
一个更方便的方法是全局安装 vercel
npm i vercel -g
vercel --prod
https://3d-text-3js.vercel.app/
Vercel Price
Hobby plan 是免费的,并且可以部署无限多个项目,如果你的项目仅是用于展示 WebGL 体验,而不用于商业化,那么使用 Hobby plan 是一个好的选择。但是,Hobby plan 有带宽和构建时间的限制,所以如果你的项目很大或很流行,你需要切换到付费计划
头部相关传输函数(Head-Related Transfer Function,HRTF)是一种声音定位算法,它描述着人耳如何从空间中的声源点接受声音。在声音到达听众的耳膜之前,由于每个人都有着不同的头部、耳廓、耳道的形状和大小,这些结构会微弱地改变声源的大小和相位并影响其感知方式,从而提高某些频率并衰减某些频率…
![]() |
卖萌的水桶 · 如何将rtf文件合并_百度知道 2 月前 |
![]() |
强悍的钥匙 · flutter打印 List元素内容 - CSDN文库 3 月前 |