使用 Three.js 创建你的第一个三维场景

首先我们需要配置一下本地环境以运行 Three.js。像书中说的那样,需要一款支持 JavaScript 的文本编辑器,以及一个本地 Web 服务器。

因为自己的博客就是使用的 ASP.NET 写的,所以我这边就直接使用本地的 ASP.NET 工程了。VS 已经集成了 JavaScript 编辑环境和 IIS 服务器,非常方便。

1. 搭建 HTML 框架

如代码清单 1.1 所示,是一个空的 HTML 框架。其中引入了 three.js 库;还引入了 TrackballControls.js,它用于相机控制交互。此外还包含了一个用于显示 Three.js 场景的 div 元素。

代码清单 1.1 空的 HTML 框架
  1. @{
  2.     ViewData["Title"] = "Example 01.01 - Basic skeleton";
  3. }
  4.  
  5. <script type="text/javascript" charset="utf-8" src="~/lib/three/three.js"></script>
  6. <script type="text/javascript" charset="utf-8" src="~/lib/three/controls/TrackballControls.js"></script>
  7. <script type="text/javascript" charset="utf-8" src="~/src/chapter-01/js/01-01.js"></script>
  8.  
  9. <!-- Div which will hold the Output -->
  10. <div id="webgl-output"></div>
  11.  
  12. <script type="text/javascript">
  13.     (function () {
  14.         // contains the code for the example
  15.         init();
  16.     })();
  17. </script>

代码清单 1.2 是 01-01.js 的内容,它打印 Three.js 库的版本号。

代码清单 1.2 01-01.js
  1. function init() {
  2.     console.log("Using Three.js version: " + THREE.REVISION);
  3. }

如图 1 所示,我们可以在控制台里看到打印的结果。

图1 运行结果

2. 渲染并查看三维对象

现在我们改写上一节的 init 函数,创建一个场景,并往场景里添加几个物体。

代码清单 2 01-02.js
  1. function init() {
  2.     // create a scene, that will hold all our elements such as objects, cameras and lights.
  3.     var scene = new THREE.Scene();
  4.  
  5.     // create a camera, which defines where we're looking at.
  6.     var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  7.  
  8.     // create a render and set the size
  9.     var renderer = new THREE.WebGLRenderer();
  10.     renderer.setClearColor(new THREE.Color(0x000000));
  11.     renderer.setSize(window.innerWidth, window.innerHeight);
  12.  
  13.     // show axes in the screen
  14.     var axes = new THREE.AxesHelper(20);
  15.     scene.add(axes);
  16.  
  17.     // create the ground plane
  18.     var planeGeometry = new THREE.PlaneGeometry(60, 20);
  19.     var planeMaterial = new THREE.MeshBasicMaterial({
  20.         color: 0xAAAAAA
  21.     });
  22.     var plane = new THREE.Mesh(planeGeometry, planeMaterial);
  23.  
  24.     // rotate and position the plane
  25.     plane.rotation.x = -0.5 * Math.PI;
  26.     plane.position.set(15, 0, 0);
  27.  
  28.     // add the plane to the scene
  29.     scene.add(plane);
  30.  
  31.     // create a cube
  32.     var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  33.     var cubeMaterial = new THREE.MeshBasicMaterial({
  34.         color: 0xFF0000,
  35.         wireframe: true
  36.     });
  37.     var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  38.  
  39.     // position the cube
  40.     cube.position.set(-4, 3, 0);
  41.  
  42.     // add the cube to the scene
  43.     scene.add(cube);
  44.  
  45.     // create a sphere
  46.     var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  47.     var sphereMaterial = new THREE.MeshBasicMaterial({
  48.         color: 0x777FF,
  49.         wireframe: true
  50.     });
  51.     var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  52.  
  53.     // position the sphere
  54.     sphere.position.set(20, 4, 2);
  55.  
  56.     // add the sphere to the scene
  57.     scene.add(sphere);
  58.  
  59.     // position and point the camera to the center of the scene
  60.     camera.position.set(-30, 40, 30);
  61.     camera.lookAt(scene.position);
  62.  
  63.     // add the output of renderer to the html element
  64.     document.getElementById("webgl-output").appendChild(renderer.domElement);
  65.  
  66.     // render the scene
  67.     renderer.render(scene, camera);
  68. }

