Unity 實現(xiàn)貼花效果的制作教程
一、前言
在云艾爾登法環(huán)時,看到地面上的血跡時,發(fā)現(xiàn)某些地方脫離的地面,似乎是通過面片的方式實現(xiàn)的效果。但是同時某些,不過這種類型的血跡有道具的效果,估計是為了實現(xiàn)碰撞檢測的功能才選擇了面片的方式
而其他的戰(zhàn)斗痕跡的效果似乎是通過貼花來實現(xiàn)的,貼花的方式多種多樣。而在Unity中,有一種給官方文檔提供代碼的解決方案。這里就在這些代碼的基礎上做一個繪圖的貼花效果,最終效果如圖所示:

二、實現(xiàn)方式介紹
簡單的來說就是通過發(fā)射一條射線與物體發(fā)生碰撞來獲取物體的基本的信息,然后提取出碰撞處該物體的UV坐標點,然后進行一個計算得到物體對應Texture2D的像素信息,然后對這些像素進行一個顏色的替換,最后就可以得到一張貼花效果的Texture2D
這種方式的第一步就是需要通過發(fā)射一條射線,然后得到碰撞檢測點的信息,這里用到的API為:

使用該API的返回結(jié)果是物體網(wǎng)格對應的UV坐標點,沒有辦法直接的去使用,需要先通過坐標轉(zhuǎn)換,即通過UV坐標來獲取到其Texture2D對應的像素點。在Unity中,我們知道UV坐標對應的范圍為0到1,這樣來說,只要將其與對應Texture2D的像素數(shù)量與UV坐標進行一個乘法計算就可以得到最后對應像素的下標位置
在得到檢測位置的像素下表后,就可以根據(jù)被貼圖的Texture2D的像素的寬高做一個計算,得到物體貼圖的替換范圍與下標,然后執(zhí)行一遍遍歷,對于所有替換的像素顏色一一對應,然后執(zhí)行一個像素顏色的計算,做一個混合即可
三、實現(xiàn)過程
檢測UV位置并替換像素顏色:
首先查閱Unity官方文檔,得到射線檢測UV坐標的代碼,核心圍繞RaycastHit對應的API來得到檢測的UV坐標并進行處理,代碼如下:
public class ExampleClass : MonoBehaviour
{
public Camera cam;
void Start()
{
cam = GetComponent<Camera>();
}
void Update()
{
if (!Input.GetMouseButton(0))
return;
RaycastHit hit;
if (!Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out hit))
return;
Renderer rend = hit.transform.GetComponent<Renderer>();
MeshCollider meshCollider = hit.collider as MeshCollider;
if (rend == null || rend.sharedMaterial == null || rend.sharedMaterial.mainTexture == null || meshCollider == null)
return;
Texture2D tex = rend.material.mainTexture as Texture2D;
Vector2 pixelUV = hit.textureCoord;
pixelUV.x *= tex.width;
pixelUV.y *= tex.height;
tex.SetPixel((int)pixelUV.x, (int)pixelUV.y, Color.black);
tex.Apply();
}
}
然后在場景中創(chuàng)建一個Quad作為射線被檢測的物體,但是同時需要注意,對于物體執(zhí)行操作時,需要理解一個細節(jié),就是物體只有在掛載網(wǎng)格碰撞體時候,才能夠獲取到對應物體的UV信息,具體的細節(jié)在官方文檔中也有提到,如圖:

創(chuàng)建完成物體后,需要通過一個材質(zhì)來賦予該物體一張貼圖,用來作為像素替換的貼圖,我這里用了一張白色的圖片,但是注意,在使用該圖片時候,注意修改該圖片的導入設置中的Read/Write Enabled為開啟狀態(tài),這樣才可以進行后續(xù)的修改:

如果你測試這段代碼,可能發(fā)現(xiàn)在點擊后并沒有發(fā)生什么變化,因為這一段代碼只會對一個像素點執(zhí)行替換操作,運行效果看起來并不明顯。為了提升顯示效果,這里可以先做一個簡單的計算,來設計一個像素塊作為替換的基本單元,以便于結(jié)果的觀察。而計算方式為通過這個像素點的下表位置來計算出一個大小合適的方格區(qū)域,定義一個Vector2的屬性,命名為replaceRange,然后修改像素替換區(qū)域的代碼:
for (int i = 0; i < replaceRange.x; i++)
{
for (int j = 0; j < replaceRange.y; j++)
{
tex.SetPixel((int)pixelUV.x+i- (int)replaceRange.x/2, (int)pixelUV.y+j-(int)replaceRange.y/2, Color.black);
}
}
然后運行整個場景,如果腳本執(zhí)行成功的話,就可以看到正確的顯示效果:

