Skip to content

Commit cbaf8f6

Browse files
committed
add 26 transparency bug
1 parent e7fc756 commit cbaf8f6

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# 26 Three.js解决方案之透明度bug
2+
3+
透明度(transparency) 在 Three.js 中很容易实现,但是透明度又存在一个 "Bug",解决起来又比较难。
4+
5+
> 请注意,这里说的 “Bug” 是加了引号的,具体原因我们稍后讲解。
6+
7+
8+
9+
<br>
10+
11+
我们先从一个简单的示例开始。
12+
13+
### 示例1:渲染一个半透明的立方体
14+
15+
所有材质的基类 Three.Material 有 2 个属性和设置透明度有关:
16+
17+
1. transparent:设置材质是否透明
18+
2. opacity:设置材质透明度,取值范围 0 - 1
19+
20+
> 由于 Material 是所有材质的基类,也就意味着所有材质都拥有上述 2 个属性
21+
22+
23+
24+
<br>
25+
26+
假设我们想创建一个半透明的立方体,代码如下:
27+
28+
```
29+
const geometry = new Three.BoxBufferGeometry(2, 2, 2)
30+
const material = new Three.MeshBasicMaterial({
31+
color: 'red',
32+
transparent: true,
33+
opacity: 0.5
34+
})
35+
const cube = new Three.Mesh(geometry, material)
36+
scene.add(cube)
37+
```
38+
39+
40+
41+
<br>
42+
43+
上面我们只是给材质设置了一个 红色,当我们运行程序的时候会发现:单独一个半透明立方体,似乎看不出任何半透明的意思。
44+
45+
> 即使给材质添加 `side: Three.DoubleSide`
46+
47+
我们需要添加多个半透明立方体,才更容易看出来彼此半透明。
48+
49+
50+
51+
我们继续修改示例。
52+
53+
<br>
54+
55+
### 渲染8个小立方体
56+
57+
我们的渲染目标:
58+
59+
1. 渲染8个不同颜色,透明度都为 0.5 的立方体
60+
61+
2. 这 8 个立方体分布在一个 2 x 2 x 2 的空间中
62+
63+
> 这种分布方式,在魔方玩具中被称为 “二阶魔方”
64+
65+
3. 每个立方体 里外 2 个面都要进行渲染
66+
67+
68+
69+
<br>
70+
71+
具体实现代码:
72+
73+
```
74+
const colors = ['red', 'blue', 'darkorange', 'darkviolet', 'green', 'tomato', 'sienna', 'crimson']
75+
const cube_size = 1 //立方体尺寸
76+
const cube_margin = 0.6 //立方体间距空隙
77+
colors.forEach((color, index) => {
78+
const geometry = new Three.BoxBufferGeometry(cube_size, cube_size, cube_size)
79+
const material = new Three.MeshPhongMaterial({
80+
color,
81+
transparent: true,
82+
opacity: 0.5,
83+
side: Three.DoubleSide
84+
})
85+
86+
const cube = new Three.Mesh(geometry, material)
87+
cube.position.x = (index % 2 ? 1 : -1) * cube_size * cube_margin
88+
cube.position.y = (Math.floor(index / 4) ? -1 : 1) * cube_size * cube_margin
89+
cube.position.z = ((index % 4) >= 2) ? 1 : -1 * cube_size * cube_margin / 2
90+
91+
scene.add(cube)
92+
})
93+
```
94+
95+
> 在目前最新的 Three.js r127 版本中,对于预置颜色的单词,只支持全小写,例如:
96+
>
97+
> 1. 红色 只可以写 `red`,不可以写成 `Red`
98+
> 2. 再或者 暗桔色 只可以写 `darkorange`,不可以写成`DarkOrange`
99+
>
100+
> 这是因为在 Color 源码中,记录内置颜色值的对象 key 都是小写,我已针对这个问题提交了自己的 pr:
101+
>
102+
> https://github.com/mrdoob/three.js/pull/21687
103+
>
104+
> 我修改了一点代码,让取值时对颜色值的字符串执行 .toLowerCase(),这样即使颜色值字符串有大写可以最终实际被转化为小写。
105+
106+
107+
108+
<br>
109+
110+
实际运行后,就会看到 8 个半透明的小立方体。通过 OrbitControls 旋转视角,看着感觉挺好的呀。
111+
112+
113+
114+
<br>
115+
116+
<!-- 如果我不告诉你,可能你一直也发现不了这里面存在着一个 “bug” -->
117+
118+
当你尝试不断变换视角查看立方体时,在某些特殊的视角下,我们看不到立方体左侧后表面。
119+
120+
> 也就是说,原本立方体左侧后表面应该也被渲染,但是实际上并未被渲染。
121+
122+
123+
124+
<br>
125+
126+
如果你实在是没有看出来什么问题,暂时相信我一下,就好像你已真的发现了那样。
127+
128+
下面听一下关于出现这个 "bug" 的解释。
129+
130+
131+
132+
<br>
133+
134+
### Three.js绘制 3D 对象的方式
135+
136+
上面提到的渲染 “Bug”,是由于 Three.js 绘制 3D 对象的方式造成的。
137+
138+
<br>
139+
140+
对于每一个几何图形,每个三角形一次只绘制一个。
141+
142+
> 立方体的 1 个面是一个 正方形,而这个正方形是由 2 个三角形构成的。
143+
>
144+
> 在绘制 1 个面(正方形),Three.js 会先后绘制 2 个三角形,最终拼接成 1 个正方形。
145+
146+
147+
148+
<br>
149+
150+
每次绘制一个三角形时,会记录 2 个事情:
151+
152+
1. 三角形的颜色
153+
154+
2. 三角形的像素深度
155+
156+
> 像素深度是指存储每个像素所用的位数,用来度量图像的分辨率。
157+
158+
3. 当绘制下一个三角形时,对于每一个像素,如果深度比之前记录的深度还要深,则不会绘制任何像素
159+
160+
> 这其实是 Three.js 绘制物体时采取的一种节省性能的策略
161+
162+
163+
164+
<br>
165+
166+
这套策略对于不透明的物体来说非常有用,但是对于透明的物体却不起作用。
167+
168+
169+
170+
<br>
171+
172+
一个立方体有 6 个面,每个面 2 个三角形,也就意味着一个立方体需要绘制 12 个三角形。
173+
174+
而这 12 个三角形究竟先绘制哪个,他们绘制的顺序是什么呢?
175+
176+
答:绘制顺序取决于我们的视角,越接近相机的三角形越优先被绘制。
177+
178+
> 这就是为什么我们上面提到的绘制 bug 只有在某些特定角度下才会出现的原因。
179+
180+
> 越靠近相机的三角形越先被绘制,这也意味着在某些角度下,远离摄像机的某个面(立方体背面或侧背面)有可能不会被绘制。
181+
182+
183+
184+
这种情况不仅会出现在立方体身上,球体上也会出现。
185+
186+
187+
188+
<br>
189+
190+
**针对以上情况,有一种解决方案:**
191+
192+
1. 将每个立方体添加 2 次到场景中
193+
2. 第 1 次添加的立方体设置只让渲染 背面(Three.BackSide)
194+
3. 第 2 次添加的立方体设置只让渲染 前面(Three.FrontSide)
195+
196+
这样一番操作过后,确保 Three.js 可以将每个立方体的前面、后面都会渲染,拼合一下 2 次的渲染结果,将 “正确的” 结果渲染出来。
197+
198+
199+
200+
<br>
201+
202+
**补充说明**
203+
204+
1. 上面的解决方案实际上需要绘制 2 次,这也许会造成性能上的浪费。
205+
2. 如果你并不是特别在乎那一点点渲染 “Bug”,你完全可以忽视它。
206+
207+
208+
209+
<br>
210+
211+
接下来我们再通过另外一个示例,讲解另外一种有针对性的解决方案。
212+
213+
214+
215+
<br>
216+
217+
### 绘制2个中心交叉的平面正方形
218+
219+
我们的示例目标是:
220+
221+
1. 绘制 2 个平面的正方形
222+
223+
> 创建平面 在 Three.js 中使用的是 Three.PlaneBufferGeometry
224+
225+
2. 给每个平面添加一个纹理贴图,并设置正方形平面透明度为 0.5
226+
227+
3. 让这 2 个平面形成 十字交叉 的状态
228+
229+
230+
231+
232+
233+
234+

0 commit comments

Comments
 (0)