three.js

字数 3136 · 2018-02-13

#javascript

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE html>

<html>
  <head>
    <meta charset="UTF-8" />
    <title>First Scene</title>
    <script type="text/javascript" src="js/three.js"></script>
    <style>
      body {
        /* set margin to 0 and overflow to hidden, to go fullscreen */
        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <!-- Div which will hold the Output -->
    <div id="WebGL-output"></div>

    <!-- Javascript code that runs our Three.js examples -->
    <script type="text/javascript">
      // once everything is loaded, we run our Three.js stuff.
      function init() {
        // create a scene, that will hold all our elements such as objects, cameras and lights.
        var scene = new THREE.Scene();

        // create a camera, which defines where we're looking at.
        var camera = new THREE.PerspectiveCamera(
          45,
          window.innerWidth / window.innerHeight,
          0.1,
          1000
        );

        // create a render and set the size
        var renderer = new THREE.WebGLRenderer();
        renderer.setClearColor(new THREE.Color(0xeeeeee));
        renderer.setSize(window.innerWidth, window.innerHeight);

        // create a cube
        var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
        var cubeMaterial = new THREE.MeshBasicMaterial({
          color: 0xff0000,
          wireframe: true,
        });
        var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

        // position the cube
        cube.position.set(-4, 3, 0);
        // add the cube to the scene
        scene.add(cube);

        // position and point the camera to the center of the scene
        camera.position.set(-30, 40, 30);
        camera.lookAt(scene.position);

        // add the output of the renderer to the html element
        document
          .getElementById("WebGL-output")
          .appendChild(renderer.domElement);

        // render the scene
        renderer.render(scene, camera);
      }
      window.onload = init;
    </script>
  </body>
</html>

预览

几何体

three.js 提供了许多种几何体。

使用几何体的方法大致如下

1
2
3
4
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 平面: PlaneGeometry
PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)
// 圆面: CircleGeometry
CircleGeometry(radius : Float, segments : Integer, thetaStart : Float, thetaLength : Float)
// 圆环: RingGeometry
RingGeometry(innerRadius : Float, outerRadius : Float, thetaSegments : Integer, phiSegments : Integer, thetaStart : Float, thetaLength : Float)
// 长方体: BoxGeometry
BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
// 球体: SphereGeometry
SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)
// 柱体: CylinderGeometry(顶部半径, 底部半径, 高度, 半径片段数, ...)
CylinderGeometry(radiusTop : Float, radiusBottom : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)
// 椎体: ConeGeometry
ConeGeometry(radius : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)
// 正八面体: OctahedronGeometry(半径, 细节数)
OctahedronGeometry(radius : Float, detail : Integer)
// 正十二面体: DodecahedronGeometry(半径, 细节数)
DodecahedronGeometry(radius : Float, detail : Integer)
// 正二十面体: IcosahedronGeometry(半径, 细节数)
IcosahedronGeometry(radius : Float, detail : Integer)

thetaStart - 旋转体的初始角,默认值 0
thetaLength - 旋转体的终止角,默认值 2PI

几何体 - 预览

光源

基础光源

环境光

环境光无差别的照亮场景中的物体,环境光没有方向,因此也不能用来投射阴影。

1
2
3
// AmbientLight( color : Integer, intensity : Float )
var light = new THREE.AmbientLight(0x404040); // soft white light
scene.add(light);

点光

点光由一个点向所有方向投射光线,点光可以投射阴影。

1
PointLight( color : Integer, intensity : Float, distance : Number, decay : Float )

intensity 光线强度,越大越强,默认 1。
distance 光线强度变为 0 的距离,设置为 0,则光线不会终止,默认 0。
decay 衰退,随着距离增加光线的衰退量,默认 1。

1
2
3
var light = new THREE.PointLight(0xff0000, 1, 100);
light.position.set(50, 50, 50);
scene.add(light);

聚光

聚光由一点向一个方向投射光线,光照范围大致是个圆锥,聚光可以投射阴影。

1
SpotLight( color : Integer, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float )
1
2
3
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(100, 1000, 100);
scene.add(spotLight);

方向光

DirectionalLight - 方向光

特殊光源

材质

贴图

1
2
3
4
5
6
7
var loader = new THREE.TextureLoader();
loader.load("models/wood.JPG", function (texture) {
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  texture.repeat = new THREE.Vector2(30, 30);
  floor.material.map = texture;
  floor.material.needsUpdate = true;
});

凹凸贴图

Bumping Map

法线贴图

Normal Mapping

粒子

动画

选择物体

选择物体可以使用 THREE.Raycaster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 使用一个二维向量存储鼠标的位置
var mouse = new THREE.Vector2();
// 存储被选择的物体
var INTERSECTED;

