[toc]本篇讨论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的写入逻辑,可以过滤掉不想要的数据,实现假设的用途。