编辑器对于 JS 的自动补齐支持的不太好。

本地使用 import 命令能解决一部分补齐:

import * as THREE from '../../../lib/three/three.js'

我们看到最后第 67 行,可以有一个总体上的感知:渲染器跟场景和相机有关。场景告知我们需要渲染什么物体,相机告诉我们从哪里观察物体。

THREE.PerspectiveCamera(fov, aspect, near, far)

fov:视角。

aspect:视口宽高比,表示渲染结果输出画布的宽高比。

near:近平面距离,表示相机能够看到的最近距离。在此距离内的物体将不会被渲染。

far:远平面距离,表示相机能够看到的最远距离。在此距离外的物体也将不会被渲染。

场景中我们会添加一个平面,注意它会绕 x 轴逆时针旋转 90 度;还添加一个方块和球体。为了方便调试查看物体摆放的位置,场景中还添加了一个坐标轴。

Three.js 坐标系的原点位于场景中心。X轴向右为正,Y轴向上为正,Z轴指向屏幕外侧为正。

AxesHelper 中的红轴为 X 轴,绿轴为 Y 轴,蓝轴为 Z 轴。

在 Three.js 中物体都通过 THREE.Mesh 表示,代表一个网络模型。它由两部分组成:几何体和材质。几何体包括形状信息,可以理解成顶点、块面这些信息;而材质包括了外观信息,可以理解成颜色、纹理这些信息。

最终的运行结果如图 2 所示,我们可以从图中感受和核对代码的逻辑。同时可以修改代码中的参数,结合结果进一步理解参数含义。

图2 运行结果

3. 添加材质、光源和阴影效果

在代码清单 3 中,我们添加了一个点光源 THREE.SpotLight。上一节中的默认材质不能反应光源,此处我们将物体材质改为兰伯特材质。兰伯特材质反应的光线强度与入射光线和表面法向量的夹角相关。

代码清单 3 01-03.js
  1. function init() {
  2.     // create a scene, that will hold all our elements such as objects, cameras and lights.
  3.     var scene = new THREE.Scene();
  4.  
  5.     // create a camera, which defines where we're looking at.
  6.     var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  7.  
  8.     // create a render and configure it with shadows
  9.     var renderer = new THREE.WebGLRenderer();
  10.     renderer.setClearColor(new THREE.Color(0x000000));
  11.     renderer.setSize(window.innerWidth, window.innerHeight);
  12.     renderer.shadowMap.enabled = true;
  13.  
  14.     // create a cube
  15.     var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  16.     var cubeMaterial = new THREE.MeshLambertMaterial({
  17.         color: 0xFF0000
  18.     });
  19.     var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  20.     cube.castShadow = true;
  21.     // position the cube
  22.     cube.position.x = -4;
  23.     cube.position.y = 2;
  24.     cube.position.z = 0;
  25.  
  26.     // create a sphere
  27.     var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  28.     var sphereMaterial = new THREE.MeshLambertMaterial({
  29.         color: 0x7777ff
  30.     });
  31.     var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  32.     // position the sphere
  33.     sphere.position.x = 20;
  34.     sphere.position.y = 4;
  35.     sphere.position.z = 2;
  36.     sphere.castShadow = true;
  37.  
  38.     // create the ground plane
  39.     var planeGeometry = new THREE.PlaneGeometry(60, 20);
  40.     var planeMaterial = new THREE.MeshLambertMaterial({
  41.         color: 0xAAAAAA
  42.     });
  43.     var plane = new THREE.Mesh(planeGeometry, planeMaterial);
  44.     // rotate and position the plane
  45.     plane.rotation.x = -0.5 * Math.PI;
  46.     plane.position.set(15, 0, 0);
  47.     plane.receiveShadow = true;
  48.  
  49.     // add the object
  50.     scene.add(cube);
  51.     scene.add(sphere);
  52.     scene.add(plane);
  53.  
  54.     // position and point the camera to the center of the scene
  55.     camera.position.x = -30;
  56.     camera.position.y = 40;
  57.     camera.position.z = 30;
  58.     camera.lookAt(scene.position);
  59.  
  60.     // add spotlight for the shadows
  61.     var spotLight = new THREE.SpotLight(0xFFFFFF);
  62.     spotLight.position.set(-40, 40, -15);
  63.     spotLight.castShadow = true;
  64.     spotLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
  65.     spotLight.shadow.camera.far = 130;
  66.     spotLight.shadow.camera.near = 40;
  67.  
  68.     scene.add(spotLight);
  69.  
  70.     // add the output of the renderer to the html element
  71.     document.getElementById("webgl-output").appendChild(renderer.domElement);
  72.  
  73.     // call the render function
  74.     renderer.render(scene, camera);
  75. }

