这次的目标是绘制一颗闪闪发光的钻石,追求效果是越接近真实越好。
先说说为此我这几天干了些什么。
1.看了stalendp blog里那篇《钻石效果》后头的参考文献
最有价值的就是ATI在2004年GDC上作的演讲,题目就叫Drawing a Diamond。但是由于只有ppt,所以很难重现工程深入学习。思路大概是预备了一张折射CubeMap,一张带模拟色散的反射CubeMap,最后再加上耀斑混合而成。
2.试验了一些简单的镜面反射
非实时的CubeMap反射最简单没什么好说的,需要注意的是要做到位置精确的镜面反射还是很难的,一不小心就会发现当物体离反射面很近时,尺寸位置都会严重错误。
像下图这样,球的镜像明显尺寸过大了。
实时获取环境CubeMap的反射也测试了下,没记错的话上图CubeMap的FaceSize设的是512,在我的一加手机上基本就跑不到正常帧了,FaceSize设成256帧率还可以接受,但画质就没法看了,总之实时反射是手机无法承受的。
这里我总结一点思想:
游戏画面表现本身就带有很多虚假的成分,因为全按真实的来计算性能根本无法承受,最简单的拿光照来说,不谈多次反射的环境光,单纯的一次平行光照实时计算都没法满足,都要预烘焙光照贴图,阴影也一样,真实的计算太费性能,一般也弄个假的完事。
所以我们会发现游戏画面和现实照片的差距总是一目了然。所以会出现很多专门掩饰的技巧。
如何抓住主要部分,在效果和性能间做出取舍,而又能尽量达到接近真实的效果,大概就是图形工程师们大多数时候在思考的事情了。
这个钻石效果的演示demo,明显要按固定环境的思路来,不需要考虑实时问题,像前面提到的那种尺寸扭曲的问题,基本也是可以忽略的,因为钻石形状类似小球,环境的轻微扭曲是能被轻易掩饰过去的。
3.到AssetStore搜索了下相关资源
发现免费的Gem Shaders就是Unity官方提供的,而且还为5.0专门更新过一次。我跑了下它的示例场景,感觉观感不是很满意。
主要是它提供的几个钻石模型形状我也不喜欢,于是自己到网上找了个大概是传说中“八星八箭”形状的模型,然后还是用这个shader试了下效果,结果发现还是很屌的。
我原本的想法是尽量用真实的光线折射、反射计算得到最终效果的。但是就算暂不考虑代码实现,不考虑实现后的性能如何,首先单纯就目前凭我有限的光学姿势能不能建立起真实准确的光学模型都很明显是成问题的。
所以这次我们还是以学习官方的示例为主,不要整天想自己搞什么大新闻了。
代码如下:
Shader "FX/Gem" { Properties { _Color ("Color", Color) = (1,1,1,1) _ReflectionStrength ("Reflection Strength", Range(0.0,2.0)) = 1.0 _EnvironmentLight ("Environment Light", Range(0.0,2.0)) = 1.0 _Emission ("Emission", Range(0.0,2.0)) = 0.0 [NoScaleOffset] _RefractTex ("Refraction Texture", Cube) = "" {} } SubShader { Tags { "Queue" = "Transparent" } // First pass - here we render the backfaces of the diamonds. Since those diamonds are more-or-less // convex objects, this is effectively rendering the inside of them. Pass { Cull Front ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float3 uv : TEXCOORD0; }; v2f vert (float4 v : POSITION, float3 n : NORMAL) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v); // TexGen CubeReflect: // reflect view direction along the normal, in view space. float3 viewDir = normalize(ObjSpaceViewDir(v)); o.uv = -reflect(viewDir, n); o.uv = mul(_Object2World, float4(o.uv,0)); return o; } fixed4 _Color; samplerCUBE _RefractTex; half _EnvironmentLight; half _Emission; half4 frag (v2f i) : SV_Target { half3 refraction = texCUBE(_RefractTex, i.uv).rgb * _Color.rgb; half4 reflection = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.uv); reflection.rgb = DecodeHDR (reflection, unity_SpecCube0_HDR); half3 multiplier = reflection.rgb * _EnvironmentLight + _Emission; return half4(refraction.rgb * multiplier.rgb, 1.0f); } ENDCG } // Second pass - here we render the front faces of the diamonds. Pass { ZWrite On Blend One One CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float3 uv : TEXCOORD0; half fresnel : TEXCOORD1; }; v2f vert (float4 v : POSITION, float3 n : NORMAL) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v); // TexGen CubeReflect: // reflect view direction along the normal, in view space. float3 viewDir = normalize(ObjSpaceViewDir(v)); o.uv = -reflect(viewDir, n); o.uv = mul(_Object2World, float4(o.uv,0)); o.fresnel = 1.0 - saturate(dot(n,viewDir)); return o; } fixed4 _Color; samplerCUBE _RefractTex; half _ReflectionStrength; half _EnvironmentLight; half _Emission; half4 frag (v2f i) : SV_Target { half3 refraction = texCUBE(_RefractTex, i.uv).rgb * _Color.rgb; half4 reflection = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.uv); reflection.rgb = DecodeHDR (reflection, unity_SpecCube0_HDR); half3 reflection2 = reflection * _ReflectionStrength * i.fresnel; half3 multiplier = reflection.rgb * _EnvironmentLight + _Emission; return fixed4(reflection2 + refraction.rgb * multiplier, 1.0f); } ENDCG } // Shadow casting & depth texture support -- so that gems can // cast shadows UsePass "VertexLit/SHADOWCASTER" } }
下面我来分析其中的姿势点:
个人水平有限很多说法可能是错的,这里申明下先。
1.折射光的模拟
首先它的折射光并没有真实计算,而是给了这样一张CubeMap图,然后实际上是通过反射在计算。
2.pass设计
大致分两步,第一个pass是绘制钻石的背面,第二个当然就是前面了。注意第二个pass里的Blend One One,表示两个图层1:1混合。
考虑到钻石是个透明的物体,这样设计也是合情合理的。
3.光照模型设计
大致也是折射光、反射光、环境光、自发光各种叠加各种乘啦。反正因为白色是255,黑色是0,这里虽说算的是光,但实际处理的还是颜色,不管是加还是乘,都是越叠越亮,符合光线叠加的基本规律。至于它的计算公式是源自准确的光学原理,还是近似的模拟,我当然也不知道啦。
4.HDR
High Dynamic Range,不负责任的说下,大概就是假如0是黑色,1是白色,那么有些部分的颜色可以超过1,这样最终画面结果会呈现出一种亮处高闪耀的效果,具体请参考Unity官方文档说明。
5.菲涅尔效果
简单说就是因为光线射向玻璃时,一部分被反射,一部分直接射进去了,导致最终反射光线呈现一个视角越平行于入射面,反射效果越强,越垂直越弱的现象。
第二个pass计算时引入了这个因子。
所以一开始我说自己难以建立准确的模型,比如像这种细微而又真实存在的现象,仅凭一点粗浅的折射反射姿势而不去深入学习又怎么能考虑到呢?
这个shader里用了好多宏,在CGIncludes文件夹里的那些文件里都可以查到,这些我大致可以理解或者叫猜到怎么回事,但要真正弄清楚肯定得费一番功夫。
这里请允许猪哥偷个懒,随便说说装作懂了的样子,具体的计算细节就不一一展开了。
结语:
至此初阶段的shader学习计划就算完成了,由于我刻意赶了下进度,偷了些懒,比计划提前了一周。一分耕耘一分收获,shader这块水很深,想要有所造诣肯定得花更多的时间。但我出于全盘考虑目前并不想在这块继续投放精力。
下一阶段目光回到C#语言这块最基础的领域,继续修内力。去年买的《C#本质论》要收尾,一直没翻的《深入理解C#》也要看完。
谈语言难免会牵涉到设计模式,但也尽量避开。
周期定在五周吧。