渲染的基本要素
场景
使用const scene = new THREE.Scene();
来创建一个场景,用于展示我们创建的物体或引入的建模
摄像机
在现实生活中,我们人眼就好比一个摄像机,离一个物体越近
,看到的物体就显得越大
,反之亦然;当我们把摄像机向左
移动,就相当于物体向右
发生了移动,反之亦然。
在threejs
中最基本的相机是透视摄像机PerspectiveCamera
,实现方式为
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
scene.add(camera)
第一个参数是Field of View
,代表视场角度,单位是度
。我们人的眼睛就是一台摄像机,我们在正前方画一条垂直于地面直线,那么必定存在两个点使得点到人眼形成的角度之和为75度,以这个75度角发散出去能看到的范围就是可观测到的范围
,如下图
第二个参数是宽高比,从上图可以看到,看出去的视角范围实际上是一个视锥,底部都是矩形的视觉区域,而每个矩形的宽高比必定为指定的宽高比。
网格(mesh)
mesh
就可以理解为要在场景中展示的东西了,要生成一个mesh
,需要一个几何体(Geometry
)(或自己的3D模型),加上该几何体的材质
(Material
)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
渲染器(renderer)
当我们有了要观察的物体后,就可以把他们渲染到场景上,用摄像机去观察了
renderer
通常使用WebGLRenderer
来创建,可以在创建WebGLRenderer
时指定画布dom
元素
const canvas = document.querySelector(".my-canvas")!;
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
也可以不传该参数,它会默认创建一个canvas
元素,我们只需要把他添加到页面指定位置即可,如
const renderer = new THREE.WebGLRenderer();
renderer.setSize(sizes.width, sizes.height);
document.body.appendChild(renderer.domElement);
最后,我们调用renderer.render
方法就可以把物体和摄像机放到场景中了
renderer.render(scene, camera);
但是,这时候在页面上看到的是一片黑,这是为什么呢?
在默认情况下,摄像机、物体都是放在正中心的位置,即(0, 0, 0)
位置,所以此时相当于我们处于立方体的内部,那么从立方体内部往外看,自然什么都看不到,这时候我们需要把摄像机向后移或者把物体往前移动(也就是在Z轴上移动)即可
camera.position.z = 5;
需要注意的是,
renderer.render
相当于拍了一张照片,任何对于物体的修改或位置的移动,都需要重新执行一次renderer.render(scene, camera);
来把场景和摄像机进行更新,即以后要做的动画,就是不断地一帧一帧地把物体实时地位置给渲染出来的过程
物体的变换
位置(position)
对于物体而言,其位置通常指的是它中心的坐标,一些实用的方法如下:
- meth.position.length():获取场景中心点(0, 0, 0)到物体中心的距离,这个其实就可以理解为在空间直角坐标系中两点之间距离的计算
- meth.position.distanceTo(someVector):物体中心到某个矢量点的距离,比如可以传
camera.position
来计算摄像机到物体的距离
position
的类型是Vector3
,可以在文档中查询Vector3
包含的所有方法,这里不作列举
Axes Helper
拉出一个坐标系参照,可以配置仅在开发环境渲染辅助坐标系
const axesHelper = new THREE.AxesHelper();
scene.add(axesHelper);
scale
即缩放,对于下面这个物体
x轴缩放设为0.5时:
x轴缩放设为-0.25时:
可以看到,缩放设为负数并不会有什么特殊的含义,并且缩放也不会改变物体的position
(中心点位置)
旋转rotation
旋转需要注意的是,旋转指的是以某根轴为中线
进行旋转,比如旋转x轴 mesh.rotation.set(last - Math.PI * 0.01, 0, 0);
,那就是以x
轴为中线进行旋转,我写了个基本的动画,其旋转的方式如下:
同理,对于Y轴而言就是:
注意这里,旋转时应该使用JS内置的
Math.PI
常量进行旋转,即圆周率的倍数。当然也不是不能指定一个整数
设置旋转角度、位置、缩放,可以使用.x = number、.y = number、.z = number
一个一个设置,也可以使用set
一次性设置3个值
组内变换
如果物体多了,一般会给既定的物体分一个组,然后对这整个组进行操作,比如旋转或缩放等
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
mesh2.position.x = 1;
const group = new THREE.Group();
group.add(mesh, mesh2);
scene.add(group);
动画
通常使用requestAnimationFrame
来实现,通过在回调函数中不断地重复调用requestAnimationFrame
来做出动画效果
但是在不同设备上,屏幕的刷新率不同,那么requestAnimationFrame
的执行速度也会不同,同理动画的速度也会不同
所以需要兼容不同设备刷新率情况下的动画,此时可以使用Date.now
来计算实现
let lastTime = Date.now();
function animate() {
const currentTime = Date.now();
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
requestAnimationFrame(animate);
group.rotation.y += 0.01 * deltaTime;
renderer.render(scene, camera);
}
animate();
此外,threeJS内部也提供了解决方案,使用内置的Clock
类来实现
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
group.rotation.y = clock.getElapsedTime() * Math.PI * 2;
renderer.render(scene, camera);
}
animate();
Clock会从0开始计时,官网上的实现说的是优先用performance.now()
实现,然后才是用Date.now
实现
那么,有了动画,除了基本的旋转之外,就可以实现比如下面的这些小动画了:
指定每秒旋转一圈:
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
group.rotation.y = clock.getElapsedTime() * Math.PI * 2;
renderer.render(scene, camera);
}
因为getElapsedTime
获取的是秒级单位,所以可以这样子写,2派
就代表一个圆的直径,那么多少秒就是多少个圆的直径,也就是转多少圈了
结合三角函数
三角函数的图像中,sin(x)
正弦函数随着x增大,其y值呈周期性地变化到1和-1
那么如果我们想让一个物体在Y轴移动出正弦函数曲线,就可以写成:
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
group.position.y = Math.sin(clock.getElapsedTime());
renderer.render(scene, camera);
}
animate();
如果想让它转圈,则可以对它的x也做类似地处理,不过呢要用余弦函数
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
group.position.y = Math.sin(clock.getElapsedTime());
group.position.x = Math.cos(clock.getElapsedTime());
renderer.render(scene, camera);
}
使用动画库
GSAP
是一个很牛B的动画库,官网是:https://gsap.com/
可以通过使用动画库API,更方便我们实现一些动画(比如直接调用预设)
import gsap from "gsap";
function animate() {
requestAnimationFrame(animate);
console.log(group.position);
renderer.render(scene, camera);
}
gsap.to(group.position, {
x: 3,
y: 2,
duration: 1,
});
gsap.to(group.position, {
x: 0,
y: -1,
delay: 2,
duration: 1,
});
animate();
可以看到控制台,它的原理就是在一秒内修改x和y分别到3和2,然后在2秒后,在1秒内把x和y分别改到0和-1
摄影机
透视摄像机(Perspective Camera)
前面已经讲了前两个参数,这里只讲后面两个参数near
和far
两个参数代表的是摄像机最近/最远可见的距离,这两个距离分别指的是近锥面
到摄像机(人)和远锥面
到摄像机(人)的距离
所以,只有far-near
之间的阴影部分区域的物体,才可以被看到(即渲染在画布中)
near和far的默认值为
0.1 - 2000
正交摄像机(Orthographic Camera)
正交摄像机没有透视功能,他与透视摄像机最大的区别是"不会有近大远小
"的效果,接收的参数是(左,右,上,下,近距,远距)
拿一个demo
举例,透视相机从正前方和侧面看一个物体是这样:
相机位置:
(0, 0, 3)
;物体位置:(0, 0, 0)
相机位置:
(2, 1, 3)
;物体位置:(0, 0, 0)
;
当换成正交相机时,从正前方看,看到的图像是:
代码设置如下:
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100);
camera.position.z = 3;
测试会发现,无论怎么移动摄像机Z轴
位置,看到的物体没有任何变化
移动X、Y轴位置,则是进行"左右" 或 "上下"的平移
代码表示的是,创建一个长宽均为2
的正方形视框去观察画面中的物体
对于上面的例子,创建的物体是中心为(0, 0, 0)
,长宽高均为1的正方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
刚刚把正交相机往正前方移动了3个单位长度,那么可以想到目前摄像机到物体正面
的距离为3 - 0.5 = 2.5
假如把正交摄像机的近距设为> 2.5
的值,那么就会看不到物体的正面了,比如把正交相机改为下面这样:
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 2.51, 100);
此时画面显示为:
因为我们此时恰好深入到了物体的内部0.01
的位置,从物体里往外看自然看不到物体本身