找回密码
 立即注册
查看: 531|回复: 0

Unity-一个简单的水墨渲染方法

[复制链接]
发表于 2020-11-23 20:10 | 显示全部楼层 |阅读模式
(本文只是一个非常简单的unity水墨渲染,如果有错误,希望大家指正,谢谢~)
(已上传GitHub:https://github.com/boringsky/Unity_ChinesePainting)
中国水墨画的渲染效果是很久很久以前就有的方法,基本思想就是分为两个部分,轮廓线渲染和内部渲染。轮廓线通常是渲染成毛笔笔触的感觉,内部则是通过普通的光照方程再加上ramp贴图控制一下渐变纹理,最后用一些模糊处理。这也是基本的卡通渲染方法。
而使用unity进行卡通渲染的基本思想,冯乐乐女神已经在《Unity Shader 入门精要》里解释得非常完整,我就不添乱了,上链接(乐乐姐的卡通渲染)。同时本文也参考了知乎上两位大佬的unity实现方法(在Unity进行水墨风3D渲染的尝试&【Unity Shader】 水墨风格渲染:如何优雅的画一只猴子 )。
轮廓线shader

乐乐姐已经介绍很详细轮廓线的渲染方法了,所以选择她在书中说的“过程式集合轮廓线渲染方法”。简言之,单独用一个pass将模型沿法线扩张一点,然后渲染成轮廓线颜色,然后再用一个pass正常渲染内部着色,遮挡住前面的部分,留下来显示出来的部分就是轮廓线啦。主要部分的代码如下:
Properties
        {
                [Header(OutLine)]
                // Stroke Color
                _StrokeColor ("Stroke Color", Color) = (0,0,0,1)
                // Noise Map
                _OutlineNoise ("Outline Noise Map", 2D) = "white" {}
                // First Outline Width
                _Outline ("Outline Width", Range(0, 1)) = 0.1
                // Second Outline Width
                _OutsideNoiseWidth ("Outside Noise Width", Range(1, 2)) = 1.3
                _MaxOutlineZOffset ("Max Outline Z Offset", Range(0,1)) = 0.5

        }
    SubShader
        {
                Tags { "RenderType"="Opaque" "Queue"="Geometry"}

                // the first outline pass
                Pass
                {
                // 主要在vertex shader内进行计算 省略部分基本参数设置
                        v2f vert (a2v v)
                        {
                                // fetch Perlin noise map here to map the vertex
                                // add some bias by the normal direction
                                float4 burn = tex2Dlod(_OutlineNoise, v.vertex);

                                v2f o = (v2f)0;
                                float3 scaledir = mul((float3x3)UNITY_MATRIX_MV, normalize(v.normal.xyz));
                                scaledir += 0.5;
                                scaledir.z = 0.01;
                                scaledir = normalize(scaledir);

                                // camera space
                                float4 position_cs = mul(UNITY_MATRIX_MV, v.vertex);
                                position_cs /= position_cs.w;

                                float3 viewDir = normalize(position_cs.xyz);
                                float3 offset_pos_cs = position_cs.xyz + viewDir * _MaxOutlineZOffset;

                                // y = cos(fov/2)
                                float linewidth = -position_cs.z / (unity_CameraProjection[1].y);
                                linewidth = sqrt(linewidth);
                                position_cs.xy = offset_pos_cs.xy + scaledir.xy * linewidth * burn.x * _Outline ;
                                position_cs.z = offset_pos_cs.z;
                                o.pos = mul(UNITY_MATRIX_P, position_cs);

                                return o;
                        }
                // fragment shader只是输出了一个颜色 不赘述
                }
}其中基本需要设置的参数都很简单明了。而基本的思想也是按照乐乐姐书中所说,在视角空间下,将顶点沿着法线扩张。而针对水墨画风格渲染,其实就是做了一个最简单的noise干扰,在这里使用noise纹理图片(_OutlineNoise)进行采样,这样又个好处就是随机出来的轮廓不会随着视角的改变而改变。
_OutlineNoise
其中稍微有点改变的是,增加了一个linewidth的操作,因为unity_CameraProjection[1].y其实就是cos(FOV/2),所以这个操作的根本目的是为了保证轮廓线随着FOV的变换也是成一定比例,同时也不会随着镜头离物体的远近距离而变换。
对比图片如下:
没有添加linewidth
添加linewidth
最后一个小trick是,再增加了一个pass进行完全相同的操作,只是宽度再稍微增加一点,然后在fragment shader里根据noise再进行一下剔除。这也是在属性里面,之前没有用到的_OutsideNoiseWidth,来控制第二个pass的轮廓线的宽度,理论上它要大于1,比第一个pass稍微宽一些。简要的代码如下:
// 在vertex shader内 只需要稍微改变一点
position_cs.xy = offset_pos_cs.xy + scaledir.xy * linewidth * burn.y * _Outline * _OutsideNoiseWidth ;

// 在fragment shader内 也稍微根据noise突变做了下剔除
fixed4 frag(v2f i) : SV_Target
{
        //clip randome outline here
        fixed4 c = _StrokeColor;
        fixed3 burn = tex2D(_OutlineNoise, i.uv).rgb;
        if (burn.x > 0.5)
                discard;
        return c;
}对比图片如下:
只有一个pass渲染轮廓线
用两个pass渲染轮廓线
内部渲染

而内部着色的基本思想和unity卡通渲染的一致,使用最基本的光照方程,再映射到一张ramp图上进行采样,最后形成的就是阶梯状的颜色过渡。在这里我用的ramp图如下:
同时,与其余的水墨渲染方法有所区别的是,我发现,相对把笔触纹理的图和最终颜色值叠加融合起来,直接将纹理笔触作为一个noise贴图,扰动uv的值之后再进行一次高斯模糊,效果感觉也不错。在这里我是用了一张笔触纹理和一个noise贴图混合的一起扰动uv。
笔触纹理图
所以最后内部着色的内部渲染部分的步骤就是,先计算半兰伯特漫反射系数,然后用笔触纹理和noise纹理稍微扰动一下,最后再采样ramp纹理的时候进行高斯模糊。代码如下:
Shader "ChinesePainting/MountainShader"
{
        Properties
        {
                [Header(OutLine)]
                //...省略上述已介绍过的

                [Header(Interior)]
                _Ramp ("Ramp Texture", 2D) = "white" {}
                // Stroke Map
                _StrokeTex ("Stroke Tex", 2D) = "white" {}
                _InteriorNoise ("Interior Noise Map", 2D) = "white" {}
                // Interior Noise Level
                _InteriorNoiseLevel ("Interior Noise Level", Range(0, 1)) = 0.15
                // Guassian Blur
                radius ("Guassian Blur Radius", Range(0,60)) = 30
                resolution ("Resolution", float) = 800  
                hstep("HorizontalStep", Range(0,1)) = 0.5
                vstep("VerticalStep", Range(0,1)) = 0.5  

        }
        SubShader
        {
                Tags { "RenderType"="Opaque" "Queue"="Geometry"}

                // the first outline pass
                // 省略

                // the second outline pass for random part, a little bit wider than last one
                // 省略

                // the interior pass
                Pass
                {
                        // 之前的vertex shader部分没有特殊操作  省略
                        float4 frag(v2f i) : SV_Target
                        {
                                fixed3 worldNormal = normalize(i.worldNormal);
                                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                                // Noise
                                // For the bias of the coordiante
                                float4 burn = tex2D(_InteriorNoise, i.uv);
                                //a little bit disturbance
                                fixed diff =  dot(worldNormal, worldLightDir);
                                diff = (diff * 0.5 + 0.5);
                                float2 k = tex2D(_StrokeTex, i.uv).xy;
                                float2 cuv = float2(diff, diff) + k * burn.xy * _InteriorNoiseLevel;

                                // This iniminate the bias of the uv movement
                                if (cuv.x > 0.95)
                                {
                                        cuv.x = 0.95;
                                        cuv.y = 1;
                                }
                                if (cuv.y >  0.95)
                                {
                                        cuv.x = 0.95;
                                        cuv.y = 1;
                                }
                                cuv = clamp(cuv, 0, 1);

                                // Guassian Blur
                                float4 sum = float4(0.0, 0.0, 0.0, 0.0);
                                float2 tc = cuv;
                                // blur radius in pixels
                                float blur = radius/resolution/4;     
                                sum += tex2D(_Ramp, float2(tc.x - 4.0*blur*hstep, tc.y - 4.0*blur*vstep)) * 0.0162162162;
                                sum += tex2D(_Ramp, float2(tc.x - 3.0*blur*hstep, tc.y - 3.0*blur*vstep)) * 0.0540540541;
                                sum += tex2D(_Ramp, float2(tc.x - 2.0*blur*hstep, tc.y - 2.0*blur*vstep)) * 0.1216216216;
                                sum += tex2D(_Ramp, float2(tc.x - 1.0*blur*hstep, tc.y - 1.0*blur*vstep)) * 0.1945945946;
                                sum += tex2D(_Ramp, float2(tc.x, tc.y)) * 0.2270270270;
                                sum += tex2D(_Ramp, float2(tc.x + 1.0*blur*hstep, tc.y + 1.0*blur*vstep)) * 0.1945945946;
                                sum += tex2D(_Ramp, float2(tc.x + 2.0*blur*hstep, tc.y + 2.0*blur*vstep)) * 0.1216216216;
                                sum += tex2D(_Ramp, float2(tc.x + 3.0*blur*hstep, tc.y + 3.0*blur*vstep)) * 0.0540540541;
                                sum += tex2D(_Ramp, float2(tc.x + 4.0*blur*hstep, tc.y + 4.0*blur*vstep)) * 0.0162162162;

                                return float4(sum.rgb, 1.0);
                        }
                        ENDCG
                }
        }
        FallBack "Diffuse"
}
其最终的效果如下:
调整光源方向
同时可以在网上搜一些不同的毛笔笔触纹理,也会有不同的效果。For Example:
最后,本文只是一个非常简单的unity水墨渲染,如果有错误,希望大家指正,谢谢~

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-17 12:26 , Processed in 0.096591 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表