前言
Shells Shader是我大概半年前实现的一个小Demo,本篇实现综合借鉴了许多内外网文章,详情见引用列表,印象中最深的是王者分享的阿狸那篇文章,不过本篇是以另一种方式实现的。Shells Shader用途很多,主要用于毛发/草地/地毯材质的渲染(但是毛发如果用纯Shell表现并不好,需要夹杂Fin,纯Shell形式表现地毯会相对好些),本篇会主要讲地毯形式渲染,并会扩展补充一些Shells Shader的其他用途(比如Shells Shader体积云,见WorkingFat前辈写的相关文章)。
这篇文章我会边重写一遍Shader(untiy2021.2+,urp12.0+),边回忆一下当时的思路。
效果
Todo
- 自定义阴影
- 支持GPU Instance
- Shell 体积云
什么是Shells
Shells就是沿几何体的法线方向生成 n 层网格并且每次将网格顶点沿法线挤出并裁剪(有点类似于外描线中顶点外扩的方式,所以顶点外扩的部分优化方法对Shells Shader同样适用),总共生成n 层网格。每层网格都会适度的采样MainTex。Shells Shader其实就是实现Shells方式的GS(Geometry Shader),类似于下图(PS鼠绘灵魂画手)。
实线部分表示Geometry Surface,虚线就是Shells.
实现
1.
对于王者分享的那篇制作方案本身我就不细讲了,因为我的实现方式不同于那篇分享,有很多文章非常详细的讲解了那篇分享的实现方式,比如:https://zhuanlan.zhihu.com/p/87443345。王者那篇分享里原理相关部分讲的非常详细,但是这种方式生成的毛发比较凌乱,而且多Pass性能消耗问题很大。本篇实现只需要通过一个Pass就可以实现Shells的几何效果,当然后面优化上加上深度、阴影共3个Pass。
因为我目前只在pc下看实现的效果,所以去除了gl版本的编译,如果想在移动端跑的同学可以在我写的基础上优化(但是手机上要想跑GS性能就。。。)
#pragma exclude_renderers gles gles3 glcore
下面的分开讲
1.Pass 1
首先要生成Shells,我们需要确定两个参数,一个是layer num,表示生成多少层shell,一个是layer space,表示层间距。
[IntRange]_LayerNum("Layer Number",Range(1,50)) = 15 _LayerSpace("Layer Space",Range(0.0, 10.0)) = 1.0
我们需要在管线的vs、gs、fs三个阶段里传递数据,所以先定义三种结构体,Pass定义大致如下
Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" struct appdata { //todo.. }; struct v2g { //todo.. }; //已经在UnlitInput.hlsl中定义过,不需要重定义 //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST; v2g vert(appdata v) { v2g o = (v2g)0; //todo.. return o; } struct g2f { //todo.. }; [maxvertexcount(90)] void geom(triangle v2g i[3], inout TriangleStream stream) { //todo.. } float4 frag(g2f i) : SV_Target { //todo.. float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); float3 color = baseColor; return float4(color, 1.0); } ENDHLSL }
可能会有人不懂[maxvertexcount(90)]这个标签的作用(90这里我随便先写的),简单的讲就声明着色器调用将输出的最大顶点数。这里就顺便说下DX11的GS和GS Instance吧:因为DX11中默认声明的instanceCount为32(bit),即1024;在GS Instance中每个Instance * 输出通道不能超过1024,比如假设我的g2f定义如下:
float2 uv : TEXCOORD0; float4 positionCS : SV_POSITION; float4 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float3 tangentWS : TEXCOORD3; float layer : TEXCOORD4;
共有17(bit)通道,那么我能够定义的maxvertexcount最大值不能超过1024/17 = 60.235 -> 60,即[maxvertexcount(60)];记住我这个算法就足够了,其实也不需要太过纠结原理,不影响使用。
如果还有疑问参考下面的几个链接的资料吧:
1.https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader
如何生成Shells呢?
首先我们想到的重要步骤是在GS中生成Shells,即使顶点沿着物体的N方向偏移,假如我们需要生成Index层Shell,那么第一层Shell我们移动 0 * N * _LayerSpace,第二层移动 1 * N * _LayerSpace,....,以此类推:(对,就是绘制outline最常用的顶点外扩的方式,只不过是在GS中写的,所以顶点外扩的优化方案对此同样适用,这里就不细讲了)
float3 positionWS = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * Index);
但是如果只是简单的移动顶点肯定不行,我们需要表现合适的密度,因为我这里模拟的是地毯毛绒的密度,所以可以用一张白噪声来表现(单通道就够),给定一个阈值,低于阈值并且LayerNum>0的部分我们自定义进行裁剪。
float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r; if (i.layer > 0.0 && cut< _CutThreshold) discard;
效果如下(分别是调整_CutThreshold、layerNum、layerSpace的效果),完整代码见后
Shader "Shell" { Properties { _BaseMap("BaseMap", 2D) = "white" {} _FurNoise("FurNoise", 2D) = "white" {} [IntRange]_LayerNum("Layer Number",Range(1,50)) = 15 _LayerSpace("Layer Space",Range(0.0, 0.1)) = 0.001 _CutThreshold("_CutThreshold", Range(0.0, 1.0)) = 0.1 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline" } LOD 100 ZWrite On Cull Back // Convention: // space at the end of the variable name // WS: world space // RWS: Camera-Relative world space. A space where the translation of the camera have already been substract in order to improve precision // VS: view space // OS: object space // CS: Homogenous clip spaces // TS: tangent space // TXS: texture space // use capital letter for regular vector, vector are always pointing outward the current pixel position (ready for lighting equation) // capital letter mean the vector is normalize, unless we put 'un' in front of it. // V: View vector (no eye vector) // L: Light vector // N: Normal vector // H: Half vector Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" int _LayerNum; float _LayerSpace; float _CutThreshold; //已经在UnlitInput.hlsl中定义过,不需要重定义 //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST; TEXTURE2D(_FurNoise); SAMPLER(sampler_FurNoise);float4 _FurNoise_ST; struct appdata { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; struct v2g { float2 uv : TEXCOORD0; float4 positionOS : SV_POSITION; float3 normalOS : TEXCOORD1; float4 tangentOS : TEXCOORD2; }; v2g vert(appdata v) { v2g o = (v2g)0; o.positionOS = v.positionOS; o.normalOS = v.normalOS; o.tangentOS = v.tangentOS; o.uv = v.uv; return o; } struct g2f { float2 uv : TEXCOORD0; float2 uv2 : TEXCOORD1; float4 positionCS : SV_POSITION; float layer : TEXCOORD3; }; //GeometryShader [maxvertexcount(96)] void geom(triangle v2g i[3], inout TriangleStream stream) { [loop] for (float j = 0; j < _LayerNum; ++j) { [unroll] for (float k = 0; k < 3; ++k) { g2f o = (g2f)0; //pos and N VertexPositionInputs vertexInput = GetVertexPositionInputs(i[k].positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(i[k].normalOS, i[k].tangentOS); //沿顶点偏移 float3 positionWS = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * j); o.positionCS = TransformWorldToHClip(positionWS); //uv o.uv = TRANSFORM_TEX(i[k].uv, _BaseMap); o.uv2 = TRANSFORM_TEX(i[k].uv, _FurNoise); //layer o.layer = (float)j/_LayerNum; //append data stream.Append(o); } stream.RestartStrip(); } } float4 frag(g2f i) : SV_Target { float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r; if (i.layer > 0.0 && cut< _CutThreshold) discard; float4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); return color; } ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ZWrite On ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags {"LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } } }
为了体验毛茸茸的感觉,我们需要把间距拉小,层数适中。标签[maxvertexcount(N)]来定义几何着色器单次调用输出的最大顶点个数,这个输出顶点个数应当是输出类型对应的顶点数的整数倍。因为我们裁剪中丢弃了部分顶点,所以需要在return前使用stream.RestartStrip()来保证输出流的格式正确。
不过目前这个效果一点也不好,只能看到一个基础形状,我们需要在此基础上继续优化我们的shader:
1.添加PBR渲染下的光照效果
这里我偷懒就使用Unity官方写好的URP Lit了,详情可以参考URP ShaderLab的Lit.Shader源码。这里我参照的是Metallic workflow。
本来花费了一个星期整理了一下Unity urp的新PBR(其实和之前没区别),以及URP12.0之后添加的新特性(下面代码中的一些用不到的Key都没有删,为了方便自己后面扩展),但是再放在这里讲篇幅就太太太长了,等我整理一篇新的吧。这里就简单粘贴一下代码(可以先参照URP的Lit.shader理解一些新特性)
Shader "Shell" { Properties { [Header(Base)][Space] [MainColor] _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1) _AmbientColor("Ambient Color", Color) = (0.0, 0.0, 0.0, 1) _BaseMap("Albedo", 2D) = "white" {} _FurScale("Fur Scale", Range(0.0, 10.0)) = 1.0 _FurNoise("FurNoise", 2D) = "white" {} _Occlusion("Occlusion", Range(0.0, 1.0)) = 0.5 [IntRange]_LayerNum("Layer Number",Range(1,50)) = 15 _LayerSpace("Layer Space",Range(0.0, 0.1)) = 0.001 _CutThreshold("_CutThreshold", Range(0.0, 1.0)) = 0.1 [Space][Header(UnityPBR)][Space] [Gamma]_Metallic("Metallic", Range(0.0, 1.0)) = 0.5 _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5 [Normal] _NormalMap("Normal", 2D) = "bump" {} _NormalScale("Normal Scale", Range(0.0, 2.0)) = 1.0 [Header(Lighting)][Space] _RimLightPower("Rim Light Power", Range(1.0, 20.0)) = 6.0 _RimLightIntensity("Rim Light Intensity", Range(0.0, 1.0)) = 0.5 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline" } LOD 100 ZWrite On Cull Back // Convention: // space at the end of the variable name // WS: world space // RWS: Camera-Relative world space. A space where the translation of the camera have already been substract in order to improve precision // VS: view space // OS: object space // CS: Homogenous clip spaces // TS: tangent space // TXS: texture space // use capital letter for regular vector, vector are always pointing outward the current pixel position (ready for lighting equation) // capital letter mean the vector is normalize, unless we put 'un' in front of it. // V: View vector (no eye vector) // L: Light vector // N: Normal vector // H: Half vector Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM // Universal Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION #pragma multi_compile_fragment _ _SHADOWS_SOFT #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION #pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3 #pragma multi_compile_fragment _ _LIGHT_LAYERS #pragma multi_compile_fragment _ _LIGHT_COOKIES #pragma multi_compile _ _CLUSTERED_RENDERING // ------------------------------------- // Unity defined keywords #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING #pragma multi_compile _ SHADOWS_SHADOWMASK #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile _ DYNAMICLIGHTMAP_ON #pragma multi_compile_fog #pragma multi_compile_fragment _ DEBUG_DISPLAY #pragma exclude_renderers gles gles3 glcore #pragma vertex vert #pragma require geometry #pragma geometry geom #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" int _LayerNum; float _LayerSpace; float _CutThreshold; float _Occlusion; float _FurScale; float _NormalScale; float _RimLightPower; float _RimLightIntensity; float _AmbientColor; //已经在LitInput.hlsl中定义过,不需要重定义 //TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST; TEXTURE2D(_FurNoise); SAMPLER(sampler_FurNoise);float4 _FurNoise_ST; TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);float4 _NormalMap_ST; struct appdata { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2g { float2 uv : TEXCOORD0; float4 positionOS : SV_POSITION; float3 normalOS : TEXCOORD1; float4 tangentOS : TEXCOORD2; UNITY_VERTEX_OUTPUT_STEREO }; v2g vert(appdata v) { v2g o = (v2g)0; o.positionOS = v.positionOS; o.normalOS = v.normalOS; o.tangentOS = v.tangentOS; o.uv = v.uv; return o; } struct g2f { float2 uv : TEXCOORD0; float4 positionCS : SV_POSITION; float4 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float3 tangentWS : TEXCOORD3; float layer : TEXCOORD4; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 5); float4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light UNITY_VERTEX_OUTPUT_STEREO }; //GeometryShader //maxvertexcount [maxvertexcount(40)] void geom(triangle v2g i[3], inout TriangleStream stream) { [loop] for (float j = 0; j < _LayerNum; ++j) { [unroll] for (float k = 0; k < 3; ++k) { g2f o = (g2f)0; //pos and N VertexPositionInputs vertexInput = GetVertexPositionInputs(i[k].positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(i[k].normalOS, i[k].tangentOS); o.normalWS = normalInput.normalWS; o.tangentWS = normalInput.tangentWS; //vertex offset o.positionWS.xyz = vertexInput.positionWS + normalInput.normalWS * (_LayerSpace * j); o.positionCS = TransformWorldToHClip(o.positionWS); //uv o.uv = TRANSFORM_TEX(i[k].uv, _BaseMap); //o.uv2 = TRANSFORM_TEX(i[k].uv, _FurNoise); //layer o.layer = (float)j/_LayerNum; //Lighting float3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); float fogFactor = ComputeFogFactor(vertexInput.positionCS.z); o.fogFactorAndVertexLight = float4(fogFactor, vertexLight); OUTPUT_LIGHTMAP_UV(i.lightmapUV, unity_LightmapST, o.lightmapUV); OUTPUT_SH(o.normalWS.xyz, o.vertexSH); //append data stream.Append(o); } stream.RestartStrip(); } } void ApplyRimLight(inout float3 color, float3 posWS, float3 viewDirWS, float3 normalWS) { float VDotN = abs(dot(viewDirWS, normalWS)); float normalFactor = pow(abs(1.0 - VDotN), _RimLightPower); Light light = GetMainLight(); float LDotV = dot(light.direction, viewDirWS); float intensity = pow(max(-LDotV , 0.0), _RimLightPower); intensity *= _RimLightIntensity * normalFactor; #if defined(MAIN_LIGHT_CALCULATE_SHADOWS) float4 shadowCoord = TransformWorldToShadowCoord(posWS); intensity *= MainLightRealtimeShadow(shadowCoord); #endif color += intensity * light.color; #ifdef _ADDITIONAL_LIGHTS int additionalLightsCount = GetAdditionalLightsCount(); for (int i = 0; i < additionalLightsCount; ++i) { int index = GetPerObjectLightIndex(i); Light light = GetAdditionalPerObjectLight(index, posWS); float LDotV = dot(light.direction, viewDirWS); float intensity = max(-LDotV , 0.0); intensity *= _RimLightIntensity * normalFactor; intensity *= light.distanceAttenuation; #if defined(MAIN_LIGHT_CALCULATE_SHADOWS) //support Point Light shadows // returns 0.0 if position is in light's shadow // returns 1.0 if position is in light intensity *= AdditionalLightRealtimeShadow(index, posWS, light.direction); #endif color += intensity * light.color; } #endif } float4 frag(g2f i) : SV_Target { float2 uv2 = i.uv / _BaseMap_ST.xy * _FurScale; float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, uv2).r; if (i.layer > 0.0 && cut< _CutThreshold) discard; float3 viewDirWS = SafeNormalize(GetCameraPositionWS() - i.positionWS); float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv2), _NormalScale); float3 bitangent = SafeNormalize(viewDirWS.y * cross(i.normalWS, i.tangentWS)); float3 normalWS = SafeNormalize(TransformTangentToWorld( normalTS, float3x3(i.tangentWS, bitangent, i.normalWS))); SurfaceData surfaceData = (SurfaceData)0; InitializeStandardLitSurfaceData(i.uv, surfaceData); surfaceData.occlusion = lerp(1.0 - _Occlusion, 1.0, i.layer); surfaceData.albedo *= surfaceData.occlusion; InputData inputData = (InputData)0; inputData.positionWS = i.positionWS; inputData.normalWS = normalWS; inputData.viewDirectionWS = viewDirWS; #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF) inputData.shadowCoord = TransformWorldToShadowCoord(i.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif inputData.fogCoord = i.fogFactorAndVertexLight.x; inputData.vertexLighting = i.fogFactorAndVertexLight.yzw; inputData.bakedGI = SAMPLE_GI(i.lightmapUV, i.vertexSH, normalWS); float4 color = UniversalFragmentPBR(inputData, surfaceData); //RimLight ApplyRimLight(color.rgb, i.positionWS, viewDirWS, normalWS); //float4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); color.rgb += _AmbientColor; color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ZWrite On ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags {"LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl" #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } } }
效果图如下:
(1)PBR(Metallic workflow)
(2)高性能 | Only One DC for Gemotry
(3)支持多光照/多点光源
总览(添加后处理等):
2.更加风格化的细节
通常毛绒距离表面越远(密度)越细,意味着裁剪程度越大,所以我们做一下处理:
float cut = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv2).r * (1.0 - i.layer);
3.顶点动画
根据Shell层级设置不同的偏移值
float3 shellAnimation(int index,float3 posOS) { float moveParam = pow(abs((float)index / _LayerNum), _BaseMove.w); float3 windAngle = _Time.w * _WindFreq.xyz; float3 windMove = moveParam * _WindMove.xyz * sin(windAngle + posOS * _WindMove.w); float3 move = moveParam * _BaseMove.xyz + windMove; return move; }
可能还需要做的优化
LOD
如果是草地这种要写LOD,写更好的裁剪方案
GPU Instancing
支持GPU Instancing,可以支持大世界铺满这种,一般也是绘制草地才需要
Tessellation
利用曲面细分平滑,详见 https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
Mesh shader(DirectX12 Ultimate /Vulkan)
采用新的shader pipeline,这个实际上是在硬件层面优化,我也在学习中,就不献丑了
引用&参考(不分引用先后)
1.https://mp.weixin.qq.com/s/aIWMEO5Qa2gNn2yCmnHbOg
2.https://xbdev.net/directx3dx/specialX/Fur/
3.https://zhuanlan.zhihu.com/p/378015611
4.https://developer.download.nvidia.com/whitepapers/2007/SDK10/FurShellsAndFins.pdf
5.https://docs.unity3d.com/Manual/StandardShaderMaterialParameterMetallic.html
6.https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader
7.http://walkingfat.com/%e5%9f%ba%e4%ba%8e%e8%a7%86%e5%b7%ae%e8%b4%b4%e5%9b%be%e7%9a%84%e6%af%9b%e5%8f%91%e6%95%88%e6%9e%9c/
8.http://walkingfat.com/bump-noise-cloud-3d%e5%99%aa%e7%82%b9gpu-instancing%e5%88%b6%e4%bd%9c%e5%9f%ba%e4%ba%8e%e6%a8%a1%e5%9e%8b%e7%9a%84%e4%bd%93%e7%a7%af%e4%ba%91/
9.http://walkingfat.com/%e5%9f%ba%e4%ba%8etessellation%e7%9a%84%e4%bd%93%e7%a7%af%e4%ba%91/
10.https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
评论已关闭