Unity仿Splatoon噴漆效果(一)
前言
專欄也開通了一個多月了,從業也好幾年了,從來也不知道寫點什麼。最近寫了幾篇文章,感覺寫文章有了一點心得,所以趁著在狀態再寫幾篇文章。
在上家公司的時候,16年開始轉型unity手遊開發,整個公司都沒有什麼經驗,當時splatoon大火,領導決定做個手機版本的splatoon, 領導把splatoon噴漆效果的實現交給我實現。
折騰出了splatoon的噴漆效果。專案做了幾個月就停了,後來團隊各奔東西。2年多了,想把以前做的噴漆效果分享出來。
這個不是好的解決方案,分享出來也就是想有專業的圖形程式設計師能給點改進的意見。
下面是這個Demo的影片
https://www。zhihu。com/video/1017888543019925504
Unity實現
(1)記錄碰撞資訊
當主角在場景中移動的時候,如果發射子彈,就從發射點向目標點發射一條射線,如果這條射線擊中地面,就把這個碰撞資訊記錄下來。
這個Demo中定義了一個叫HitObj的結構體資訊
public
class
HitObj
{
public
GameObject
obj
;
public
Vector2
texcoord
;
public
Color
color
;
public
int
coltype
;
public
string
decalname
=
“Gun_H100”
;
public
float
decalwidth
=
1。0f
;
public
float
decalheight
=
1。0f
;
public
float
decalrot
=
0。0f
;
};
當按住鍵盤空格鍵Space鍵的時候,會發射一條射線,射線擊中地面,就把這個碰撞資訊加入到陣列列表中。
public void CreatePaint()
{
int coltype = mUseTeam0 ? (int)xEnumDefine。TeamFlag。Team_0 : (int)xEnumDefine。TeamFlag。Team_1;
Color color = mUseTeam0 ? CustomColorData_0。Color_Team_0 : CustomColorData_0。Color_Team_1;
RaycastHit rayhit;
Ray ray = new Ray(mGunPos。position, -Vector3。up);
if (Physics。Raycast(ray, out rayhit, 10。0f))
{
CameraRender。HitObj obj = new CameraRender。HitObj();
obj。obj = rayhit。collider。gameObject;
obj。texcoord = rayhit。textureCoord2;
obj。coltype = coltype;
obj。color = color;
obj。decalname = “Gun_H100”;
obj。decalwidth = 2。5f;
obj。decalheight = 2。5f;
obj。decalrot = 0。0f;
CameraRender。Instance。M_StaticObjArray。Add(obj);
}
}
對於結構體的資訊
texcoord是擊中點的二次uv座標
顏色型別是程式碼中定義的,定義了兩種, 代表兩個陣營。
public enum TeamFlag
{
Invalid = -1,
Team_0 = 0,
Team_1 = 1,
}
顏色也對應了有兩種。
public class CustomColorData_0
{
public static Color Color_Team_0 = new Color(93 / 255。0f, 25 / 255。0f, 231 / 255。0f);
public static Color Color_Team_1 = new Color(233 / 255。0f, 106 / 255。0f, 173 / 255。0f);
}
decalname 就是使用的噴射的貼圖的名字。這邊測試用了一個“Gun_H100”
decalwidth和decalheight定義的是噴射的貼花尺寸
(2)攝像機後處理
對於程式碼中的CameraRender類,他應該掛在Camera元件下面,如圖所示。
只有掛載在Camera元件下,Unity的
OnPostRender
函式才會呼叫
OnPostRender
就是在攝像機完成渲染的時候呼叫,如圖所示
在OnPostRender函式中,對於收集的碰撞資訊,如果這個碰撞物體掛載了StaticObj_Render指令碼,則呼叫StaticObj_Render的Render函式渲染貼花。
然後再把陣列清空
void OnPostRender()
{
for (int i = 0; i < mStaticObjArray。Count; i++ )
{
HitObj obj = mStaticObjArray[i];
if (obj == null)
continue;
StaticObj_Render render = obj。obj。GetComponent
if (render == null)
continue;
render。Render(obj。texcoord, mQuadMaterial, obj。color, obj。coltype, obj。decalname, obj。decalwidth, obj。decalheight, obj。decalrot);
}
mStaticObjArray。Clear();
}
(3)掛載StaticObj_Render的物體渲染
對於場景中可渲染物體的說明
對於當前的Demo場景,美術在製作的時候分成了三部分。如圖所示
分別為不可渲染的外圍物件(外圍的修飾場景), 不可渲染的物件(主場景內的物件),可渲染的物件
對於可渲染的物件,如下圖所示, 每一個可渲染GameObject都掛載了StaticObj_Render指令碼
對於這些可渲染的GameObject, 美術在3dmax中製作的時候是需要按照一定的要求
斬二次uv
的。
StaticObj_Render的渲染
對於每一個StaticObj_Render都會建立一個RenderTexture
private int mWidth = 128;
private int mHeight = 128;
if (mDecalTex == null)
{
mWidth = PaintResourceManager。Instance。mDecalWidth;
mHeight = PaintResourceManager。Instance。mDecalHeight;
mDecalTex = new RenderTexture(mWidth, mHeight, 0);
mDecalTex。Create();
}
看看StaticObj_Render的Render函式
首先看看第一段, 當這個物件第一次被噴射的時候,替換shader, 然後設定貼花的法線圖,
環境貼圖。
// 使用新材質 。。。
if (!mNewMat)
{
for (int i = 0; i < mMats。Length; i++ )
{
Material mat = mMats[i];
if (mat == null)
continue;
SetMaterial(mat);
mat。SetTexture(“_DecalTex”, mDecalTex);
Texture _Normal = PaintResourceManager。Instance。GetDecalBumpTex(“T_Detail_Rocky_N”);
mat。SetTexture(“_DecalBump”, _Normal);
Cubemap cube = PaintResourceManager。Instance。GetCubemap(“SkyboxCube”);
mat。SetTexture(“_DecalSky”, cube);
}
mNewMat = true;
}
void SetMaterial(Material mat)
{
mat。shader = Shader。Find(“SpraySoldier/Mobile/StaticObjDecal”);
}
然後是下面一段,把當前的硬體的RenderTarget設定為這個Obj建立的RenderTexture, 這樣下面所有的DrawCall都會畫到這張RenderTexture上
Graphics。SetRenderTarget(mDecalTex);
再接下來一段, 這個函式也只調用一次,就是用GL函式畫一次全屏RT, 這個RT設定為白色。
(GL是unity提供的底層介面,用來繪製基礎圖元。)
void OnceRenderGlobalScreenRT(Material quadmat)
{
if (mOnceRenderGScnRT)
return;
quadmat。SetPass(1);
GL。PushMatrix();
GL。LoadOrtho();
GL。Begin(GL。QUADS);
GL。Vertex3(0, 0, 0);
GL。Vertex3(0, 1, 0);
GL。Vertex3(1, 1, 0);
GL。Vertex3(1, 0, 0);
GL。End();
GL。PopMatrix();
mOnceRenderGScnRT = true;
}
下一步就是主要的繪製函數了。透過傳入HitObj的資料,設定到材質中,然後用GL函式繪製出來。
PaintResourceManager。DecalInfo info = PaintResourceManager。Instance。GetDecalTex(decalname);
if (info。texture == null)
return;
quadmat。SetTexture(“_MainTex”, info。texture);
quadmat。SetColor(“_Color”, quadcolor);
quadmat。SetFloat(“_Rotation”, decalrot);
quadmat。SetVector(“_CenterPos”, texcoord);
float width = decalwidht / 20。0f;
float height = decalheight / 20。0f;
int decalRow = info。row;
int decalCol = info。column;
int randomx = Random。Range(0, decalRow - 1);
int randomy = Random。Range(0, decalCol - 1);
float unitx = (1。0f / decalRow);
float unity = (1。0f / decalCol);
float startuvx = unitx * randomx;
float startuvy = unity * randomy;
quadmat。SetPass(0);
GL。PushMatrix();
GL。LoadOrtho();
GL。Begin(GL。QUADS);
if (mUV)
GL。TexCoord2(startuvx, startuvy);
else
GL。TexCoord2(0, 0);
GL。Vertex3(texcoord。x - width, texcoord。y - height, 0);
if (mUV)
GL。TexCoord2(startuvx, startuvy + unity);
else
GL。TexCoord2(0, 1);
GL。Vertex3(texcoord。x - width, texcoord。y + height, 0);
if (mUV)
GL。TexCoord2(startuvx + unitx, startuvy + unity);
else
GL。TexCoord2(1, 1);
GL。Vertex3(texcoord。x + width, texcoord。y + height, 0);
if (mUV)
GL。TexCoord2(startuvx + unitx, startuvy);
else
GL。TexCoord2(1, 0);
GL。Vertex3(texcoord。x + width, texcoord。y - height, 0);
GL。End();
GL。PopMatrix();
上圖注意到,對於傳入的貼花貼圖,是否序列圖。如果是序列圖的話,會從圖中隨機取一塊影象。這樣做的好處是噴射貼花的形狀是變化的。(如下圖所示。)
再下步就是恢復攝像機的RenderTarget
Graphics。SetRenderTarget(Camera。main。targetTexture);
最後就是會把噴射資料寫入到一個數組中,這樣可以在邏輯判斷的時候,就可以透過資料,而不是透過影象判斷腳下的這個點是否在貼圖中(就如Splatoon一樣,是否需要扣血,是否可以潛行。)
public int mSimuBufWidth = 200; // 模擬rendertexture的畫素資訊
public int mSimuBufHeight = 200; // 模擬rendertexture的畫素資訊
private List
private void WriteSimuBufData(float decalwid, float decalhei, Vector2 decaluv, int coltype)
{
int centerx = (int)(decaluv。x * mSimuBufWidth);
int centery = (int)(decaluv。y * mSimuBufHeight);
int width = (int)(decalwid * mSimuBufWidth);
int height = (int)(decalhei * mSimuBufHeight);
for (int j = (centery - height); j < (centery + height); j++)
{
for(int i = (centerx - width); i < (centerx + width); i++)
{
if (i < 0)
continue;
if (i > (mSimuBufWidth - 1))
continue;
if (j < 0)
continue;
if (j > (mSimuBufHeight - 1))
continue;
int pos = ((mSimuBufHeight - 1) - j) * mSimuBufWidth + i;
if (pos > (mSimuBufWidth * mSimuBufHeight - 1))
continue;
mSimuBuf[pos] = coltype;
}
}
(4)貼圖shader的實現
對於視屏中貼花表面的效果的實現,這裡我想說一句,當初的實現是比較暴力的。當初是美術在Shader Forge中調出這樣的效果,然後透過生成的程式碼,把實現效果部份的程式碼,複製到貼花shader中,使貼花也有相應的效果。
// diffuse :
loat
NdotL
=
dot
(
normalDirection
,
lightDirection
);
float3
diffuse
=
max
(
0。0
,
NdotL
)
*
attenColor
+
UNITY_LIGHTMODEL_AMBIENT
。
rgb
;
// gloss :
float
gloss
=
_DecalGloss
;
float
specPow
=
exp2
(
gloss
*
10。0
+
1。0
);
// specular :
NdotL
=
max
(
0。0
,
NdotL
);
float4
node_74
=
texCUBE
(
_DecalSky
,
viewReflectDirection
);
float3
specularColor
=
float3
(
_DecalSpecular
,
_DecalSpecular
,
_DecalSpecular
);
float3
specularAmb
=
(
node_74
。
rgb
*
node_74
。
a
*
_DecalSpecAmbient
)
*
specularColor
;
看過程式碼會發現,最初的物件Shader和替換後的噴漆Shader, 都是用頂點/片源著色器寫的。當時想用Surface Shader來寫的,設想Surface Shader只要實現一個顏色函式就可以了。但是發現美術除錯的效果,沒法很好的整合進Surface Shader中(可能是理解的不夠好。), 所以為了有lightmap, 和燈光效果,重寫了內建Shader(Mobile/Diffuse和Mobile/Bumped Diffuse), 使用自定義的頂點/片源著色器,就可以整合進Shader Forge的效果了。
(5)關於二次uv
開啟3dmax, 找到渲染物件區域。
可以看到,一個整塊的地板被切割成很多小塊。這樣做是為了斬二次uv的需要,定義了一個基礎的尺寸,例如圖中地形塊的一部分,那麼大的模型他的二次uv是全斬的。以這個模型大小為基準來斬其他模型的二次uv。
專案github地址
https://
github。com/xieliujian/U
nityDemo_Splatoon2
關於這個實現的幾點不足
(1)邊緣效果處理不好。
如下圖所示, 在把Decal影象畫到RenderTexture上的時候,沒有處理好邊緣顏色的溢位現象。
(2)每一個噴射會有切邊,在物件於物件的交界處。
如圖所示,理論上應該是一個完整的形狀,(透過查詢邊緣的塊,這個應該可以在邏輯層解決這個問題。)
(3)RenderTexture使用太多了。
如下圖所示,有多少個可渲染物件,就要建立個多少個RenderTexture
展望
收藏了一篇關於splatoon的文章,裡面有提到splatoon的技術實現,還有Unity商城有牛B的外掛。
破曉:《Splatoon 2》繪製效果的簡單實現
可能有時間研究的話,希望再深入研究下Splatoon噴漆實現。