logo头像
Snippet 博客主题

threejs精灵和粒子系统

前面我们了解到了场景中的网格对象由几何体和材质组成,并且分别系统学习了它们。这节我们将学习一个特殊的网格对象—–粒子(精灵)。

了解粒子

一个粒子(新版叫精灵)是 一个二维平面(小方块) ,它总是面向摄像机。

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
69
70
71
72
73
74
75
<!-- chapter-07-01.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particles</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

//var canvasRenderer = new THREE.CanvasRenderer();
var canvasRenderer = new THREE.WebGLRenderer();
canvasRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
canvasRenderer.setSize(window.innerWidth, window.innerHeight);

camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 150;
document.getElementById("WebGL-output").appendChild(canvasRenderer.domElement);

createSprites();
render();

function createSprites() {
// 创建精灵材质
var material = new THREE.SpriteMaterial();
for (var x = -5; x < 5; x++) {
for (var y = -5; y < 5; y++) {
// 创建精灵,只需要指定材质
var sprite = new THREE.Sprite(material);
sprite.position.set(x * 10, y * 10, 0);
scene.add(sprite);
}
}
}

function render() {
stats.update();
requestAnimationFrame(render);
canvasRenderer.render(scene, camera);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


大量粒子

如果要创建大量粒子,使用上面THREE.Sprite一个个创建那就太耗性能了。对于这种场景,我们要使用THREE.PointCloudMaterialTHREE.PointCloud来管理。

THREE.PointCloud构造函数接收两个属性:几何体和材质,和创建网格对象类似。材质用来给粒子着色或添加纹理,而几何体则用来指定粒子的位置,几何体每个顶点将会以粒子的形态展示出来。

THREE.PointCloudMaterial材质的属性:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!-- chapter-07-02.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particle Basic Material</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);

camera.position.x = 20;
camera.position.y = 0;
camera.position.z = 150;
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var cloud;
var controls = new function () {
this.size = 4;
this.transparent = true;
this.opacity = 0.6;
this.vertexColors = true;
this.color = 0xffffff;
this.sizeAttenuation = true;
this.rotateSystem = true;

this.redraw = function () {
if (scene.getObjectByName("particles")) {
scene.remove(scene.getObjectByName("particles"));
}
createParticles(controls.size, controls.transparent, controls.opacity, controls.vertexColors, controls.sizeAttenuation, controls.color);
};
};

var gui = new dat.GUI();
gui.add(controls, 'size', 0, 10).onChange(controls.redraw);
gui.add(controls, 'transparent').onChange(controls.redraw);
gui.add(controls, 'opacity', 0, 1).onChange(controls.redraw);
gui.add(controls, 'vertexColors').onChange(controls.redraw);
gui.addColor(controls, 'color').onChange(controls.redraw);
gui.add(controls, 'sizeAttenuation').onChange(controls.redraw);
gui.add(controls, 'rotateSystem');

controls.redraw();
render();

// 使用THREE.PointCloud创建大量粒子
function createParticles(size, transparent, opacity, vertexColors, sizeAttenuation, color) {
// 创建点云材质
var material = new THREE.PointCloudMaterial({
size: size,
transparent: transparent,
opacity: opacity,
vertexColors: vertexColors,
sizeAttenuation: sizeAttenuation,
color: color
});

// 创建自定义几何体
var geom = new THREE.Geometry();
var range = 500;
for (var i = 0; i < 15000; i++) {
var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
geom.vertices.push(particle);
var color = new THREE.Color(0x00ff00);
color.setHSL(color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l);
geom.colors.push(color);
}

// 创建点云对象
cloud = new THREE.PointCloud(geom, material);
cloud.name = "particles";
scene.add(cloud);
}