var raycaster = new THREE.Raycaster();

function onDocumentMouseMove(event) {
  event.preventDefault();
  // 将坐标标准化到 (-1,1)
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
// 注册dom事件
document.addEventListener("mousemove", onDocumentMouseMove, false);

function animate() {
  // setFromCamera(坐标, 射线原点)
  raycaster.setFromCamera(mouse, camera);
  // intersectObjects(需要判断相交的对象, 是否递归)
  var intersects = raycaster.intersectObjects(scene.children, true);
  if (intersects.length > 0) {
    INTERSECTED = intersects[0].object;
    // ...
  }
  // ...
}
// ...

素描几何体 - 预览

加载模型

加载 obj 模型

Three.js 提供了加载 obj 模型的 js 库 OrbitControls.js,导入即可使用。

1
<script type="text/javascript" src="js/OBJLoader.js"></script>

核心代码十分简单

1
2
3
4
var loader = new THREE.OBJLoader();
loader.load("models/house.obj", function (object) {
  scene.add(object);
});

obj 模型 - 预览

加载带 mtl 的 obj 模型

带 mtl 的 obj 模型 - 预览

obj_with_mtl

效果如图所示,代码也不复杂。

首先导入加载 obj,mtl 的 js 库。

1
2
<script type="text/javascript" src="js/OBJLoader.js"></script>
<script type="text/javascript" src="js/MTLLoader.js"></script>

接着调用 MTLLoader 加载 mtl,在 mtl 的回调中再调用 OBJLoader 加载 obj 即可。

1
2
3
4
5
6
7
8
9
10
11
var mtlLoader = new THREE.MTLLoader();
mtlLoader.load("models/BrickRound.mtl", function (materials) {
  materials.preload();

  var objLoader = new THREE.OBJLoader();
  objLoader.setMaterials(materials);
  objLoader.load("models/BrickRound.obj", function (object) {
    object.scale.set(2, 2, 2);
    scene.add(object);
  });
});

注意一下我们使用的 mtl 文件,最后一行绑定了纹理贴图。

# Blender MTL File: 'BrickRound.blend'
# Material Count: 1
newmtl Material.001_BrickRound0105_5_SPEC.png
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ni 1.000000
d 1.000000
illum 2
map_Kd models/BrickRound0105_5_SPEC.png

加载 fbx 格式的模型

fbx 模型 - 预览

自动制作骨骼动画 - mixamo.com

特效

我们可以使用 EffectComposer 给画面添加一些特效。而 EffectComposer 可以使用多个 Pass 来生成画面。

首先导入 EffectComposer 库,由于 EffectComposer 依赖于 CopyShaderShaderPass,所以这两个也要加上。

1
2
3
<script src="js/CopyShader.js"></script>
<script src="js/postprocessing/ShaderPass.js"></script>
<script src="js/postprocessing/EffectComposer.js"></script>

接着就可以添加一些特效 Pass,可选的 Pass 有很多,每个 Pass 需要的参数和可调的参数都各不相同。

1
2
<script src="js/postprocessing/OutlinePass.js"></script>
<script src="js/postprocessing/RenderPass.js"></script>

EffectComposer 的使用方法大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var composer = new THREE.EffectComposer(renderer);
// 添加一个RenderPass,用于输入secen的场景
var renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加一个OutlinePass 增加描边特效
var outlinePass = new THREE.OutlinePass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  scene,
  camera
);
// outlinePass 需要设置选中的物体
outlinePass.selectedObjects = [obj3d];
// 设置描边颜色
outlinePass.visibleEdgeColor.set("#ff0000");
composer.addPass(outlinePass);
// 添加一个 ShaderPass(CopyShader) 将 outlinePass 的结果输出
var copyPass = new THREE.ShaderPass(THREE.CopyShader);
composer.addPass(copyPass);
// 注意给最后的Pass设置renderToScreen,否则不会输出到屏幕
copyPass.renderToScreen = true;

另外我们还需要修改渲染循环使用 composer.render() 代替 renderer.render()

1
2
3
4
function animate() {
  requestAnimationFrame(animate);
  composer.render();
}

最终结果如下:
描边特效

描边特效

不是所有的 Pass 都会输出到屏幕,我们可以使用 ShaderPass(CopyShader) 来将结果输出。

有多个 RenderPass 时,要注意把 clear 设置为 false

多场景

显示多个场景,可以使用 div 将页面分割。

在渲染方法中, 调用 renderer.setScissorTest( true ); 使裁剪生效。
调用 renderer.setViewport 设置渲染范围,
调用 renderer.setScissor 设置裁剪范围,
最后调用 renderer.render( scene, camera ); 渲染页面。
有多个场景的情况下,上述代码将调用多次。

