Skip to content

Commit fc91ba7

Browse files
committed
update 26 transparent bug
1 parent cbaf8f6 commit fc91ba7

File tree

1 file changed

+278
-1
lines changed

1 file changed

+278
-1
lines changed

26 Three.js解决方案之透明度bug.md

Lines changed: 278 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,290 @@ colors.forEach((color, index) => {
222222

223223
> 创建平面 在 Three.js 中使用的是 Three.PlaneBufferGeometry
224224
225-
2. 给每个平面添加一个纹理贴图,并设置正方形平面透明度为 0.5
225+
2. 给每个平面添加一个颜色和纹理贴图,两面都渲染,并设置正方形平面透明度为 0.5
226+
227+
> 贴图我们直接使用网上的 2 张图片资源
228+
>
229+
> 图片都是由背景色的,并非背景透明的 PNG
226230
227231
3. 让这 2 个平面形成 十字交叉 的状态
228232

233+
> 也就是说让其中一个平面的 y 轴旋转 180度
234+
235+
236+
237+
<br>
238+
239+
**实现的代码:**
240+
241+
```
242+
const planeDataArr = [
243+
{
244+
color: 'red',
245+
ratation: 0,
246+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/happyface.png'
247+
},
248+
{
249+
color: 'yellow',
250+
ratation: Math.PI * 0.5,
251+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/hmmmface.png'
252+
}
253+
]
254+
255+
planeDataArr.forEach((value) => {
256+
const geometry = new Three.PlaneBufferGeometry(2, 2)
257+
const textureLoader = new TextureLoader()
258+
const material = new Three.MeshBasicMaterial({
259+
color: value.color,
260+
map: textureLoader.load(value.imgsrc),
261+
opacity: 0.5,
262+
transparent: true,
263+
side:Three.DoubleSide
264+
})
265+
const plane = new Three.Mesh(geometry, material)
266+
plane.rotation.y = value.ratation
267+
scene.add(plane)
268+
})
269+
```
270+
271+
272+
273+
<br>
274+
275+
这次,我们将很容易看到,当红色平面一侧完全覆盖住黄色平面一侧时,会完全看不到黄色平面那一侧。
276+
277+
> 实际运行效果我就不贴图了,你可以将上面代码实际运行一下。
278+
279+
> 你就假装此刻你看到了。
280+
281+
282+
283+
<br>
284+
285+
用我们上面讲过的理论可解释这个现象:即 红色平面一侧颜色深度大于黄色平面一侧,当完全覆盖住之后黄色平面那一侧就不会再进行渲染,所以我们就看不到了。
286+
287+
288+
289+
<br>
290+
291+
**解决方案:将上面 2 个平面拆分成 4 个平面,这样可以确保每个平面都会被渲染。**
292+
293+
由于我们这个场景 2 个平面十字交叉,所以我们就直接创建 4 个小的平面,然后将这 4 个小平面组合成 “2 个十字相交的平面”。
294+
295+
具体实现的方式是:
296+
297+
1. 将原本 较大的 1 个平面拆分成 2 个小平面
298+
299+
2. 设置这 2 个小平面的纹理贴图偏移,各自占一半
300+
301+
> 这样可以最终让 2 个小平面贴合成 1 个完整的平面(纹理)
302+
303+
3. 为了方便我们计算旋转,好让他们形成十字交叉,所以我们可以将 每组小平面放置在同一个空间中
304+
305+
> 这需要使用到 Three.Object3D
306+
307+
308+
309+
<br>
310+
311+
**实际代码:**
312+
313+
```
314+
const planeDataArr = [
315+
{
316+
color: 'red',
317+
ratation: 0,
318+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/happyface.png'
319+
},
320+
{
321+
color: 'yellow',
322+
ratation: Math.PI * 0.5,
323+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/hmmmface.png'
324+
}
325+
]
326+
327+
planeDataArr.forEach((value) => {
328+
const base = new Three.Object3D()
329+
base.rotation.y = value.ratation
330+
scene.add(base)
331+
332+
const plane_size = 2
333+
const half_size = plane_size / 2
334+
const geometry = new Three.PlaneBufferGeometry(half_size, plane_size)
335+
const arr = [-1, 1]
336+
arr.forEach((x) => {
337+
const textureLoader = new TextureLoader()
338+
const texture = textureLoader.load(value.imgsrc)
339+
texture.offset.x = x < 1 ? 0 : 0.5
340+
texture.repeat.x = 0.5
341+
const material = new Three.MeshBasicMaterial({
342+
color: value.color,
343+
map: texture,
344+
transparent: true,
345+
opacity: 0.5,
346+
side: Three.DoubleSide
347+
})
348+
const plane = new Three.Mesh(geometry, material)
349+
plane.position.x = x * half_size / 2
350+
base.add(plane)
351+
});
352+
})
353+
```
354+
355+
>假设我们目标平面宽高均为 2,那么:
356+
>
357+
>1. 我们将该目标平面拆分成 2 个 宽 1、高 2 的平面
358+
>2. 获取并设置纹理贴图,并分别设置纹理的 offset.x 、repeat.x 各占 一半,也就是 0.5
359+
>3. 我们知道拆分出的 2 个小平面他们 x 轴相差 1 个小平面的宽度,由于我们设置的内部循环数组为 [-1,1],所以 2 个小平面的 x 值应该是 正负宽度一半的一半。
360+
361+
362+
363+
<br>
364+
365+
这一次,我们再运行就会发现,无论任何视角下,红色平面不再会这该黄色平面了。
366+
367+
368+
369+
<br>
370+
371+
**这是第 2 种解决透明 "bug" 的方案:将对象进行拆分**
372+
373+
**但是请注意,该解决方式只适合那些简单,且位置相对固定的 3D 对象。**
374+
375+
> 若物体本身就比较复杂,面比较多,还要再拆分,那么就太消耗渲染性能
376+
>
377+
> 并且位置必须相对固定,若不固定会增加我们拼接的难度
378+
379+
380+
381+
<br>
229382

383+
接下来讲解第 3 种解决方案。
230384

385+
### 启用alphaTest来避免遮挡问题
231386

387+
首先我们回顾一下上面的例子,当时示例中 2 个平面的纹理贴图背景是不透明的。
388+
389+
那我们可以尝试另外 2 个图片贴图,他们是背景透明的 PNG 图片。
390+
391+
我们要使用材质(Three.Material) 的一个属性 .alphaTest。
392+
393+
394+
395+
<br>
396+
397+
**.alphaTest属性介绍**
398+
399+
.alphaTest 是一个透明度检测值,值得类型是 Number,取值范围为 0 - 1。
400+
401+
若透明度低于该值,则不会进行渲染。
402+
403+
> 反之,只有某个点透明度高于该值的才会进行渲染
404+
405+
.alphaTest 默认值为 0。
406+
407+
> 也就是说默认情况下即使透明度为 0 也会进行渲染
408+
>
409+
> 假设我们给材质设置有 color,那么肯定就会渲染出内容
410+
411+
412+
413+
<br>
414+
415+
我们在最初 2 个平面的代码基础上进行修改。
416+
417+
1. 修改纹理贴图资源,这次使用背景透明的 PNG 图片
418+
2. 材质不再设置 .opacity 属性,改设置 .alphaTest 属性
419+
420+
<br>
421+
422+
修改后的代码如下:
423+
424+
```
425+
const planeDataArr = [
426+
{
427+
color: 'red',
428+
ratation: 0,
429+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/tree-01.png'
430+
},
431+
{
432+
color: 'yellow',
433+
ratation: Math.PI * 0.5,
434+
imgsrc: 'https://threejsfundamentals.org/threejs/resources/images/tree-02.png'
435+
}
436+
]
437+
438+
planeDataArr.forEach((value) => {
439+
const geometry = new Three.PlaneBufferGeometry(2, 2)
440+
const textureLoader = new TextureLoader()
441+
const material = new Three.MeshBasicMaterial({
442+
color: value.color,
443+
map: textureLoader.load(value.imgsrc),
444+
alphaTest: 0.5,
445+
transparent: true,
446+
side:Three.DoubleSide
447+
})
448+
const plane = new Three.Mesh(geometry, material)
449+
plane.rotation.y = value.ratation
450+
scene.add(plane)
451+
})
452+
```
453+
454+
> 请注意,上面代码中我们取了一个透明度的中间值,将 .alphaTest 属性值设置为 0.5。
455+
>
456+
> 你可以尝试将 .alphaTest 分别设置为 0.2 或 0.8 看看结果会有什么变化。
457+
>
458+
> > 最终边缘清晰度取决于贴图图片中抠图的精细程度。若边缘越不清晰(也就是越模糊),最终呈现出的白边会越严重。
459+
460+
461+
462+
<br>
463+
464+
实际运行后就会发现,2 棵不同颜色的树木,彼此十字交叉,可以透过前面树的枝叶看到另外一颗树。
465+
466+
467+
468+
### 本文小节
469+
470+
我们讲解了为什么会在某些视角下,某些半透明的物体个别地方三角面不会被渲染的原因。
471+
472+
通过几个示例,讲解了 3 种解决方案:
473+
474+
1. 将物体添加 2 份,1份负责渲染前面,另外一份负责渲染后面
475+
476+
> 缺点:增加渲染工作量
477+
478+
2. 将物体(或平面)进行拆分,已确保每 1 份均会有机会被渲染
479+
480+
> 缺点:只适合简单的物体,且位置固定容易拼凑
481+
482+
3. 通过设置 .alphaTest,以实现透明渲染
483+
484+
> 缺点:若贴图抠图不够精细,容易出现白边
485+
486+
487+
488+
<br>
489+
490+
就像上面我们提到的,每一种解决方案都有各自的使用场景和缺点。
491+
492+
我们今后在实际的项目中,一定要根据实际情况来作出选择,看使用哪种方案。
493+
494+
> 说白了,无非就是在性能、复杂度、精细化方面进行取舍,最终找出合适的方案。
495+
496+
497+
498+
<br>
499+
500+
> 你可能会注意到本章节我并没有贴出完整的示例代码,而仅仅贴出了核心的代码。
501+
>
502+
> 我是这样认为的,如果到了今天你依然无法自己写出完整的代码,还需要靠复制我完整的示例代码,那你干脆别学 Three.js了,放弃吧。
503+
504+
505+
506+
<br>
232507

508+
至此,本章结束。
233509

510+
目前我们所有的示例都是基于 1 个 画布(canvas) 和 1 个 镜头(camera),下一节我们讲解同一个网页中渲染多个 画布 和多个镜头。
234511

0 commit comments

Comments
 (0)