使用Three.js创建第一个三维场景(下)

本文案例代码:链接: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.mapSizeshadow.camera.farshadow.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中的材质MeshPhysicalMaterialMeshStandardMaterial(以及被弃用的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.rotationSpeedthis.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创建用户控制界面和快速获取场景渲染时的帧数

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值