1
2
3
4
5
6
7
8
9
renderer.setScissorTest(true);
renderer.setViewport(left, top, width, height);
renderer.setScissor(left, top, width, height);
var camera = scene.userData.camera;
var update = scene.userData.update;
if (update != null && update instanceof Function) {
  update(delta);
}
renderer.render(scene, camera);

对于 Controls 类,通常使用第二个参数传入 DOM 限制作用范围。

1
var controls = new THREE.OrbitControls(camera, scene.userData.element);

另外我们还需要注意 <canvas> <div> 的层次,如果 <div><canvas> 遮挡,事件可能无法传递。这时我们可以使用 CSS 的 z-index 来提高 div 的层次,注意 z-index 只有在 position 存在时才会生效(position 为默认值 static 时,z-index 不生效)。

1
2
3
4
#main {
  position: relative;
  z-index: 1;
}

Demo - 材质切换

模型格式说明

obj 文件

mtl 文件

Material Library

mtl 文件是由多个这样的片段组成的

newmtl wire_224198087
    Ns 32
    d 1
    Tr 0
    Tf 1 1 1
    illum 2
    Ka 0.8784 0.7765 0.3412
    Kd 0.8784 0.7765 0.3412
    Ks 0.3500 0.3500 0.3500
    map_Kd BrickRound.png

这一行描述的是材质的名称,在这里是 wire_224198087

newmtl wire_224198087

这一段描述了颜色和光照信息

    Ns 32
    d 1
    illum 2
    Ka 0.8784 0.7765 0.3412
    Kd 0.8784 0.7765 0.3412
    Ks 0.3500 0.3500 0.3500

Ka 指定环境光的反射率,其中 a 是 ambient。
Kd 指定漫反射的反射率,其中 d 是 diffuse。
Ks 指定镜面反射的反射率,其中 s 是 specular。
Ka,Kd,Ks 后面的三个数分别对应 r g b ,取值范围为 0 到 1。

illum 指定光了照模式,有 0 到 10 共计 11 中模式, 用来指定是否启用颜色,环境光,反射,透明,阴影等等。

d dissolve,溶解,大致是指透明度,1 完全不透明,0 完全透明。

Ns 镜面反射指数,通常值在 0 到 1000 之间,值越大,镜面感越强,值很小时几乎观察不到镜面反射。

Ni 光密度,范围通常在 1 到 10 之间,值为 1 时,代表光穿过物体时不发生弯曲,值越大,弯曲越多,玻璃的光密度大约是 1.5。

描述贴图的信息通常以 map 作为前缀

    map_Ka -s 1 1 1 -o 0 0 0 -mm 0 1 chrome.mpc
    map_Kd -s 1 1 1 -o 0 0 0 -mm 0 1 chrome.mpc
    map_Ks -s 1 1 1 -o 0 0 0 -mm 0 1 chrome.mpc
    map_Ns -s 1 1 1 -o 0 0 0 -mm 0 1 wisp.mps
    map_d -s 1 1 1 -o 0 0 0 -mm 0 1 wisp.mps
    disp -s 1 1 .5 wisp.mps
    decal -s 1 1 1 -o 0 0 0 -mm 0 1 sand.mps
    bump -s 1 1 1 -o 0 0 0 -bm 1 sand.mpb

map_Kd 渲染时,纹理的值会乘以 Kd
map_Kd 的格式为 map_Kd -options args filename

options 大致有如下这些

    -blendu on | off
    -blendv on | off
    -clamp on | off
    -imfchan r | g | b | m | l | z
    -mm base gain
    -o u v w
    -s u v w
    -t u v w
    -texres value

-o 纹理偏移量
-s 纹理的缩放值,用来拉伸或是收缩纹理,默认值 1 1 1

u 是纹理水平方向上的值
v 是纹理垂直方向上的值
w 是 3D 纹理的深度值

mtl 格式详细说明 - paulbourke.net

fbx 文件

fbx 是 Autodesk 公司所拥有的格式 (目前 3ds max 和 maya 都是 Autodesk 公司的产品)。
fbx 有两个版本,一个是 ASCII 的,一个是二进制的,二进制的版本没有公开的规格说明,官方提供了读写 sdk。

FBX 可以存储 模型,材质,贴图,动画,光线,相机等等。

houdini 的 fbx 说明

词汇

English 中文
rigged 绑定骨骼的
seamless 无缝的

术语

FXAA
Fast Approximate Anti-Aliasing 快速近似抗锯齿

OpenGL Programming/Post-Processing
Post-processing are effects applied after the main OpenGL scene is rendered.

资源

Demo - stemkoski.github.io