代码第 63 行,spotLight.castShadow 表示点光源是否产生阴影(不是所有光线都会产生阴影)。物体上也需要指定此物体是否是阴影的产生者,如第 20 行,可以通过 cube.castShadow 开启。除此之外,还需要定义阴影的接收者,此处是平面,如代码 47 行所示,可以通过 plane.receiveShadow 设置。

spotLight.shadow.camera.far 表示从光源位置开始,在光线照射下阴影范围的最远距离。如果一个物体在这个距离之外,则不会产生阴影。

spotLight.shadow.camera.near 表示从光源位置开始,在光线照射下阴影范围的最近距离。如果一个物体在这个距离之内,则不会产生阴影。

这边看着比较麻烦,又要指定阴影的产生者,又要指定接收者。

其实是因为阴影的模拟计算比较耗时,这样逐个指定更具选择性,对不必要的物体可以不进行渲染。

在 Three.js 中,使用深度贴图绘制阴影。如代码第 64 行,spotLight.shadow.mapSize 指定贴图的大小,大小越大,阴影精度越高。同时不要忘记渲染器使能深度贴图绘制,如代码第 12 行所示,通过 renderer.shadowMap.enabled 开启。

图 3 是运行结果,可以看到物体上有了明暗的渐变,同时也产生了阴影。

图3 运行结果

4. 让你的场景动起来

让场景动起来的方式,就是更新每帧渲染的内容。这边我们使用浏览器提供的 requestAnimationFrame API 来进行每帧的更新。

requestAnimationFrame 和 setInterval 类似,都是提供定时回调功能。

不过 requestAnimationFrame 的执行频率通常与浏览器的屏幕刷新频率相同。依托于浏览器本身的刷新策略,不会浪费系统资源,画面也更加流畅。

4.1 引入 requestAnimationFrame() 方法

我们先做好帧更新的框架,并先引入性能监测。如代码清单 4.1 所示,我们通过引入 Stats.js 库还实现性能监测、

代码清单 4.1 引入 Stats.js
  1. @{
  2.     ViewData["Title"] = "Example 01.04 - Materials, light and animation";
  3. }
  4.  
  5. <script type="text/javascript" charset="utf-8" src="~/lib/three/three.js"></script>
  6. <script type="text/javascript" charset="utf-8" src="~/lib/three/controls/TrackballControls.js"></script>
  7.  
  8. <script type="text/javascript" charset="utf-8" src="~/lib/util/Stats.js"></script>
  9. <script type="text/javascript" charset="utf-8" src="~/js/util.js"></script>
  10.  
  11. <script type="text/javascript" charset="utf-8" src="~/src/chapter-01/js/01-04.js"></script>
  12.  
  13. <!-- Div which will hold the Output -->
  14. <div id="webgl-output"></div>
  15.  
  16. <script type="text/javascript">
  17.     (function () {
  18.         // contains the code for the example
  19.         init();
  20.     })();
  21. </script>

