
前言
你是否曾想在 Unity 中輕鬆實現中國水墨風格的畫面渲染?傳統的渲染技術難以模擬水墨的【流動感】、【暈染效果】、【筆觸質感】,這讓許多開發者感到頭疼!不過今天要介紹的 ChinesePainting 水墨渲染大師正是為了解決這個問題而生!
本篇文章將帶你深入了解 ChinesePainting 的強大功能,包括【筆觸模擬】、【流動渲染】、【Shader 調整】,並結合實際範例,讓你快速上手這款插件,如果你正在尋找一款能夠完美呈現水墨風格的插件,那麼千萬不要錯過這次的介紹!

Unity ChinesePainting 水墨風插件介紹
ChinesePainting 是一款專為 Unity 設計的 水墨風渲染插件,旨在將中國傳統水墨畫風格融入現代遊戲開發中,透過自訂 Shader 來實現【潑墨】、【暈染】、【筆觸流動】等中國傳統水墨畫效果,開發者可以在虛擬世界中重現中國畫的獨特韻味,為遊戲增添濃厚的東方藝術氛圍。

功能特色
- 輪廓線渲染:
透過擴展模型的法線,實現類似毛筆筆觸的輪廓線效果,增強物體的邊緣表現力。 - 內部著色:
採用光照模型結合漸變貼圖(ramp texture),控制顏色的層次變化,模擬水墨畫中濃淡相間的效果。 - 多重 Pass 渲染:
利用【噪聲貼圖】和【筆觸紋理】,對模型表面進行擾動處理,增加隨機性,使渲染效果更接近手繪風格。
適用場景
- 武侠冒险游戏:
在以古代武俠為背景的遊戲中,水墨風格能營造出濃厚的江湖氛圍,增強玩家的代入感。 - 中國歷史類遊戲
描繪中國【歷史事件】或【傳說】的遊戲中,水墨風格可呈現獨特的文化質感,使玩家更深入地體驗歷史情境。 - 仙俠類遊戲
在以【神話】或【仙俠】為題材的遊戲中,水墨風格能夠展現出【夢幻】且【神秘】的世界,提升遊戲的藝術表現力。

Unity ChinesePainting 水墨風插件導讀
中國水墨畫的渲染效果是很久很久以前就有的方法,基本想法就是分成兩個部分,輪廓線渲染和內部渲染。輪廓線通常是渲染成毛筆筆觸的感覺,內部則是透過普通的光照方程式再加上 ramp 貼圖控制一下漸變紋理,最後用一些模糊處理。這也是基本的卡通渲染方法。
而使用 Unity 進行卡通渲染的基本思想,作者馮樂樂已經在《Unity Shader 入門精要》裡解釋得非常完整,我就不添亂了,上鏈接(樂樂姐的卡通渲染)。同時本文也參考了知乎上兩位大佬的 Unity 實作方法(在 Unity 中如何進行水墨風 3D 渲染 &【Unity Shader】 水墨風格渲染:如何優雅的畫一隻猴子)。

Unity ChinesePainting 水墨風輪廓線 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)進行採樣,這樣又個好處就是隨機出來的輪廓不會隨著視角的改變而改變。

其中稍微有點改變的是,增加了一個 linewidth 的操作,因為 unity_CameraProjection[1].y 其實就是 cos(FOV/2),所以這個操作的根本目的是為了確保輪廓線隨著 FOV 的變換也是成一定比例,同時也不會隨著鏡頭離物體的遠近距離而變換。
對比圖片如下:


最後一個小 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;
}
對比圖片如下:


Unity ChinesePainting 水墨風內部渲染
而內部著色的基本思想和 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 範例:


最後,本文只是一個非常簡單的 Unity 水墨渲染,如果有錯誤,希望大家指正,謝謝~
Unity ChinesePainting 水墨渲染大師相關網站 & 插件下載點
【ChinesePainting】
GitHub 下載連結:ChinesePainting
————————————————
更多好用插件:【Unity 好用插件推薦】持續更新,一起讓遊戲開發事半功倍!
本文原創(或整理)於亞洲電玩通,未經作者與本站同意不得隨意引用、轉載、改編或截錄。
特約作家簡介
支持贊助 / DONATE
亞洲電玩通只是很小的力量,但仍希望為復甦台灣遊戲研發貢獻一點動能,如果您喜歡亞洲電玩通的文章,或是覺得它們對您有幫助,歡迎給予一些支持鼓勵,不論是按讚追蹤或是贊助,讓亞洲電玩通持續產出,感謝。
BTC |
![]() |
352Bw8r46rfXv6jno8qt9Bc3xx6ptTcPze |
|
ETH |
![]() |
0x795442E321a953363a442C76d39f3fbf9b6bC666 |
|
TRON |
![]() |
TCNcVmin18LbnXfdWZsY5pzcFvYe1MoD6f |