1- <!DOCTYPE html> < html lang ="zh "> < head >
2- < meta charset ="utf-8 ">
3- < title > Billboards</ title >
4- < meta name ="viewport " content ="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0 ">
5- < meta name ="twitter:card " content ="summary_large_image ">
6- < meta name ="twitter:site " content ="@threejs ">
7- < meta name ="twitter:title " content ="Three.js – Billboards ">
8- < meta property ="og:image " content ="https://threejs.org/files/share.png ">
9- < link rel ="shortcut icon " href ="/files/favicon_white.ico " media ="(prefers-color-scheme: dark) ">
10- < link rel ="shortcut icon " href ="/files/favicon.ico " media ="(prefers-color-scheme: light) ">
11-
12- < link rel ="stylesheet " href ="/manual/resources/lesson.css ">
13- < link rel ="stylesheet " href ="/manual/resources/lang.css ">
14- <!-- Import maps polyfill -->
15- <!-- Remove this when import maps will be widely supported -->
16- < script async src ="
https://unpkg.com/[email protected] /dist/es-module-shims.js "
> </ script > 17-
18- < script type ="importmap ">
1+ <!DOCTYPE html>
2+ < html lang ="zh ">
3+
4+ < head >
5+ < meta charset ="utf-8 ">
6+ < title > 广告牌(Billboards)</ title >
7+ < meta name ="viewport " content ="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0 ">
8+ < meta name ="twitter:card " content ="summary_large_image ">
9+ < meta name ="twitter:site " content ="@threejs ">
10+ < meta name ="twitter:title " content ="Three.js – Billboards ">
11+ < meta property ="og:image " content ="https://threejs.org/files/share.png ">
12+ < link rel ="shortcut icon " href ="/files/favicon_white.ico " media ="(prefers-color-scheme: dark) ">
13+ < link rel ="shortcut icon " href ="/files/favicon.ico " media ="(prefers-color-scheme: light) ">
14+
15+ < link rel ="stylesheet " href ="/manual/resources/lesson.css ">
16+ < link rel ="stylesheet " href ="/manual/resources/lang.css ">
17+ <!-- Import maps polyfill -->
18+ <!-- Remove this when import maps will be widely supported -->
19+ < script async src ="
https://unpkg.com/[email protected] /dist/es-module-shims.js "
> </ script > 20+
21+ < script type ="importmap ">
1922{
2023 "imports" : {
2124 "three" : "../../build/three.module.js"
2225 }
2326}
2427</ script >
25- < link rel ="stylesheet " href ="/manual/zh/lang.css ">
26- </ head >
27- < body >
28- < div class ="container ">
29- < div class ="lesson-title ">
30- < h1 > Billboards</ h1 >
31- </ div >
32- < div class ="lesson ">
33- < div class ="lesson-main ">
34- < p > 抱歉,还没有中文翻译哦。 < a href ="https://github.com/mrdoob/three.js "> 欢迎加入翻译</ a > ! 😄</ p >
35- < p > < a href ="/manual/en/billboards.html "> 英文原文链接</ a > .</ p >
28+ < link rel ="stylesheet " href ="/manual/zh/lang.css ">
29+ </ head >
30+
31+ < body >
32+ < div class ="container ">
33+ < div class ="lesson-title ">
34+ < h1 > 广告牌(Billboards)</ h1 >
35+ </ div >
36+ < div class ="lesson ">
37+ < div class ="lesson-main ">
38+ < p > 在 < a href ="canvas-textures.html "> 上一篇文章</ a > 我们使用了一个 < a href ="/docs/#api/en/textures/CanvasTexture "> < code
39+ class ="notranslate " translate ="no "> CanvasTexture</ code > </ a >
40+ 在人物上创作标签(Labels)和徽标(Badges)。有时我们想制作一些总是面对相机的东西。Three.js提供了 < a href ="/docs/#api/en/objects/Sprite "> < code
41+ class ="notranslate " translate ="no "> Sprite</ code > </ a > 和
42+ < a href ="/docs/#api/en/materials/SpriteMaterial "> < code class ="notranslate "
43+ translate ="no "> SpriteMaterial</ code > </ a > 来实现这个功能。
44+ </ p >
45+
46+
47+ < p > 我们修改这个徽标的例子 < a href ="canvas-textures.html "> 使用Canvas作为纹理</ a > ,
48+ 应用 < a href ="/docs/#api/en/objects/Sprite "> < code class ="notranslate " translate ="no "> Sprite</ code > </ a > and
49+ < a href ="/docs/#api/en/materials/SpriteMaterial "> < code class ="notranslate "
50+ translate ="no "> SpriteMaterial</ code > </ a >
51+ </ p >
52+
53+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no "> function makePerson(x, labelWidth, size, name, color) {
54+ const canvas = makeLabelCanvas(labelWidth, size, name);
55+ const texture = new THREE.CanvasTexture(canvas);
56+ // 因为我们的Canvas的尺寸可能不是2的N次方
57+ // 在两个维度上适当地设置filter属性
58+ texture.minFilter = THREE.LinearFilter;
59+ texture.wrapS = THREE.ClampToEdgeWrapping;
60+ texture.wrapT = THREE.ClampToEdgeWrapping;
61+
62+ - const labelMaterial = new THREE.MeshBasicMaterial({
63+ + const labelMaterial = new THREE.SpriteMaterial({
64+ map: texture,
65+ - side: THREE.DoubleSide,
66+ transparent: true,
67+ });
68+
69+ const root = new THREE.Object3D();
70+ root.position.x = x;
3671
72+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
73+ root.add(body);
74+ body.position.y = bodyHeight / 2;
75+
76+ const head = new THREE.Mesh(headGeometry, bodyMaterial);
77+ root.add(head);
78+ head.position.y = bodyHeight + headRadius * 1.1;
79+
80+ - const label = new THREE.Mesh(labelGeometry, labelMaterial);
81+ + const label = new THREE.Sprite(labelMaterial);
82+ root.add(label);
83+ label.position.y = bodyHeight * 4 / 5;
84+ label.position.z = bodyRadiusTop * 1.01;</ pre >
85+ < p > 现在标签始终是面向相机了。</ p >
86+ < p > </ p >
87+ < div translate ="no " class ="threejs_example_container notranslate ">
88+ < div > < iframe class ="threejs_example notranslate " translate ="no " style =" "
89+ src ="/manual/examples/resources/editor.html?url=/manual/examples/billboard-labels-w-sprites.html "> </ iframe >
90+ </ div >
91+ < a class ="threejs_center " href ="/manual/examples/billboard-labels-w-sprites.html " target ="_blank "> 点击在新窗口打开</ a >
92+ </ div >
93+ < p > </ p >
94+ < p > 一个问题是,从某些角度来看的话,标签与人物重合了。 </ p >
95+ < div class ="threejs_center "> < img src ="../resources/images/billboard-label-z-issue.png " style ="width: 455px; ">
3796 </ div >
97+ < p > 我们可以通过移动标签的位置来解决此问题。</ p >
98+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
99+ +// 如果单位是米,这里就用0.01
100+ +// 也就是以厘米作为标签的单位
101+ +const labelBaseScale = 0.01;
102+ const label = new THREE.Sprite(labelMaterial);
103+ root.add(label);
104+ -label.position.y = bodyHeight * 4 / 5;
105+ -label.position.z = bodyRadiusTop * 1.01;
106+ +label.position.y = head.position.y + headRadius + size * labelBaseScale;
107+
108+ -// 如果单位是米,这里就用0.01
109+ -// 也就是以厘米作为标签的单位
110+ -const labelBaseScale = 0.01;
111+ label.scale.x = canvas.width * labelBaseScale;
112+ label.scale.y = canvas.height * labelBaseScale;</ pre >
113+ < p > </ p >
114+ < div translate ="no " class ="threejs_example_container notranslate ">
115+ < div > < iframe class ="threejs_example notranslate " translate ="no " style =" "
116+ src ="/manual/examples/resources/editor.html?url=/manual/examples/billboard-labels-w-sprites-adjust-height.html "> </ iframe >
117+ </ div >
118+ < a class ="threejs_center " href ="/manual/examples/billboard-labels-w-sprites-adjust-height.html "
119+ target ="_blank "> 点击在新窗口打开</ a >
120+ </ div >
121+ < p > </ p >
122+ < p > 我们可以用Billboard做的另一件事是绘制立面(Facades)。</ p >
123+ < p > 我们不绘制 3D 对象,而是使用图片绘制 2D 平面化的 3D 对象,这通常比绘制 3D 对象要快。</ p >
124+ < p > 例如,我们用树木网络制作一个场景,我们让每一棵树的底部是圆柱体,顶部是圆锥体。</ p >
125+ < p > 第一步,我们创建圆锥体和圆柱体的Geometry和Material,所有的树都会复用这些。</ p >
126+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
127+ const trunkRadius = .2;
128+ const trunkHeight = 1;
129+ const trunkRadialSegments = 12;
130+ const trunkGeometry = new THREE.CylinderGeometry(
131+ trunkRadius, trunkRadius, trunkHeight, trunkRadialSegments);
132+
133+ const topRadius = trunkRadius * 4;
134+ const topHeight = trunkHeight * 2;
135+ const topSegments = 12;
136+ const topGeometry = new THREE.ConeGeometry(
137+ topRadius, topHeight, topSegments);
138+
139+ const trunkMaterial = new THREE.MeshPhongMaterial({color: 'brown'});
140+ const topMaterial = new THREE.MeshPhongMaterial({color: 'green'});</ pre >
141+ < p > 然后我们创建一个函数,对每一棵树的树干和树顶创建一个 < a href ="/docs/#api/en/objects/Mesh "> < code class ="notranslate "
142+ translate ="no "> Mesh</ code > </ a >
143+ ,并把它们都加入到一个 < a href ="/docs/#api/en/core/Object3D "> < code class ="notranslate "
144+ translate ="no "> Object3D</ code > </ a > 对象下。</ p >
145+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
146+ function makeTree(x, z) {
147+ const root = new THREE.Object3D();
148+ const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
149+ trunk.position.y = trunkHeight / 2;
150+ root.add(trunk);
151+
152+ const top = new THREE.Mesh(topGeometry, topMaterial);
153+ top.position.y = trunkHeight + topHeight / 2;
154+ root.add(top);
155+
156+ root.position.set(x, 0, z);
157+ scene.add(root);
158+
159+ return root;
160+ }</ pre >
161+ < p > 然后我们会创建一个循环,生成树网络。</ p >
162+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
163+ for (let z = -50; z <= 50; z += 10) {
164+ for (let x = -50; x <= 50; x += 10) {
165+ makeTree(x, z);
166+ }
167+ }</ pre >
168+ < p > 让我们再增加一个地平面。</ p >
169+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
170+ // 添加地面
171+ {
172+ const size = 400;
173+ const geometry = new THREE.PlaneGeometry(size, size);
174+ const material = new THREE.MeshPhongMaterial({color: 'gray'});
175+ const mesh = new THREE.Mesh(geometry, material);
176+ mesh.rotation.x = Math.PI * -0.5;
177+ scene.add(mesh);
178+ }</ pre >
179+ < p > 然后把背景调整为浅蓝(lightblue)</ p >
180+
181+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
182+ const scene = new THREE.Scene();
183+ -scene.background = new THREE.Color('white');
184+ +scene.background = new THREE.Color('lightblue');</ pre >
185+ < p > 我们得到了一个树木网络</ p >
186+ < p > </ p >
187+ < div translate ="no " class ="threejs_example_container notranslate ">
188+ < div > < iframe class ="threejs_example notranslate " translate ="no " style =" "
189+ src ="/manual/examples/resources/editor.html?url=/manual/examples/billboard-trees-no-billboards.html "> </ iframe >
190+ </ div >
191+ < a class ="threejs_center " href ="/manual/examples/billboard-trees-no-billboards.html "
192+ target ="_blank "> 点击在新窗口打开</ a >
193+ </ div >
194+ < p > </ p >
195+ < p > 这里有11x11或者121棵树,每棵树由12个多边形组成的锥体 +
196+ 48个多边形组成的树干组成,所以每棵树包含60个多边形。121*60的结果是7260,这并不是很多,当然更精细的3D树可能有1000-3000个多边形构成。如果3000个多边形构成的树,那么121棵树将包含363000个多边形。
197+ </ p >
198+ < p > 使用Facades,可以降低这个数字。</ p >
199+ < p > 我们可以在一些绘图应用中手动创建一个Facade,现在让我们编写一些代码来手动生成一个。</ p >
200+ < p > 现在写一些代码把对象绘制到纹理中,使用一个 < code class ="notranslate " translate ="no "> RenderTarget</ code > ,我们提到过使用
201+ < code class ="notranslate " translate ="no "> RenderTarget</ code >
202+ 来渲染,具体在这篇 < a href ="rendertargets.html "> 渲染目标</ a > 文章里。
203+ </ p >
204+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
205+ function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
206+ const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
207+ const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
208+ const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
209+
210+ camera.position.copy(boxCenter);
211+ camera.position.z += distance;
212+
213+ // 为视锥体选择合适的near和far值
214+ // 可以把盒模型包裹进来
215+ camera.near = boxSize / 100;
216+ camera.far = boxSize * 100;
217+
218+ camera.updateProjectionMatrix();
219+ }
220+
221+ function makeSpriteTexture(textureSize, obj) {
222+ const rt = new THREE.WebGLRenderTarget(textureSize, textureSize);
223+
224+ const aspect = 1; // 因为Render Target是正方形
225+ const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
226+
227+ scene.add(obj);
228+
229+ // 计算对象的盒模型
230+ const box = new THREE.Box3().setFromObject(obj);
231+
232+ const boxSize = box.getSize(new THREE.Vector3());
233+ const boxCenter = box.getCenter(new THREE.Vector3());
234+
235+ // 设置相机去构建盒模型
236+ const fudge = 1.1;
237+ const size = Math.max(...boxSize.toArray()) * fudge;
238+ frameArea(size, size, boxCenter, camera);
239+
240+ renderer.autoClear = false;
241+ renderer.setRenderTarget(rt);
242+ renderer.render(scene, camera);
243+ renderer.setRenderTarget(null);
244+ renderer.autoClear = true;
245+
246+ scene.remove(obj);
247+
248+ return {
249+ position: boxCenter.multiplyScalar(fudge),
250+ scale: size,
251+ texture: rt.texture,
252+ };
253+ }</ pre >
254+ < p > 关于上面代码的一些注意事项:</ p >
255+ < p > 我们使用之前代码定义好的视图的 (< code class ="notranslate " translate ="no "> fov</ code > ) 属性,
256+ </ p >
257+ < p > 我们计算一个包含树的盒模型,这和 < a href ="load-obj.html "> 加载.obj的文件</ a > 中提到的方式一致,有一点微小的改变。</ p >
258+ < p > 我们再次调用 < code class ="notranslate " translate ="no "> frameArea</ code > ,稍微改写了一下< a
259+ href ="load-obj.html "> 加载.obj的文件</ a > 。
260+ 在这种情况下,我们计算相机需要离物体多远,从而让它的视野以包含对象。然后我们将相机的-z值设置为从对象盒模型的中心到此的距离。</ p >
261+ < p > 我们将想要适应的大小乘以1.1倍(< code class ="notranslate " translate ="no "> fudge</ code > )
262+ 来确保树完全在渲染目标中。这是因为我们用来计算对象是否适合相机的视口的尺寸,没有考虑到对象的边缘可能会超出我们的可视区域之外。我们可以计算出如何让盒子100%合适,但这会浪费很多空间,所以我们就是蒙混
263+ < em > (fudge)</ em > 一下。
264+ </ p >
265+ < p > 然后我们渲染到RenderTarget中,然后从场景中移除此对象。 </ p >
266+ < p > 重点是需要场景中的灯光,但我们需要确保场景中没有其他东西。</ p >
267+ < p > 我们也不能给场景设置背景色。</ p >
268+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
269+ const scene = new THREE.Scene();
270+ -scene.background = new THREE.Color('lightblue');</ pre >
271+ < p > 最后我们返回了纹理,位置和缩放比例,我们需要创建Facade,让它看起来在同一个地方。</ p >
272+ < p > 然后我们制作一棵树,调用此代码:</ p >
273+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
274+ // 创建Billboard纹理
275+ const tree = makeTree(0, 0);
276+ const facadeSize = 64;
277+ const treeSpriteInfo = makeSpriteTexture(facadeSize, tree);</ pre >
278+ < p > 然后我们可以制作一个Facade网络,而不是树网络。</ p >
279+ < pre class ="prettyprint showlinemods notranslate lang-js " translate ="no ">
280+ +function makeSprite(spriteInfo, x, z) {
281+ + const {texture, offset, scale} = spriteInfo;
282+ + const mat = new THREE.SpriteMaterial({
283+ + map: texture,
284+ + transparent: true,
285+ + });
286+ + const sprite = new THREE.Sprite(mat);
287+ + scene.add(sprite);
288+ + sprite.position.set(
289+ + offset.x + x,
290+ + offset.y,
291+ + offset.z + z);
292+ + sprite.scale.set(scale, scale, scale);
293+ +}
294+
295+ for (let z = -50; z <= 50; z += 10) {
296+ for (let x = -50; x <= 50; x += 10) {
297+ - makeTree(x, z);
298+ + makeSprite(treeSpriteInfo, x, z);
299+ }
300+ }</ pre >
301+ < p > 在上面的代码中,我们应用了定位Facade所需的偏移量和缩放比例,因此他会出现在和原树同一个地方。</ p >
302+ < p > 现在我们已经完成了Facade纹理的制作,我们可以再次设置背景。</ p >
303+ < pre class ="prettyprint showlinemods notranslate lang-js "
304+ translate ="no "> scene.background = new THREE.Color('lightblue');</ pre >
305+ < p > 现在我们得到了一个全是树Facades的场景。</ p >
306+ < p > </ p >
307+ < div translate ="no " class ="threejs_example_container notranslate ">
308+ < div > < iframe class ="threejs_example notranslate " translate ="no " style =" "
309+ src ="/manual/examples/resources/editor.html?url=/manual/examples/billboard-trees-static-billboards.html "> </ iframe >
310+ </ div >
311+ < a class ="threejs_center " href ="/manual/examples/billboard-trees-static-billboards.html "
312+ target ="_blank "> 点击在新窗口打开</ a >
313+ </ div >
314+ < p > </ p >
315+ < p >
316+ 与上面的树模型相比,它们看起来非常相似。我们使用了低分辨率纹理,只有64x64像素,所以Facade是块状的,你当然可以提高分辨率。通常Facade只会用在非常远处的物体,因为当它们非常小的时候,低分辨率纹理就足够了。它节省了绘制远处只有几个像素的精致树模型时间。
317+ </ p >
318+ < p > 另一个问题是我们只能从一侧查看树。这往往是通过渲染更多的Facade来解决,比如绘制对象周围的8个方向,然后根据实际相机的方向来设置要展示的Facade。</ p >
319+ < p > 是否使用Facade由你决定,如果你决定去使用它们,希望这篇文章给了你一些想法和解决方案。</ p >
38320 </ div >
39321 </ div >
40-
322+ </ div >
323+
41324 < script src ="/manual/resources/prettify.js "> </ script >
42325 < script src ="/manual/resources/lesson.js "> </ script >
43326
44327
45328
46329
47- </ body > </ html >
330+ </ body >
331+
332+ </ html >
0 commit comments