本篇讨论UnityGraphicsPrograming第一册第十章内容ProjectionSpray。

▲10.1 首先
大家好,我是すぎのひろのり!很遗憾的是,他这次不在。
临近截稿日的某一天,我问道“すぎ亲,文章写了没?”。
“啊!”,他回复道。看来完全忘掉了的样子。
最近他很忙的样子,机会难得,我想介绍一下他的作品,就简单的为他代笔一下。

▲10.1.1 ProjectionSpray
すぎ很积极的在Github上公开自己的作品,里面我个人觉得很有趣的是这个:
https://github.com/sugi-cho/Unity-ProjectionSpray-v2
向3D模型喷射喷雾,实现上色。

喷雾从喷雾设备中喷出,凃到模型的身体表面。令人感到神奇。

可以实现Stencil一样的过滤。

▲10.2 总结
以后有机会的话,一定要请すぎ详细解说一下。
すぎ在面试的时候说过,感觉和自己很相似的同事,仲田先生的仓库中,也有很多优秀的代码,值得推荐。
https://github.com/nobnak
失礼了。

▲00_viewNormal
既然真正的作者没写教学文章,代笔的作者也只是简单的表述了项目的意图,那么只能从工程代码开始看起了,原作者准备了6个场景。
00_viewNormal这个场景中包含了一个显示World法线方向的shader。
画面中的模型看起来,佛像正面绿色、黄色、红色、黑色都有,没有什么特别的。

▲01_pointLight
在脚本PointLightComponent.cs中:
为模型的材质提供3个参数:_LitPos _Intencity _LitCol
模仿了点光源组件的效果,提供光源坐标、光源强度、光源颜色。

在simple-pointLight.shader中:
简单计算漫反射颜色,简单处理了点光源的衰弱。

▲02_spotLight
在脚本SpotLightComponent.cs中:
为模型材质提供了6个参数,除了Spot光源的位置、强度、颜色,还有Spot光源的WorldToLight矩阵、Clip矩阵、Cookie。虽然挂了一个Spot Light组件,但是shader中并未使用系统默认参数,所以这个组件不会生效。
Spot光源相比Point光源要更复杂一些,除了会衰减外,还有明显的光照朝向,而Cookie贴图可以产生过滤效果,就好比从窗户射进来的光线会在墙上留下窗栏的影子。
在simple-spotLight.shader中:
除了类似于Point光源的衰减和漫反射光照,还有Cookie采样、有效区域判定。
将Spot光源看做相机,转移到Spot空间,再进行投影矩阵计算,使有效顶点约束在指定范围
var projMatrix = Matrix4x4.Perspective(angle, 1f, 0f, range);
near为0导致变换的位移为0,aspect为1意味着angle内的顶点都会缩放进一个正方形。
half2 litUv = projPos.xy / projPos.z;
直接将Z坐标作为W分量相除,在near为0的特殊情况,得到NDC坐标。
这样计算Spot光源光照的过程挺简单优美。

▲03_spotLight-withShadow
这个场景中实现了使用自定义的RT绘制阴影,摆脱渲染管线的束缚。
在传统的阴影渲染流程中,将相机位置移动到光源位置渲染出一个深度图,称为ShadowMap。
着色shader中对ShadowMap的采样可以判断当前位置是否在阴影内,以及阴影衰减。
本例中创建了一个enabled属性为false的Camera,用于处理投影矩阵和ShadowMap绘制Pass:
Camera.targetTexture = depthOutput; //指定渲染目标
Camera.SetReplacementShader(depthRenderShader, "RenderType");
Camera.Render(); //执行一次相机的渲染循环
Camera.SetReplacementShader或者Camera.RenderWithShader使用shader替换功能:
1.如果replacementTag为空,指定shader中的Subshader直接生效。
2.如果replacementTag不为空,将对比Tag值。不符合Tag和Tag值的Object将不被渲染。

本例中用到了阴影Pass,并在着色Pass中采样ShadowMap
o.depth = abs(UnityObjectToViewPos(pos).z); //使用光源空间坐标的Z值写入深度
half lightDepth = tex2D(_LitDepth, litUv).r; //采样shader
atten *= 1.0 - saturate(10 * abs(lightSpacePos.z) - 10 * lightDepth);
这里阴影Pass的输出为RFloat,单个32位浮点数,可保存超过1的值。

▲00_shouUv2
本例用到了一个UV展开Shader,返回UV的xy分量作为Clip空间位置和颜色输出。
我们希望渲染出来的结果表现的与贴图上下一致,所以需要在DX平台反转UV的Y轴。
例如:UV位置为(0, 0)的点,在DX中对应Texture的左上角,当我们用这个位置进行采样时,Unity已经内置帮我们处理了Texture的翻转,所以结果不会出错。但是当我们要将位置发送到屏幕上时(对UV空间进行写入),需要手动处理UV的翻转,使(0, 0)对应(0,1)这个位置。

▲01_spotDraw
最后一个场景,将揭开喷雾实现的真相。

Drawable.shader:可被喷雾的模型(简称SO)采用的shader
采样_MainTex获得颜色值,由程序每帧指定内容。

Drawable.cs:实现对_MainTex的写入
Graphics.DrawMeshNow(mesh, transform.localToWorldMatrix);
将SO的表面顶点转到点光源的裁剪空间,采样Cookie,得到喷雾值;
写入到临时RT(基于UV空间,逐SO配置),基于UV采样后实现蒙皮。

fillCrack材质:可选的后处理模糊效果

DrawingController.cs:控制渲染流程
有2个Spot光源,分别用于大面积低强度喷雾和小角度高强度喷雾。
spot.UpdateDrawingMat(); //绘制ShadowMap,用于采样阴影强度。
spot.Draw(drawable); //对所有PO进行上色,使用一个很大的光照角度;

var worldToDrawerMatrix = transform.worldToLocalMatrix;
确定Spot光源的朝向,根据shader中对z轴的反转操作,这里使用-z轴为朝向;

pos = cam.ScreenToWorldPoint(pos);
pinSpot.transform.position = cam.transform.position;
pinSpot.transform.LookAt(pos); //设置喷头的起点和朝向
将屏幕上的一点(用户的点击),转换到世界空间(相机近截面上的点);
用户的点击会改变第二个spot光源的transform,影响朝向。

ProjectionSpray.shader:
i.uv.y = 1 - i.uv.y; //DX中UV在左上角开始
col.rgb = lerp(col.rgb, _Color.rgb, saturate(col.a * _Emission * atten * cookie));
col来自于上一帧缓存,_Color则是喷雾的颜色,如果喷雾有效覆盖旧结果。

总结:
作者灵活运用了点光源的机制,而不仅仅是将点光源用于辅助光源。
精确控制对Texture的写入逻辑,可以过滤掉不想要的数据,实现假设的用途。


关注成长,注重因果。