unity 模型渐变消失_Unity-一个简单的水墨渲染方法

这篇博客介绍了在Unity中实现简单水墨渲染的方法,包括轮廓线shader和内部渲染两部分。作者采用了过程式集合轮廓线渲染,利用法线扩张和噪声纹理创建轮廓线,并通过调整linewidth确保轮廓线宽度与视角变化一致。内部渲染则结合光照方程、ramp贴图和高斯模糊实现颜色过渡。文章提供了代码示例和效果对比,鼓励读者尝试不同毛笔笔触纹理以获得不同效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

04f5d952ab44b2fcfa043a1330cffa39.png

(本文只是一个非常简单的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)进行采样,这样又个好处就是随机出来的轮廓不会随着视角的改变而改变。

cf7aeb54bb20b84cff64a1dce3f5b8db.png
_OutlineNoise

其中稍微有点改变的是,增加了一个linewidth的操作,因为unity_CameraProjection[1].y其实就是cos(FOV/2),所以这个操作的根本目的是为了保证轮廓线随着FOV的变换也是成一定比例,同时也不会随着镜头离物体的远近距离而变换。

对比图片如下:

7e5e558a138379c79e0f93c729357dac.gif
没有添加linewidth

2206de10a183dfa58b8c1e223212ec75.gif
添加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;
}

对比图片如下:

e34bd161b84df04b742e306847e04862.png
只有一个pass渲染轮廓线

f13b82d785b3f2b9ee60dd06b64f9929.png
用两个pass渲染轮廓线

内部渲染

而内部着色的基本思想和unity卡通渲染的一致,使用最基本的光照方程,再映射到一张ramp图上进行采样,最后形成的就是阶梯状的颜色过渡。在这里我用的ramp图如下:

1ccd5662fc5c02be66f85dc5b90124f9.png

同时,与其余的水墨渲染方法有所区别的是,我发现,相对把笔触纹理的图和最终颜色值叠加融合起来,直接将纹理笔触作为一个noise贴图,扰动uv的值之后再进行一次高斯模糊,效果感觉也不错。在这里我是用了一张笔触纹理和一个noise贴图混合的一起扰动uv。

f2510f145b9f5e668903e7c56512fc3b.png
笔触纹理图

所以最后内部着色的内部渲染部分的步骤就是,先计算半兰伯特漫反射系数,然后用笔触纹理和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"
}

其最终的效果如下:

06bbdefa18ec002e20f0fa4d7d308d9b.png

f13b82d785b3f2b9ee60dd06b64f9929.png

8f0656d0f7155c1f44a4c852ba761e48.gif
调整光源方向

同时可以在网上搜一些不同的毛笔笔触纹理,也会有不同的效果。For Example:

c0a8f2d41e96eb6f6163fa97894e301d.png

b07a7a307e694d21ab02ccb014015476.png

最后,本文只是一个非常简单的unity水墨渲染,如果有错误,希望大家指正,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值