代码清单 4.2 是帧更新的基本框架。我们主要看到第 82 至 88 行的 renderScene() 函数,其中对性能监测状态进行更新,并重新将 renderScene() 通过 requestAnimationFrame 注册为回调函数。需要注意,场景渲染 renderer.render 也移到了帧更新的函数中。

代码清单 4.2 帧更新基本框架
  1. function init() {
  2.     var stats = initStats();
  3.  
  4.     // default setup
  5.     var scene = new THREE.Scene();
  6.     var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  7.     var renderer = new THREE.WebGLRenderer();
  8.  
  9.     renderer.setClearColor(new THREE.Color(0x000000));
  10.     renderer.setSize(window.innerWidth, window.innerHeight);
  11.     renderer.shadowMap.enabled = true;
  12.  
  13.     // show axes in the screen
  14.     var axes = new THREE.AxesHelper(20);
  15.     scene.add(axes);
  16.  
  17.     // create the ground plane
  18.     var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
  19.     var planeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
  20.     var plane = new THREE.Mesh(planeGeometry, planeMaterial);
  21.     plane.receiveShadow = true;
  22.  
  23.     // rotate and position the plane
  24.     plane.rotation.x = -0.5 * Math.PI;
  25.     plane.position.x = 15;
  26.     plane.position.y = 0;
  27.     plane.position.z = 0;
  28.  
  29.     // add the plane to the scene
  30.     scene.add(plane);
  31.  
  32.     // create a cube
  33.     var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  34.     var cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
  35.     var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  36.     cube.castShadow = true;
  37.  
  38.     // position the cube
  39.     cube.position.x = -4;
  40.     cube.position.y = 4;
  41.     cube.position.z = 0;
  42.  
  43.     // add the cube to the scene
  44.     scene.add(cube);
  45.  
  46.     var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  47.     var sphereMaterial = new THREE.MeshLambertMaterial({ color: 0x7777ff });
  48.     var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  49.  
  50.     // position the sphere
  51.     sphere.position.x = 20;
  52.     sphere.position.y = 0;
  53.     sphere.position.z = 2;
  54.     sphere.castShadow = true;
  55.  
  56.     // add the sphere to the scene
  57.     scene.add(sphere);
  58.  
  59.     // position and point the camera to the center of the scene
  60.     camera.position.x = -30;
  61.     camera.position.y = 40;
  62.     camera.position.z = 30;
  63.     camera.lookAt(scene.position);
  64.  
  65.     // add subtle ambient light
  66.     var ambientLight = new THREE.AmbientLight(0x353535);
  67.     scene.add(ambientLight);
  68.  
  69.     // add spotlight for the shadows
  70.     var spotLight = new THREE.SpotLight(0xffffff);
  71.     spotLight.position.set(-10, 20, -5);
  72.     spotLight.castShadow = true;
  73.     scene.add(spotLight);
  74.  
  75.     // add the output of the renderer to the html element
  76.     document.getElementById("webgl-output").appendChild(renderer.domElement);
  77.  
  78.     // call the render function
  79.     var step = 0;
  80.     renderScene();
  81.  
  82.     function renderScene() {
  83.         stats.update();
  84.  
  85.         // render using requestAnimationFrame
  86.         requestAnimationFrame(renderScene);
  87.         renderer.render(scene, camera);
  88.     }
  89. }

这边使用了一个新的光源 THREE.AmbientLight 环境光。环境光不会产生阴影效果。

运行结果如图 4 所示,可以看到左上角显示了帧率情况。同时因为添加了环境光,场景看着比之前的例子更亮了一点。

图4 运行结果

4.2 旋转立方体和弹跳球

现在我们在 4.1 的基础上加上物体的动画,让立方体进行旋转、让球进行弹跳。

如代码清单 4.3 所示,我们只需要在 renderScene() 增加对立方体角度的设置以及球的位置设置即可。