var step = 0;
function render() {
stats.update();
if (controls.rotateSystem) {
step += 0.01;
cloud.rotation.x = step;
cloud.rotation.z = step;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


样式化粒子

默认粒子会渲染成小方块,下面来看看怎么改变粒子样式。

1. 使用HTML5画布样式化粒子

前面提到过,THREE.PointCloudMaterial的map属性可以为粒子加载纹理,以此来改变粒子样式(关于纹理后续会详细介绍,这里只需关注使用)。首先我们看看把HTML5的画布转换为纹理来作为粒子的纹理运用。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<!-- chapter-07-03.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particles - Canvas based texture - WebGL</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);

camera.position.x = 20;
camera.position.y = 0;
camera.position.z = 150;
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

// 获取画布的纹理
var getTexture = function () {
var canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;

var ctx = canvas.getContext('2d');
// the body
ctx.translate(-81, -84);

ctx.fillStyle = "orange";
ctx.beginPath();
ctx.moveTo(83, 116);
ctx.lineTo(83, 102);
ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
ctx.lineTo(111, 116);
ctx.lineTo(106.333, 111.333);
ctx.lineTo(101.666, 116);
ctx.lineTo(97, 111.333);
ctx.lineTo(92.333, 116);
ctx.lineTo(87.666, 111.333);
ctx.lineTo(83, 116);
ctx.fill();

// the eyes
ctx.fillStyle = "white";
ctx.beginPath();
ctx.moveTo(91, 96);
ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
ctx.moveTo(103, 96);
ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
ctx.fill();

// the pupils
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.arc(101, 102, 2, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(89, 102, 2, 0, Math.PI * 2, true);
ctx.fill();

var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;
};

var cloud;
var controls = new function () {
this.size = 15;
this.transparent = true;
this.opacity = 0.6;
this.color = 0xffffff;
this.rotateSystem = true;
this.sizeAttenuation = true;

this.redraw = function () {
if (scene.getObjectByName("pointcloud")) {
scene.remove(scene.getObjectByName("pointcloud"));
}
createPointCloud(controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color);
};
};

var gui = new dat.GUI();
gui.add(controls, 'size', 0, 20).onChange(controls.redraw);
gui.add(controls, 'transparent').onChange(controls.redraw);
gui.add(controls, 'opacity', 0, 1).onChange(controls.redraw);
gui.addColor(controls, 'color').onChange(controls.redraw);
gui.add(controls, 'sizeAttenuation').onChange(controls.redraw);
gui.add(controls, 'rotateSystem');
controls.redraw();
render();

function createPointCloud(size, transparent, opacity, sizeAttenuation, color) {
var geom = new THREE.Geometry();
var material = new THREE.PointCloudMaterial({
size: size,
transparent: transparent,
opacity: opacity,
map: getTexture(), // 使用外部纹理
sizeAttenuation: sizeAttenuation,
color: color
});

var range = 500;
for (var i = 0; i < 5000; i++) {
var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
geom.vertices.push(particle);
}

cloud = new THREE.PointCloud(geom, material);
cloud.name = 'pointcloud';
// 渲染之前所以粒子按z轴排序,解决部分粒子交叠问题,但是注意会影响性能。
cloud.sortParticles = true;
// 开启后摄像机可见范围外的粒子不会渲染,必要时,使用可以提高性能和帧率。
cloud.FrustumCulled = true;
scene.add(cloud);
}

var step = 0;
function render() {
stats.update();
if (controls.rotateSystem) {
step += 0.01;
cloud.rotation.x = step;
cloud.rotation.z = step;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

2. 使用纹理样式化粒子

前面使用HTML5画布输出纹理还是显得麻烦,我们还有一种更直接的方式,你可以使用THREE.ImageUtils.loadTexture()函数将图像加载为THREE.Texture,然后赋值给材质的map属性。

注意图片应该是正方形的,并且尺寸最好是2的幂,背景选择黑色(能够更正确地融合)。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!-- chapter-07-04.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particles - Rainy scene</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);

camera.position.x = 20;
camera.position.y = 40;
camera.position.z = 110;
camera.lookAt(new THREE.Vector3(20, 30, 0));
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var cloud;
var controls = new function () {
this.size = 3;
this.transparent = true;
this.opacity = 0.6;
this.color = 0xffffff;
this.sizeAttenuation = true;
this.redraw = function () {
scene.remove(scene.getObjectByName("particles1"));
scene.remove(scene.getObjectByName("particles2"));
createPointCloud(controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color);
};
};

var gui = new dat.GUI();
gui.add(controls, 'size', 0, 20).onChange(controls.redraw);
gui.add(controls, 'transparent').onChange(controls.redraw);
gui.add(controls, 'opacity', 0, 1).onChange(controls.redraw);
gui.addColor(controls, 'color').onChange(controls.redraw);
gui.add(controls, 'sizeAttenuation').onChange(controls.redraw);
controls.redraw();
render();

function createPointCloud(size, transparent, opacity, sizeAttenuation, color) {
// 加载本地图片作为粒子纹理
var texture = THREE.ImageUtils.loadTexture("../assets/textures/particles/raindrop-3.png");

var geom = new THREE.Geometry();
var material = new THREE.ParticleBasicMaterial({
size: size,
transparent: transparent,
opacity: opacity,
map: texture,
blending: THREE.AdditiveBlending,
sizeAttenuation: sizeAttenuation,
color: color
});

var range = 40;
for (var i = 0; i < 1500; i++) {
var particle = new THREE.Vector3(
Math.random() * range - range / 2,
Math.random() * range * 1.5,
Math.random() * range - range / 2);
particle.velocityY = 0.1 + Math.random() / 5;
particle.velocityX = (Math.random() - 0.5) / 3;
geom.vertices.push(particle);
}

cloud = new THREE.ParticleSystem(geom, material);
cloud.sortParticles = true;
scene.add(cloud);
}

function render() {
stats.update();

// 模拟下雨效果
var vertices = cloud.geometry.vertices;
vertices.forEach(function (v) {
v.y = v.y - (v.velocityY);
v.x = v.x - (v.velocityX);
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
});

requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


使用精灵贴图

精灵贴图是很多小精灵图片打包在一张图片上,一次加载这张大图后,使用时通过纹理的偏移来选取需要的小图片。

这个示例展示了创建两个场景使用不同摄像机(游戏里场景和UI界面就可以这样使用)

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<!-- chapter-07-05.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particles - Sprites</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();

// 透视相机场景和正交相机场景
var scene = new THREE.Scene();
var sceneOrtho = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 250);
var cameraOrtho = new THREE.OrthographicCamera(0, window.innerWidth, window.innerHeight, 0, -10, 10);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);

camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 50;
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

// 球体
var material = new THREE.MeshNormalMaterial();
var geom = new THREE.SphereGeometry(15, 20, 20);
var mesh = new THREE.Mesh(geom, material);
scene.add(mesh);

// 获取精灵贴图纹理
var getTexture = function () {
var texture = new THREE.ImageUtils.loadTexture("../assets/textures/particles/sprite-sheet.png");
return texture;
};

var controls = new function () {
this.size = 150;
this.sprite = 0;
this.transparent = true;
this.opacity = 0.6;
this.color = 0xffffff;
this.rotateSystem = true;

this.redraw = function () {
sceneOrtho.children.forEach(function (child) {
if (child instanceof THREE.Sprite) sceneOrtho.remove(child);
});
createSprite(controls.size, controls.transparent, controls.opacity, controls.color, controls.sprite);
};
};

var gui = new dat.GUI();
gui.add(controls, 'sprite', 0, 4).step(1).onChange(controls.redraw);
gui.add(controls, 'size', 0, 120).onChange(controls.redraw);
gui.add(controls, 'transparent').onChange(controls.redraw);
gui.add(controls, 'opacity', 0, 1).onChange(controls.redraw);
gui.addColor(controls, 'color').onChange(controls.redraw);
controls.redraw();
render();

function createSprite(size, transparent, opacity, color, spriteNumber) {
var spriteMaterial = new THREE.SpriteMaterial({
opacity: opacity,
color: color,
transparent: transparent,
map: getTexture()
}
);

// 通过map的offset/repeat选择精灵贴图里的精灵图片,范围0~1
spriteMaterial.map.offset = new THREE.Vector2(0.2 * spriteNumber, 0);
spriteMaterial.map.repeat = new THREE.Vector2(1 / 5, 1);

var sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(size, size, size);
sprite.position.set(100, 50, -10);
sprite.velocityX = 5; // 精灵x轴移动速度因子
sceneOrtho.add(sprite); // 小人精灵添加到正交场景中
}


var step = 0;
function render() {
stats.update();

// 透视相机y轴运动
camera.position.y = Math.sin(step += 0.01) * 20;

// 正交相机里的精灵x轴左右移动
sceneOrtho.children.forEach(function (e) {
if (e instanceof THREE.Sprite) {
// move the sprite along the bottom
e.position.x = e.position.x + e.velocityX;
if (e.position.x > window.innerWidth) {
e.velocityX = -5;
e.material.map.offset.set(1 / 5 * (controls.sprite % 4), 0);
}
if (e.position.x < 0) {
e.velocityX = 5;
}
}
});


requestAnimationFrame(render);

webGLRenderer.render(scene, camera);
webGLRenderer.autoClear = false; // 关闭自动清理,不然在sceneOrtho render时会清理scene的,那么就会导致看不到scene里的球体了
webGLRenderer.render(sceneOrtho, cameraOrtho);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


从高级几何体创建THREE.PointCloud

前面我们知道了THREE.PointCloud是基于几何体的顶点来渲染每个粒子的。前面我们都是使用的自定义顶点,其实也可以直接用高级几何体创建点云。

示例:使用上节的环状扭结几何体创建粒子

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<!-- chapter-07-06.html -->
<!DOCTYPE html>
<html>
<head>
<title>3D Torusknot</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 50;
camera.lookAt(new THREE.Vector3(10, 0, 0));
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var knot;
var controls = new function () {
this.radius = 13;
this.tube = 1.7;
this.radialSegments = 156;
this.tubularSegments = 12;
this.p = 5;
this.q = 4;
this.heightScale = 3.5;
this.asParticles = false;
this.rotate = false;

this.redraw = function () {
if (knot) scene.remove(knot);
// 创建环状扭结几何体
var geom = new THREE.TorusKnotGeometry(controls.radius, controls.tube, Math.round(controls.radialSegments), Math.round(controls.tubularSegments), Math.round(controls.p), Math.round(controls.q), controls.heightScale);

if (controls.asParticles) {
// 创建点云对象
knot = createPointCloud(geom);
} else {
// 创建网格对象
knot = createMesh(geom);
}

scene.add(knot);
};
};

var gui = new dat.GUI();
gui.add(controls, 'radius', 0, 40).onChange(controls.redraw);
gui.add(controls, 'tube', 0, 40).onChange(controls.redraw);
gui.add(controls, 'radialSegments', 0, 400).step(1).onChange(controls.redraw);
gui.add(controls, 'tubularSegments', 1, 20).step(1).onChange(controls.redraw);
gui.add(controls, 'p', 1, 10).step(1).onChange(controls.redraw);
gui.add(controls, 'q', 1, 15).step(1).onChange(controls.redraw);
gui.add(controls, 'heightScale', 0, 5).onChange(controls.redraw);
gui.add(controls, 'asParticles').onChange(controls.redraw);
gui.add(controls, 'rotate').onChange(controls.redraw);
controls.redraw();
render();

// 获取当前画布的纹理信息
function generateSprite() {
var canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
var context = canvas.getContext('2d');
var gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
gradient.addColorStop(0.4, 'rgba(0,0,64,1)');
gradient.addColorStop(1, 'rgba(0,0,0,1)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;
}

function createPointCloud(geom) {
var material = new THREE.PointCloudMaterial({
color: 0xffffff,
size: 3,
transparent: true,
blending: THREE.AdditiveBlending,
map: generateSprite() // 使用HTML5画布的纹理
});

var cloud = new THREE.PointCloud(geom, material);
cloud.sortParticles = true;
return cloud;
}

function createMesh(geom) {
var meshMaterial = new THREE.MeshNormalMaterial({});
meshMaterial.side = THREE.DoubleSide;
var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [meshMaterial]);
return mesh;
}

function render() {
stats.update();
if (controls.rotate) {
knot.rotation.y += 0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>