本文案例代码:链接:https://pan.baidu.com/s/1ZkttHSD74vEWEI0likcsew取码:y150
学习书籍:Three.js开发指南(第3版)
使用光照、阴影、材质和动画美化场景,并给场景添加辅助库
一、美化第一个三维场景
1.代码整理
在美化昨天的项目之前,首先对项目进行整理。在昨天的基础上更改js文件
01-03.js
function init() {
//首先定义场景(scene)、摄像机(camera)和渲染器(renderer)对象。
var scene = new THREE.Scene()
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
var renderer = new THREE.WebGLRenderer()
renderer.setClearColor(new THREE.Color(0x000000))
renderer.setSize(window.innerWidth, window.innerHeight)
//创建平面
var planeGeometry = new THREE.PlaneGeometry(60, 20)
var planeMaterial = new THREE.MeshBasicMaterial({color: 0xAAAAAA})
var plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.rotation.x = -0.5 * Math.PI
plane.position.set(15, 0, 0)
//立方体
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4)
var cubeMaterial = new THREE.MeshBasicMaterial({color: 0xFF0000})
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
cube.position.set(-4, 3, 0)
//球体
var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff})
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(20, 4, 2)
//将平面,立方体,球体添加到场景中
scene.add(plane)
scene.add(cube)
scene.add(sphere)
//设置摄像机的位置。
camera.position.set(-30, 40, 30)
camera.lookAt(scene.position)
//最后将渲染的结构添加到HTML框架的div元素中。
document.getElementById("webgl-output").appendChild(renderer.domElement)
//告诉渲染器使用指定的摄像机来渲染场景
renderer.render(scene, camera)
}
与昨天相比,整理后的代码去掉了轴THREE.AxesHelper(20)
,并去除了线框(wireframe
)属性,把物体渲染为实体物体。效果如下:
2.添加材质、光源和阴影效果
在Three.js中添加材质和光源和前一节基本一样。通过THREE.SpotLight
定义光源,并设置照射场景的位置,通过shadow.mapSize
、shadow.camera.far
和shadow.camera.near
三个参数来控制阴影的精细程度。
01-03.js
//定义光源
var spotLight = new THREE.SpotLight(0xFFFFFF)
spotLight.position.set(-40, 40, -15)//设置光源位置
spotLight.shadow.mapSize = new THREE.Vector2(1024, 1024)
spotLight.shadow.camera.far = 130
spotLight.shadow.camera.near = 40
scene.add(spotLight);//将光源添加进场景中
如果这时候渲染场景,那么你看到的结果和没有添加光源时是没有区别的,这是因为不同的材质对光源的反应是不一样的。在之前使用的基本材质(THREE.MeshBasicMaterial
)不会对光源有任何反应,基本材质只会使用指定的颜色来渲染物体。所以,还需要改变平面、球体和立方体的材质:
//创建平面
var planeGeometry = new THREE.PlaneGeometry(60, 20)
//将MeshLambertMaterial改变为MeshLamberMaterial
var planeMaterial = new THREE.MeshLambertMaterial({
color: 0xAAAAAA
})
....
将场景中的材质改为MeshLamberMaterial
。Three.js中的材质MeshPhysicalMaterial
和MeshStandardMaterial
(以及被弃用的MeshPhongMaterial
)在渲染是会对光源产生反应。完成材质更改后,渲染效果如下:
虽然立方体和球体已经好看了很多,但是还缺少阴影的效果。
由于渲染阴影需要耗费大量的计算资源,所以默认情况下Three.js中是不会渲染阴影的。为了渲染阴影效果,需要对代码做如下修改:
renderer.setSize(window.innerWidth, window.innerHeight)
//告诉渲染器需要阴影效果
renderer.shadowMap.enabled = true
通过设置shadowMapEnabled
属性为true
来告诉渲染器需要阴影效果。这时候如果查看修改的效果,那么还是没有任何区别,因为还需要明确的指定哪个物体投射阴影castShadow
、哪个物体接受阴影receiveShadow
。通过将相应的属性设置为true
来指定球体和立方体在平面上投影。
plane.receiveShadow = true
...
cube.castShadow = true
...
sphere.castShadow = true
接下来定义能够产生阴影的光源。因为并不是所有的光源都能够产生阴影,但是通过THREE.SpotLight
定义的光源是能够产生阴影的。只需要将属性castShadow
设置为true就可以将阴影渲染出来了。
spotLight.castShadow = true
这样,场景中就有了光源产生的阴影,最后添加THREE.AmbientLight
全局光到场景中,照亮场景
//此光全局均匀地照亮场景中的所有对象。该光没有方向,因此不能用来投射阴影。
var ambienLight = new THREE.AmbientLight(0x353535);
scene.add(ambienLight);
3.让场景动起来
想要让场景动起来,那么首先需要解决的问题是如何在特定的时间间隔重新渲染场景。如果通过setInterval()
方法指定某个函数每隔几毫秒执行一次,那么将导致较高的CPU使用率和性能不良,因为setInterval()方法并没与屏幕的刷新同步。我们可以通过requestAnimationFrame函数完成渲染。通过这个函数,可以向浏览器提供一个回调函数,并且无需定义回调间隔,浏览器会自行决定最佳回调时机。
创建负责绘制场景的回调函数:
01-03.js
function renderScene(){
requestAnimationFrame(renderScene)
renderer.render(scene,camera)
}
在renderScene()方法中,requestAnimationFrame()方法又一次被调用了,这样做的目的是保证动画能够持续运行。 接下来还需要对代码进行修改,在场景创建完毕后,不再调用renderer.render()方法,而是调用renderSene()来启动动画。
document.getElementById("webgl-output").appendChild(renderer.domElement)
//告诉渲染器使用指定的摄像机来渲染场景
renderScene()
运行代码后,效果和之前的相比没有任何区别,这是因为还没有为物体添加任何动画效果.接下来该添加动画效果了,扩展renderScene()
方法来实现红色立方体围绕轴进行旋转。
function renderScene() {
//让立方体围绕轴进行旋转
cube.rotation.x += 0.02
cube.rotation.y += 0.02
cube.rotation.z += 0.02
requestAnimationFrame(renderScene)
renderer.render(scene, camera)
}
以上代码只是在每次调用renderScene()
时使得每个坐标轴的rotation
属性增加0.02,其效果就是立方体将围绕它的每个轴进行缓慢的旋转.完成了立方体旋转,下面将让蓝色球体弹跳起来。
旋转立方体修改的是rotation
属性,让小球弹跳起来,则需要修改球体在场景中的位置(position
)属性。
var step = 0
function renderScene() {
...
//让球上下弹跳
step += 0.04
sphere.position.x = 20 + (10 * (Math.cos(step)))
sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)))
requestAnimationFrame(renderScene)
renderer.render(scene, camera)
}
为了让小球弹跳,需要同时改变球体在x轴和y轴的位置。Math.cos()
和Math.sin()
方法使用step变量就可以创建出球体的运行轨迹,在这里只需要知道step+=0.4定义了球体弹跳的速度就可以。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAhuNGrX-1613396227735)(https://i.ibb.co/pKMjPjd/04.gif)]
4.添加辅助库显示帧数
完成动画之后,如何查看每秒渲染的帧数呢?这个时候就需要使用辅助库Stats.js
,这个库也是Three.js作者开发的,主要用来检测动画运行时的帧数。
为了能够显示帧数,需要在HTML的<head>
标签嵌入这个辅助库
<script type="text/javascript" src="../../libs/util/Stats.js"></script>
然后初始化帧数统计模块,由于initStats
函数并不是这个案例中特有的组件,因此将它保存在辅助函数库util.js文件中。
util.js
/**
* 初始化帧数统计模块
*
* @param {Number} type 0: fps, 1: ms, 2: mb, 3+: custom
* @returns stats javascript object
*/
function initStats(type) {
var panelType = (typeof type !** 'undefined' && type) && (!isNaN(type)) ? parseInt(type) : 0;
var stats = new Stats();
stats.showPanel(panelType); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
return stats;
}
上面的函数初始化帧数统计模块,并用输入的type参数来设置将要显示的统计内容。它可以是:每秒渲染的帧数、每渲染一帧所花费的时间或者内存占用量。
在HTML页面中引用辅助函数库
<script type="text/javascript" src="../js/util.js"></script>
最后在init
函数中调用initStats
方法。
01-03.js
function init(){
//初始化统计模块
var stats = initStats()
...
function renderScene(){
//在renderScene函数中每渲染完一帧后,调用stats.update函数更新统计
stats.update()
...
}
}
完成上述代码后,统计图像将会显示在浏览器左上方
5.使用dat.GUI简化实验流程
在创建类似三维场景和动画时,我们需要尝试很多次才能够确定最合适的速度和颜色。如果有一个GUI(可视化图像界面)允许我们在运行期间修改这些属性,那么事情就会变得很简单。幸运的是,这样的GUI是存在的。Google员工创建了名为dat.GUI的库(相关地址:https://github.com/dataarts/dat.gui),使用这个库可以很容易地创建出能够改变代码变量的界面组件。
接下来使用dat.GUI库为示例添加用户操作界面。使得我们可以:
- 控制小球弹跳的数据
- 控制立方体的旋转
首先将上方的示例重新复制一份,js更名为01-04.js,然后在HTML中引入dat.gui
<script type="text/javascript" src="../../libs/util/dat.gui.js"></script>
接下来需要定义一个JavaScript对象,该对象将保存希望通过dat.GUI改变的属性。
01-04.js
var controls = new function() {
this.rotationSpeed = 0.02;
this.bouncingSpeed = 0.03;
};
在这个JavaScript对象中,定义了两个属性this.rotationSpeed
和this.bouncingSpeed
,以及它们的默认值。接下来需要将这个JavaScript对象传递给dat.GUI对象,并设置这两个属性的取值范围。
var gui = new dat.GUI();
gui.add(controls, 'rotationSpeed', 0, 0.5);
gui.add(controls, 'bouncingSpeed', 0, 0.5);
立方体旋转速度(rotationSpeed)和球体弹跳速度(bouncingSpeed)的取值范围为0~0.5。现在需要做的就是在renderScene()
中直接引用这两个属性,这样当dat.GUI中修改这两个属性的值时,就可以影响相应物体的选择速度和弹跳速度。
function renderScene() {
...
//让立方体围绕轴进行旋转
cube.rotation.x += controls.rotationSpeed;
cube.rotation.y += controls.rotationSpeed;
cube.rotation.z += controls.rotationSpeed;
//让球上下弹跳
step += controls.bouncingSpeed;
sphere.position.x = 20 + (10 * (Math.cos(step)))
sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)))
}
现在运行这个示例就可以看到控制弹跳速度和旋转速度的用户界面。
6.利用鼠标移动摄像机
为了方便在不同角度观察场景,需要引入TrackballControls.js
文件,该文件用于实现利用鼠标移动摄像机。
在HTML中引入文件
<!-- 控制器库,可以利用鼠标任意移动摄像机,以便从不同角度观察场景 -->
<script type="text/javascript" charset="UTF-8" src="../../libs/three/controls/TrackballControls.js"></script>
在辅助函数库util.js文件中初始化TrackballControls
util.js
/**
* 初始化轨迹球控制器来控制场景
*
* @param {THREE.Camera} camera
* @param {THREE.Renderer} renderer
*/
function initTrackballControls(camera, renderer) {
//参数的作用在以后会详细学到,这里引入只是为了方便示例的观察
var trackballControls = new THREE.TrackballControls(camera, renderer.domElement);
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.2;
trackballControls.panSpeed = 0.8;
trackballControls.noZoom = false;
trackballControls.noPan = false;
trackballControls.staticMoving = true;
trackballControls.dynamicDampingFactor = 0.3;
trackballControls.keys = [65, 83, 68];
return trackballControls;
}
在01-04.js中调用initTrackballControls
,由于它需要响应文档对象模型(DOM)元素的事件,所有它的初始化代码要放到appendChild函数调用之后,如下:
01-04.js
//将渲染的结构添加到HTML框架的div元素中。
document.getElementById("webgl-output").appendChild(renderer.domElement)
//初始化轨迹球控制器
var trackballControls = initTrackballControls(camera, renderer);
var clock = new THREE.Clock();
最后,和帧数统计模块相似,TrackballControls
库也需要在renderScene
函数渲染完一帧后更新
function renderScene() {
trackballControls.update(clock.getDelta());
...
}
到此,可以通过鼠标来控制摄像机了,左键:选择摄像机,从不同角度观察场景;滚轮:拉近或拉远摄像机;右键:平移摄像机;
7.场景对浏览器的自适应
在运行上面的示例并改变浏览器大小的时候,发现场景不会随着浏览器大小的改变而自动做出调整。接下来需要为浏览器注册一个事件监听器
function init() {
// 监听resize事件
window.addEventListener('resize', onResize, false);
...
}
这样,每当浏览器尺寸改变时onResize()
方法就会被执行。在onResize()方法中需要更新摄像机和渲染器,代码如下:
//浏览器宽口大小发生改变时
function onResize() {
//屏幕的长宽比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
对于摄像机,需要更新它的aspect
属性,这个属性表示屏幕的长宽比;对于渲染器,需要改变它的尺寸。
总结
通过两天的学习,知道了如何创建THREE.Scene对象,添加摄像机、光源和需要渲染的物体;如何给场景添加阴影和动画效果;添加辅助库dat.GUI和stats.js创建用户控制界面和快速获取场景渲染时的帧数