代码清单 4.3 物体动画
  1.     function renderScene() {
  2.         stats.update();
  3.  
  4.         // rotate the cube around its axes
  5.         cube.rotation.x += 0.02;
  6.         cube.rotation.y += 0.02;
  7.         cube.rotation.z += 0.02;
  8.  
  9.         // bounce the sphere up and down
  10.         step += 0.04;
  11.         sphere.position.x = 20 + (10 * (Math.cos(step)));
  12.         sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));
  13.  
  14.         // render using requestAnimationFrame
  15.         requestAnimationFrame(renderScene);
  16.         renderer.render(scene, camera);
  17.     }

sphere.position.x = 20 + (10 * (Math.cos(step)));

振幅 10,范围 10 ~ 30。

sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));

振幅 10,范围 2 ~ 12。

最终的运行效果如视频 1 所示。

视频1 物体动画

5. 使用 dat.GUI 简化试验流程

dat.GUI 是一个用于创建简单图形用户界面(GUI)的JavaScript库。它可以用于创建交互式控件,例如滑块、按钮、选择器等,用于在运行时调整程序的参数和行为。

引入 dat.GUI 库的方式和 4.1 中引入性能监测库一样:

  • <script type="text/javascript" charset="utf-8" src="~/lib/util/dat.gui.js"></script>

代码清单 01-05.js 是具体的实现代码,我们直接从第 81 行看起。第 81 至 84 行,创建了一个名为 controls 的对象,它包含了两个属性 rotationSpeed 和 bouncingSpeed,分别表示立方体的旋转速度和球的弹跳速度。

第 86 行创建了一个dat.GUI对象,用于显示用户界面。第 87 和第 88 行通过调用 add 方法,将 controls 对象中的 rotationSpeed 和 bouncingSpeed 属性添加到用户界面中,以便用户可以通过界面来调整它们的值。

代码清单 5 01-05.js
  1. function init() {
  2.     var stats = initStats();
  3.  
  4.     // create a scene, that will hold all our elements such as objects, cameras and lights.
  5.     var scene = new THREE.Scene();
  6.  
  7.     // create a camera, which defines where we're looking at.
  8.     var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  9.  
  10.     // create a render and set the size
  11.     var renderer = new THREE.WebGLRenderer();
  12.  
  13.     renderer.setClearColor(new THREE.Color(0x000000));
  14.     renderer.setSize(window.innerWidth, window.innerHeight);
  15.     renderer.shadowMap.enabled = true;
  16.  
  17.     // create the ground plane
  18.     var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
  19.     var planeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
  20.     var plane = new THREE.Mesh(planeGeometry, planeMaterial);
  21.     plane.receiveShadow = true;
  22.  
  23.     // rotate and position the plane
  24.     plane.rotation.x = -0.5 * Math.PI;
  25.     plane.position.x = 15;
  26.     plane.position.y = 0;
  27.     plane.position.z = 0;
  28.  
  29.     // add the plane to the scene
  30.     scene.add(plane);
  31.  
  32.     // create a cube
  33.     var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  34.     var cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
  35.     var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  36.     cube.castShadow = true;
  37.  
  38.     // position the cube
  39.     cube.position.x = -4;
  40.     cube.position.y = 3;
  41.     cube.position.z = 0;
  42.  
  43.     // add the cube to the scene
  44.     scene.add(cube);
  45.  
  46.     var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  47.     var sphereMaterial = new THREE.MeshLambertMaterial({ color: 0x7777ff });
  48.     var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  49.  
  50.     // position the sphere
  51.     sphere.position.x = 20;
  52.     sphere.position.y = 0;
  53.     sphere.position.z = 2;
  54.     sphere.castShadow = true;
  55.  
  56.     // add the sphere to the scene
  57.     scene.add(sphere);
  58.  
  59.     // position and point the camera to the center of the scene
  60.     camera.position.x = -30;
  61.     camera.position.y = 40;
  62.     camera.position.z = 30;
  63.     camera.lookAt(scene.position);
  64.  
  65.     // add subtle ambient lighting
  66.     var ambienLight = new THREE.AmbientLight(0x353535);
  67.     scene.add(ambienLight);
  68.  
  69.     // add spotlight for the shadows
  70.     var spotLight = new THREE.SpotLight(0xffffff);
  71.     spotLight.position.set(-10, 20, -5);
  72.     spotLight.castShadow = true;
  73.     scene.add(spotLight);
  74.  
  75.     // add the output of the renderer to the html element
  76.     document.getElementById("webgl-output").appendChild(renderer.domElement);
  77.  
  78.     // call the render function
  79.     var step = 0;
  80.  
  81.     var controls = new function () {
  82.         this.rotationSpeed = 0.02;
  83.         this.bouncingSpeed = 0.03;
  84.     }
  85.  
  86.     var gui = new dat.GUI();
  87.     gui.add(controls, "rotationSpeed", 0, 0.5);
  88.     gui.add(controls, "bouncingSpeed", 0, 0.5);
  89.  
  90.     // attach them here, since appendChild needs to be called first
  91.     var trackballControls = initTrackballControls(camera, renderer);
  92.     var clock = new THREE.Clock();
  93.  
  94.     render();
  95.  
  96.     function render() {
  97.         // update the stats and the controls
  98.         trackballControls.update(clock.getDelta());
  99.         stats.update();
  100.  
  101.         // rotate the cube around its axes
  102.         cube.rotation.x += controls.rotationSpeed;
  103.         cube.rotation.y += controls.rotationSpeed;
  104.         cube.rotation.z += controls.rotationSpeed;
  105.  
  106.         // bounce the sphere up and down
  107.         step += controls.bouncingSpeed;
  108.         sphere.position.x = 20 + (10 * (Math.cos(step)));
  109.         sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));
  110.  
  111.         // render using requestAnimationFrame
  112.         requestAnimationFrame(render);
  113.         renderer.render(scene, camera);
  114.     }
  115. }