修改替換信息為圖片信息:
上面我們對于每一個像素的顏色值進行替換時,使用的是指定的顏色數(shù)字。接下來就需要進行一定的擴展,將信息的提取方式修改為圖片提取的方式。
同樣定義一個Texture2D屬性命名為:coverTex,然后提取這張Texture2D的信息,并覆蓋掉對應點擊點的像素信息,這里定義一個Draw方法來單獨的處理這件事情:
public void Draw(Texture2D orginTex,Texture2D coverTex,Vector2 pixelUV)
{
for (int i = 0; i < coverTex.width; i++)
{
for (int j = 0; j < coverTex.height; j++)
{
Color colorOriginal = orginTex.GetPixel((int)pixelUV.x + i - (int)coverTex.width / 2, (int)pixelUV.y + j - (int)coverTex.height / 2);
Color colorCover = coverTex.GetPixel(i, j);
Color colorResult = colorCover * colorCover.a + (1 - colorCover.a) * colorOriginal;
orginTex.SetPixel((int)pixelUV.x + i - (int)coverTex.width / 2, (int)pixelUV.y + j - (int)coverTex.height / 2, colorResult);
}
}
}
注意,在上面的代碼中,我們對于兩個顏色值有一個簡單的計算用來混合兩個有透明通道的顏色值。假設顏色A要覆蓋掉顏色B,這里使用的計算公式為:
A*A.a+(1-A.a)*B
通過上面的公式,可以簡單的處理兩個顏色的a通道的覆蓋結(jié)果,也許這種方式不是很準確,但是對于完全透明的像素或者完全不透明的像素的混合還是比較有效的,這樣就很方便 的處理不規(guī)則形狀的貼花
將上面的顏色快的方式替換掉,可以觀察一下效果:

當我們使用到一張圓形的貼圖后,我們就可以看到成功的執(zhí)行了替換
運行時使用復制貼圖:
如果我們直接使用本體的貼圖來修改材質(zhì),就會發(fā)現(xiàn)本地的資源也被執(zhí)行了修改,這樣會造成下次進入游戲,整個貼圖的狀態(tài)也不會刷新。為了避免這個問題,可以在每次執(zhí)行像素替換時,復制一份貼圖來作為被貼畫的材質(zhì)貼圖,不過這里就不進行演示,可以在自己的項目中,根據(jù)需要來決定是否執(zhí)行該操作
修改幀檢測斷觸問題:
上面的一個代碼,有一個特點,就是可以通過連續(xù)的繪制來做出一些圖案,有一些類似于江南白景圖游戲中抽卡前的繪制效果,但是通過上面的代碼來實現(xiàn)時,就會發(fā)現(xiàn)如果鼠標移動的過快,相鄰的兩個繪制點之間會產(chǎn)生空隙,如圖所示:

為了解決這樣一個問題,這里在每一幀執(zhí)行繪制之后都緩存本幀的UV坐標,同時在繪制時與上一幀的UV坐標進行距離對比,如果超出一定的距離。就在中間執(zhí)行插值的操作
同時為了保證性能,需要固定距離的執(zhí)行插值操作,為了簡化計算,將兩幀坐標的距離分為X與Y方向分別進行判斷,同時為了保證斜率,得到最大偏差的方向進行等距的插值,具體的邏輯代碼為:
if (!isClick)
{
Draw(tex, coverTex, pixelUV);
catchPos = pixelUV;
isClick = true;
}
else
{
if (Vector2.Distance(pixelUV, catchPos) < coverTex.width / 4)
{
Draw(tex, coverTex, pixelUV);
}
else
{
Vector2 pixelCatchUV = catchPos;
float lerpNum=0;
float interval = 1 / (Mathf.Max(Mathf.Abs(pixelUV.x - pixelCatchUV.x), Mathf.Abs(pixelUV.y - pixelCatchUV.y)) / (coverTex.width/4));
while (lerpNum<=1)
{
lerpNum += interval;
catchPos = Vector2.Lerp(pixelCatchUV, pixelUV, InterpolationCalculation(lerpNum));
Draw(tex, coverTex, catchPos);
}
catchPos = pixelUV;
Draw(tex, coverTex, catchPos);
}
}
執(zhí)行代碼的顯示結(jié)果為:

