本文代码仍然使用 Svelte 作为脚手架。如果不了解 Svelte,推荐阅读 Svelte 下的 three.js 开发:基础框架 ,本文也是继续上文的内容。
光源与材质
在 Three.js 中添加新的材质和灯光非常简单。要出现阴影,则前提就是要有光源(这个应该不难理解)。
我们在 Svelte 下的 three.js 开发:基础框架 代码的基础上,添加一个光源:
1 2 3 4 5 6 7 8 9 let spotLight = new THREE .SpotLight (0xFFFFFF );spotLight.position .set (-40 , 40 , -15 ); spotLight.castShadow = true ; spotLight.shadow .mapSize = new THREE .Vector2 (1024 , 1024 ); spotLight.shadow .camera .far = 130 ; spotLight.shadow .camera .near = 40 ; scene.add (spotLight);
在前文中,场景中的物体使用的材质是 MeshBasicMaterial
,它不能和光源发生作用,只能指定渲染的颜色。因此需要改变该场景中的物体(平面、立方体、球体)的材质:
1 2 3 let planeMaterial = new THREE .MeshLambertMaterial let cubeMaterial = new THREE .MeshLambertMaterial let sphereMaterial = new THREE .MeshLambertMaterial
需要把立方体和球体的线框模式(wireframe
)去掉。
阴影
没有阴影,上面的场景就很假,就会给人一种贴图感觉。由于我们已经添加了光源,也更换了物体的材质,那么只需要将阴影选项打开即可:
1 2 3 4 5 6 renderer.shadowMap .enabled = true ; plane.receiveShadow = true ; cube.castShadow = true ; sphere.castShadow = true ;
暂时忽略锯齿,还是能看到效果前面那个好一点点:
动画
如果要让这个场景变成动画,就需要在固定的间隔(帧率)下重新绘制场景。HTML5 以前通常只能用 setInterval
,现在则可以使用 requestAnimationFrame
。
1 2 3 4 5 6 7 8 9 10 11 12 onMount (() => { domElem.appendChild (renderer.domElement ); let frame; function renderScene ( ) { frame = requestAnimationFrame (renderScene); renderer.render (scene, camera); } renderScene (); return () => cancelAnimationFrame (frame); })
return () => cancelAnimationFrame(frame);
类似注销操作,也就是结束销毁动作。
stats.js
现在没有任何改变,因为还没有任何动画操作。为了直观的看到渲染信息,可以使用 Stats.js
,这个 js 库和 three.js 是同一个作者。
1 2 yarn add stats.js yarn add @types/stats.js --dev
使用这个库也很简单,引入 Stats
:
1 2 3 4 5 6 7 8 9 10 import Stats from 'stats.js' ;function initStats (type : number ) { let stats = new Stats (); stats.showPanel (type ) domElem.appendChild (stats.dom ); return stats; }
使用也非常简单:
1 2 3 4 5 6 7 8 9 10 11 onMount (() => { domElem.appendChild (renderer.domElement ); let stats = initStats (0 ); let frame; function renderScene ( ) { stats.update (); frame = requestAnimationFrame (renderScene) renderer.render (scene, camera); }
为了让 Stats 的面板固定在元素的左上角,而不是视口的左上角。可以考虑修改定位。postion
默认是 static
,也称为静态定位。要让 Stats 的面板根据 div
定位,首先就要修改它的定位:
在 App.svelte 中添加样式:
1 2 3 4 5 6 7 8 9 <main > <div class ="three-scene" bind:this ={domElem} > </div > </main > <style > .three-scene { position : relative; } </style >
为了将 Stats 相对于 div
偏移,可以在 initStats
中修改 Stats 的样式,将定位的默认值从 fixed
修改为 absolute
:
1 stats.dom .style .position = "absolute"
absolute
相对于最近的非 static 定位元素进行偏移,而 fixed
是相对于屏幕视口位置偏移。
可以看到,现在 Stats 的面板会固定在元素的左上角:
立方体旋转
现在场景还是静态的,我们必须指定每一帧的变化量,例如,让立方体每个轴向随着帧的更新加上 0.02,那么可以在 renderScene
中更新旋转的值:
1 2 3 4 5 6 7 8 9 10 function renderScene ( ) { stats.update (); cube.rotation .x += 0.02 ; cube.rotation .y += 0.02 ; cube.rotation .z += 0.02 ; frame = requestAnimationFrame (renderScene) renderer.render (scene, camera); }
现在场景里立方体就开始缓慢旋转了。
球体的弹跳
为了让球体来回运动,三角函数肯定是免不了的:
1 2 3 4 5 6 7 8 9 10 11 12 13 let step = 0 ;function renderScene ( ) { stats.update (); step += 0.04 ; sphere.position .x = 20 + 10 * (Math .cos (step)); sphere.position .y = 2 + 10 * Math .abs (Math .sin (step)); frame = requestAnimationFrame (renderScene) renderer.render (scene, camera); }
忽略低劣的 Gif 分辨率,看看动画就行了:
本文内容参考自 《Learning Three.js》
dat.GUI
为了在浏览器上直接操作动画的速度,可以使用 Google 开源的 dat.gui.js。
应该很熟悉了:
1 2 yarn add dat.gui yarn add @types/dat.gui --dev
使用方式都差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { GUI } from 'dat.gui' ;let controls = new function ( ) { this .rotationSpeed = 0.02 ; this .bouncingSpeed = 0.03 ; } function initGUI (controls ) { let gui = new GUI (); gui.add (controls, 'rotationSpeed' , 0 , 0.5 ); gui.add (controls, 'bouncingSpeed' , 0 , 0.5 ); return gui; } onMount (() => { domElem.appendChild (renderer.domElement ); let gui = initGUI (controls); domElem.appendChild (gui.domElement );
使用 dat.gui 控制变量:
1 2 3 4 5 cube.rotation .x += controls.rotationSpeed ; cube.rotation .y += controls.rotationSpeed ; cube.rotation .z += controls.rotationSpeed ; step += controls.bouncingSpeed ;
同样可以考虑修改定位,让它在元素的右上角。由于 div
元素是块级元素,可以修改 display
让它变成行内元素。
1 2 3 4 .three-scene { position : relative; display : inline-block; }
类似地,在 initGUI
中添加:
1 2 3 gui.domElement .style .position = "absolute" ; gui.domElement .style .right = "0px" ; gui.domElement .style .top = "0px" ;
由于帧率和分辨率的原因,下面 gif 看看就行,并不代表实际效果这么劣质 :
控制场景旋转
可以使用轨迹球控制场景旋转,这里用到的是 three.js 自带的 js,可以不需要另外导入新的库:
1 import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js' ;
初始化的过程没什么意外:
1 2 3 4 5 6 7 8 9 10 11 12 function initTrackballControls (camera, renderer ) { let trackballControls = new 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; }
在 onMount
中声明:
1 2 3 4 5 6 7 let trackballControls = initTrackballControls (camera, renderer);let clock = new THREE .Clock ();function renderScene ( ) { trackballControls.update (clock.getDelta ());
这样就可以用鼠标控制相机了:
监听视口大小改变
如果将场景铺满视口,自然会希望当用户改变窗口大小的时候,自动重新渲染场景。要实现这个操作,可以将默认的长宽设置为 window.innerWidth
和 window.innerHeight
。之后加入针对 window
对象的 resize
事件的监听。
在 App.svelte 中的 <script>
中加入响应函数:
1 2 3 4 5 function onResize ( ) { camera.aspect = window .innerWidth / window .innerHeight ; camera.updateProjectionMatrix (); renderer.setSize (window .innerWidth , window .innerHeight ) }
添加监听器:
1 2 3 4 5 6 7 ... </script > <svelte:window on:resize ={onResize} /> <main > ...
这样就改变窗口大小就会自动设置渲染的大小和相机位置了。
小结
到了这里,three.js 的基础框架才算完成了。