运行结果如图 5 所示,我们可以看到右上角的两个滑块,可以通过滑动它们调节旋转速度以及弹跳速度。

图5 运行结果

我们继续看到代码清单 5 的第 91 行。我们一早就引入了 TrackballControls 的库,但是没有初始化,此处就对其进行了初始化。需要注意的是,要在把渲染的 dom appendChild 到浏览器 dom 中,才能初始化 TrackballControls。初始化完之后,我们就可以按住鼠标左键来转动摄像机。

TrackballControls 库可以鼠标控制相机位置,所以必须在渲染的 dom 添加到了浏览器 dom 之后,才能绑定鼠标等事件。

6. 场景对浏览器的自适应

目前我们的渲染区域还不能自适应屏幕,渲染区域在初始化的时候就固定了。

为了实现渲染区域自适应屏幕,如代码清单 5 第 3 行所示,我们绑定 resize 事件监听。当窗口的大小发生改变时,会触发 resize 事件,然后执行我们绑定的 onResize 函数。

onResize() 函数在第 125 至 129 行,其中改变了相机的 aspect 宽高比参数,并更新了投影矩阵。最后设置了渲染器新的渲染尺寸。

相机的投影矩阵和 fov、aspect、far、near 相关,而窗口大小的改变只影响到 aspect 参数。