總結(jié)
從實現(xiàn)過程中面臨的一些問題來看,這種貼畫效果的實現(xiàn)限制條件很多,性能表現(xiàn)上也是比較差的,適合做一些局部的貼畫效果實現(xiàn),比如百景圖的抽卡繪制的效果
而若想實現(xiàn)全局的效果,在UV平鋪方面與貼圖的緩存方面都有很大的挑戰(zhàn),還是建議嘗試一下其他方式,最后,貼上完整的代碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
public Camera cam;
public Texture2D coverTex;
private Texture2D catchTexture;
private Vector2 catchPos;
private bool isFirst=true;
private bool isClick = false;
void Awake()
{
Application.targetFrameRate = 200;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
isFirst = true;
}
DrawSticker();
}
public void DrawSticker()
{
if (!Input.GetMouseButton(0))
{
isClick = false;
return;
}
RaycastHit hit;
if (!Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out hit))
return;
Renderer rend = hit.transform.GetComponent<Renderer>();
MeshCollider meshCollider = hit.collider as MeshCollider;
if (rend == null || rend.sharedMaterial == null || rend.sharedMaterial.mainTexture == null || meshCollider == null)
return;
if (isFirst)
{
if (catchTexture == null)
{
catchTexture = rend.material.mainTexture as Texture2D;
}
rend.material.mainTexture = Instantiate(catchTexture);
isFirst = false;
}
Texture2D tex = rend.material.mainTexture as Texture2D;
Vector2 pixelUV = hit.textureCoord;
pixelUV.x *= tex.width;
pixelUV.y *= tex.height;
if (!isClick)
{
Draw(tex, coverTex, pixelUV);
catchPos = pixelUV;
isClick = true;
}
else
{
if (Vector2.Distance(pixelUV, catchPos) < coverTex.width / 4)
{
Draw(tex, coverTex, pixelUV);
}
else
{
Vector2 pixelCatchUV = catchPos;
float lerpNum=0;
float interval = 1 / (Mathf.Max(Mathf.Abs(pixelUV.x - pixelCatchUV.x), Mathf.Abs(pixelUV.y - pixelCatchUV.y)) / (coverTex.width/4));
while (lerpNum<=1)
{
lerpNum += interval;
catchPos = Vector2.Lerp(pixelCatchUV, pixelUV, InterpolationCalculation(lerpNum));
Draw(tex, coverTex, catchPos);
}
catchPos = pixelUV;
Draw(tex, coverTex, catchPos);
}
}
tex.Apply();
}
float InterpolationCalculation(float num)
{
return 3 * Mathf.Pow(num, 2) - 2 * Mathf.Pow(num, 3);
}
public void Draw(Texture2D orginTex,Texture2D coverTex,Vector2 pixelUV)
{
for (int i = 0; i < coverTex.width; i++)
{
for (int j = 0; j < coverTex.height; j++)
{
Color colorOriginal = orginTex.GetPixel((int)pixelUV.x + i - (int)coverTex.width / 2, (int)pixelUV.y + j - (int)coverTex.height / 2);
Color colorCover = coverTex.GetPixel(i, j);
Color colorResult = colorCover * colorCover.a + (1 - colorCover.a) * colorOriginal;
orginTex.SetPixel((int)pixelUV.x + i - (int)coverTex.width / 2, (int)pixelUV.y + j - (int)coverTex.height / 2, colorResult);
}
}
}
}
以上就是Unity 實現(xiàn)貼花效果的制作教程的詳細內(nèi)容,更多關于Unity的資料請關注腳本之家其它相關文章!
相關文章
C#實現(xiàn)Windows Form調(diào)用R進行繪圖與顯示的方法
眾所周知R軟件功能非常強大,可以很好的進行各類統(tǒng)計,并能輸出圖形。下面介紹一種R語言和C#進行通信的方法,并將R繪圖結(jié)果顯示到WinForm UI界面上的方法,文中介紹的很詳細,需要的朋友可以參考下。2017-02-02
Winform項目中TextBox控件DataBindings屬性
這篇文章介紹了Winform項目中TextBox控件DataBindings屬性的用法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02