代码清单 5 01-06.js
  1. function init() {
  2.     // listen to the resize events
  3.     window.addEventListener('resize', onResize, false);
  4.  
  5.     var camera;
  6.     var scene;
  7.     var renderer;
  8.  
  9.     // initialize stats
  10.     var stats = initStats();
  11.  
  12.     // create a scene, that will hold all our elements such as objects, cameras and lights.
  13.     scene = new THREE.Scene();
  14.  
  15.     // create a camera, which defines where we're looking at.
  16.     camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  17.  
  18.     // create a render and set the size
  19.     renderer = new THREE.WebGLRenderer();
  20.  
  21.     renderer.setClearColor(new THREE.Color(0x000000));
  22.     renderer.setSize(window.innerWidth, window.innerHeight);
  23.     renderer.shadowMap.enabled = true;
  24.  
  25.     // create the ground plane
  26.     var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
  27.     var planeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
  28.     var plane = new THREE.Mesh(planeGeometry, planeMaterial);
  29.     plane.receiveShadow = true;
  30.  
  31.     // rotate and position the plane
  32.     plane.rotation.x = -0.5 * Math.PI;
  33.     plane.position.x = 15;
  34.     plane.position.y = 0;
  35.     plane.position.z = 0;
  36.  
  37.     // add the plane to the scene
  38.     scene.add(plane);
  39.  
  40.     // create a cube
  41.     var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  42.     var cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
  43.     var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  44.     cube.castShadow = true;
  45.  
  46.     // position the cube
  47.     cube.position.x = -4;
  48.     cube.position.y = 3;
  49.     cube.position.z = 0;
  50.  
  51.     // add the cube to the scene
  52.     scene.add(cube);
  53.  
  54.     var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  55.     var sphereMaterial = new THREE.MeshLambertMaterial({ color: 0x7777ff });
  56.     var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  57.  
  58.     // position the sphere
  59.     sphere.position.x = 20;
  60.     sphere.position.y = 0;
  61.     sphere.position.z = 2;
  62.     sphere.castShadow = true;
  63.  
  64.     // add the sphere to the scene
  65.     scene.add(sphere);
  66.  
  67.     // position and point the camera to the center of the scene
  68.     camera.position.x = -30;
  69.     camera.position.y = 40;
  70.     camera.position.z = 30;
  71.     camera.lookAt(scene.position);
  72.  
  73.     // add subtle ambient lighting
  74.     var ambienLight = new THREE.AmbientLight(0x353535);
  75.     scene.add(ambienLight);
  76.  
  77.     // add spotlight for the shadows
  78.     var spotLight = new THREE.SpotLight(0xffffff);
  79.     spotLight.position.set(-10, 20, -5);
  80.     spotLight.castShadow = true;
  81.     scene.add(spotLight);
  82.  
  83.     // add the output of the renderer to the html element
  84.     document.getElementById("webgl-output").appendChild(renderer.domElement);
  85.  
  86.     // initialize the trackball controls and the clock which is needed
  87.     var trackballControls = initTrackballControls(camera, renderer);
  88.     var clock = new THREE.Clock();
  89.  
  90.     // call the render function
  91.     var step = 0;
  92.  
  93.     var controls = new function () {
  94.         this.rotationSpeed = 0.02;
  95.         this.bouncingSpeed = 0.03;
  96.     };
  97.  
  98.     var gui = new dat.GUI();
  99.     gui.add(controls, 'rotationSpeed', 0, 0.5);
  100.     gui.add(controls, 'bouncingSpeed', 0, 0.5);
  101.  
  102.     render();
  103.  
  104.     function render() {
  105.  
  106.         // update the stats and the controls
  107.         trackballControls.update(clock.getDelta());
  108.         stats.update();
  109.  
  110.         // rotate the cube around its axes
  111.         cube.rotation.x += controls.rotationSpeed;
  112.         cube.rotation.y += controls.rotationSpeed;
  113.         cube.rotation.z += controls.rotationSpeed;
  114.  
  115.         // bounce the sphere up and down
  116.         step += controls.bouncingSpeed;
  117.         sphere.position.x = 20 + (10 * (Math.cos(step)));
  118.         sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));
  119.  
  120.         // render using requestAnimationFrame
  121.         requestAnimationFrame(render);
  122.         renderer.render(scene, camera);
  123.     }
  124.  
  125.     function onResize() {
  126.         camera.aspect = window.innerWidth / window.innerHeight;
  127.         camera.updateProjectionMatrix();
  128.         renderer.setSize(window.innerWidth, window.innerHeight);
  129.     }
  130. }

最后这个样例就不贴图了,因为样例本身就是能在 Web 上运行的。以下就是整体代码在浏览器上运行的效果。