最近在项目中优化粒子特效,发现Unity的粒子特效消耗非常高就想着能不能优化。
发现用ComputeShader来计算的话效率会非常非常的高,因为粒子的轨迹运算都是在GPU中去进行的,大大降低了drawcall,效率也大大提升了。
本项目参考了B站某UP的文章:https://www.bilibili.com/read/cv3356979?from=search,在这里十分感谢。
项目仓库地址:https://github.com/Kirkice/ComputeShaderDemo
这里的粒子总数目达到100万,帧率可以维持在30fps左右,drawcall只有2,不敢想象如果是用普通的粒子特效来做会是什么样的。
ComputeShader可以用做通用计算,也就是gpu负责主要的计算过程,最后再将结果传递给cpu,这类非图形计算被称为GPGPU。ComputeShader虽然位于渲染流水线之外,但它支持读写GPU资源,我们可以将运行结果直接传递到渲染管线,从而来完成一些图形处理的效果,这样就没有了从显存到内存的时间开销(此类过程速度很慢)。
也就是说CPU将需要计算的粒子参数传给GPU来计算,由于GPU是用平行架构处理大量的并行数据,所以大量并行无序数据的少分支逻辑(少if)适合GPGPU,将这些复杂的轨迹运算丢给GPU效率会比CPU大得多得多。
申明部分
粒子的结构体
struct Particle
{
public Vector3 pos; //起始位置
public Vector3 newPos; //更新位置
}
一些全局变量
int ThreadBlockSize = 256; //线程组大小
int blockPerGrid; //每个组
ComputeBuffer ParticleBuffer, argsBuffer;
private uint[] _args;
private int number; //粒子数目
public int width, height; //设置长宽范围
public int interval; //间隔距离
public float randomDegree; //随机角度
public float radius_r, radius_R, length_h; //圆台的小半径、大半径、长度
[SerializeField]
private Mesh Particle_Mesh; //粒子网格
[SerializeField]
ComputeShader _computeShader; //computeshader
[SerializeField]
private Material _material; //粒子材质
在C#脚本中我们会用到一个 Dispatch 函数,这个函数就是定义线程组的数量。
在二维中,如果图片的解析度是(256*128),那么Dispatch和numthreads的分配可以如下:
(但是numthreads有上限,numthreads.x * numthreads.y * numthreads.z 必须小于等于 1024)
PS:关于Group 与 Thread 的关系如下图
假设有一张 4X4的图像,使用参数 Dispatch(k, 2, 2, 1) 与 [numthreads(2, 2, 1)]
Group 的 长、宽、高 是由 numthreads 所设定的,同理一个Group的大小是不能超过 1024 。
同理 [numthreads(32, 32, 1)] 的 Group 大小就是 32 * 32 * 1 ( 32 * 32 像素 )
然而如果你的 GPU 有 64 核心,而你的 Group 也有 64 个,那么每个核心将能处理一个 Group。
假设 ThreadSize = ( numthreads.x * numthreads.y * numthreads.z )
那么不同的 ThreadSize 在不同硬件厂下分配的资源是不同的
建议:
AMD:ThreadSize 使用 64 的倍数 ( wavefront 架构 )
NVIDIA:ThreadSize 使用 32 的倍数 ( SIMD32 (Warp) 架构 )
在这个阶段,设置ComputeBuffer、设置粒子的位置然后setdata。
private void Start()
{
number = width * height;
randomDegree = Random.Range(1, 359); //随机一个1-359的度数
Particle[] particles = new Particle[number]; //创建粒子数组
blockPerGrid = (number + ThreadBlockSize - 1) / ThreadBlockSize;
ParticleBuffer = new ComputeBuffer(number, 24); //创建第一个ComputeBuffer 6*4 ----> 24
_args = new uint[5] { 0, 0, 0, 0, 0 };
argsBuffer = new ComputeBuffer(1, _args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
//粒子的开始位置设0
for (int i = 0; i < width; ++i) //遍历设置粒子位置
{
for (int j = 0; j < height; ++j)
{
int id = i * height + j;
float x = (float)i / (width - 1);
float y = (float)j / (height - 1);
particles[id].pos = new Vector3((x * interval), (y * interval), y * interval);
particles[id].newPos = new Vector3((x * interval), (y * interval), y * interval);
}
}
//setdata
ParticleBuffer.SetData(particles);
}
在Update中去更新ComputeShader中的传递数据。
private void Update()
{
randomDegree = Random.Range(1, 359);
UpdateComputeShader();
argsBuffer.SetData(_args);
Graphics.DrawMeshInstancedIndirect(Particle_Mesh, 0, _material, new Bounds(Vector3.zero, new Vector3(100f, 100f, 100f)), argsBuffer);
}
C#将需要传递给GPU运算的数据传递给ComputeShader
private void UpdateComputeShader()
{
int kernelId = _computeShader.FindKernel("CSMain");
_computeShader.SetFloat("_deltaTime", Time.deltaTime);
_computeShader.SetFloat("_radius_r", radius_r);
_computeShader.SetFloat("_radius_R", radius_R);
_computeShader.SetFloat("_length_h", length_h);
_computeShader.SetFloat("_randomDegree", randomDegree);
_computeShader.SetBuffer(kernelId, "_ParticleBuffer", ParticleBuffer);
_computeShader.Dispatch(kernelId, blockPerGrid, 1, 1);
_args[0] = (uint)Particle_Mesh.GetIndexCount(0);
_args[1] = (uint)number;
_args[2] = (uint)Particle_Mesh.GetIndexStart(0);
_args[3] = (uint)Particle_Mesh.GetBaseVertex(0);
_material.SetBuffer("_ParticleBuffer", ParticleBuffer);
_material.SetMatrix("_GameobjectMatrix", this.transform.localToWorldMatrix);
}
对应的一些参数
struct Particle
{
float3 pos; //起始位置
float3 newPos; //更新位置
};
RWStructuredBuffer<Particle> _ParticleBuffer;
float _deltaTime;
float _radius_r;
float _radius_R;
float _length_h;
float _randomDegree;
CSMain
这一部分是轨迹运算部分,噪点运算是用的网络的噪点算法,在结尾部分回帖出源码。
然后就是计算粒子的运动范围,是一个圆台,大小圆半径以及高度是由C#传过来的,在每一帧会计算当前粒子所在位置的截面圆的半径,粒子超过这个半径就重新计算粒子的位置,我采用随机算法是通过C#传递一个随机1-359的度数值,然后将坐标用参数方程来表现。
计算完位置之后,直接交给shader来渲染粒子。
[numthreads(256, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
float3 position = _ParticleBuffer[id.x].pos;
_ParticleBuffer[id.x].pos += curlNoise(position) * 0.2; //随机位置
//_ParticleBuffer[id.x].pos.x += _deltaTime * 0.1; //位移
//形状为圆台的范围
float posDis = sqrt((_ParticleBuffer[id.x].pos.y * _ParticleBuffer[id.x].pos.y) + (_ParticleBuffer[id.x].pos.z * _ParticleBuffer[id.x].pos.z));
float rangeDis = ((_radius_R - _radius_r) / _length_h) * _ParticleBuffer[id.x].pos.x;
float multNum = rangeDis - posDis;
if (multNum < 0)
{
_ParticleBuffer[id.x].pos = float3(_ParticleBuffer[id.x].pos.x, rangeDis * sin(_randomDegree), rangeDis * cos(_randomDegree));
}
if (_ParticleBuffer[id.x].pos.x > _length_h)
{
_ParticleBuffer[id.x].pos = float3(1, 0.1 * sin(_randomDegree), 0.1 * cos(_randomDegree));
}
}
属性部分:
[HDR]_Color("Color",color) = (1,1,1,1)
_MainTex("_MainTex", 2D) = "white" {}
_Size("Size",float) = 1.6
[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", Float) = 4
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull Mode", Float) = 0
这里用了个矩阵将粒子转到计算出来的坐标位置和大小
float4x4 GetModelToWorldMatrix(float3 pos)
{
float4x4 transformMatrix = float4x4(
_Size,0,0,pos.x,
0,_Size,0,pos.y,
0,0,_Size,pos.z,
0,0,0,1
);
return transformMatrix;
}
C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class gpuComputeShader : MonoBehaviour
{
struct Particle
{
public Vector3 pos; //起始位置
public Vector3 newPos; //更新位置
}
int ThreadBlockSize = 256; //线程组大小
int blockPerGrid; //每个组
ComputeBuffer ParticleBuffer, argsBuffer;
private uint[] _args;
private int number; //粒子数目
public int width, height; //设置长宽范围
public int interval; //间隔距离
public float randomDegree; //随机角度
public float radius_r, radius_R, length_h; //圆台的小半径、大半径、长度
[SerializeField]
private Mesh Particle_Mesh; //粒子网格
[SerializeField]
ComputeShader _computeShader; //申明computeshader
[SerializeField]
private Material _material; //粒子材质
private void Start()
{
number = width * height;
randomDegree = Random.Range(1, 359); //随机一个1-359的度数
Particle[] particles = new Particle[number]; //创建粒子数组
blockPerGrid = (number + ThreadBlockSize - 1) / ThreadBlockSize;
ParticleBuffer = new ComputeBuffer(number, 24); //创建第一个ComputeBuffer 6*4 ----> 24
_args = new uint[5] { 0, 0, 0, 0, 0 };
argsBuffer = new ComputeBuffer(1, _args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
//粒子的开始位置设0
for (int i = 0; i < width; ++i) //遍历设置粒子位置
{
for (int j = 0; j < height; ++j)
{
int id = i * height + j;
float x = (float)i / (width - 1);
float y = (float)j / (height - 1);
particles[id].pos = new Vector3((x * interval), (y * interval), y * interval);
particles[id].newPos = new Vector3((x * interval), (y * interval), y * interval);
}
}
//setdata
ParticleBuffer.SetData(particles);
}
private void Update()
{
randomDegree = Random.Range(1, 359);
UpdateComputeShader();
argsBuffer.SetData(_args);
Graphics.DrawMeshInstancedIndirect(Particle_Mesh, 0, _material, new Bounds(Vector3.zero, new Vector3(100f, 100f, 100f)), argsBuffer);
}
private void UpdateComputeShader()
{
int kernelId = _computeShader.FindKernel("CSMain");
_computeShader.SetFloat("_deltaTime", Time.deltaTime);
_computeShader.SetFloat("_radius_r", radius_r);
_computeShader.SetFloat("_radius_R", radius_R);
_computeShader.SetFloat("_length_h", length_h);
_computeShader.SetFloat("_randomDegree", randomDegree);
_computeShader.SetBuffer(kernelId, "_ParticleBuffer", ParticleBuffer);
_computeShader.Dispatch(kernelId, blockPerGrid, 1, 1);
_args[0] = (uint)Particle_Mesh.GetIndexCount(0);
_args[1] = (uint)number;
_args[2] = (uint)Particle_Mesh.GetIndexStart(0);
_args[3] = (uint)Particle_Mesh.GetBaseVertex(0);
_material.SetBuffer("_ParticleBuffer", ParticleBuffer);
_material.SetMatrix("_GameobjectMatrix", this.transform.localToWorldMatrix);
}
}
ComputeShader
#pragma kernel CSMain
#include "SimplexNoise3D.cginc"
struct Particle
{
float3 pos; //起始位置
float3 newPos; //更新位置
};
RWStructuredBuffer<Particle> _ParticleBuffer;
float _deltaTime;
float _radius_r;
float _radius_R;
float _length_h;
float _randomDegree;
float nrand(float2 uv)
{
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}
uint rng_state;
uint rand_xorshift()
{
rng_state ^= (rng_state << 13);
rng_state ^= (rng_state >> 17);
rng_state ^= (rng_state << 5);
return rng_state;
}
float3 snoiseVec3(float3 x) {
float s = snoise(x);
float s1 = snoise(float3(x.y - 19.1, x.z + 33.4, x.x + 47.2));
float s2 = snoise(float3(x.z + 74.2, x.x - 124.5, x.y + 99.4));
float3 c = float3(s, s1, s2);
return c;
}
float3 curlNoise(float3 p) {
const float e = .01;
float3 dx = float3(e, 0.0, 0.0);
float3 dy = float3(0.0, e, 0.0);
float3 dz = float3(0.0, 0.0, e);
float3 p_x0 = snoiseVec3(p - dx);
float3 p_x1 = snoiseVec3(p + dx);
float3 p_y0 = snoiseVec3(p - dy);
float3 p_y1 = snoiseVec3(p + dy);
float3 p_z0 = snoiseVec3(p - dz);
float3 p_z1 = snoiseVec3(p + dz);
float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y;
float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z;
float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x;
const float divisor = 1.0 / (2.0 * e);
return normalize(float3(x, y, z) * divisor);
}
[numthreads(256, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
float3 position = _ParticleBuffer[id.x].pos;
_ParticleBuffer[id.x].pos += curlNoise(position) * 0.2; //随机位置
//_ParticleBuffer[id.x].pos.x += _deltaTime * 0.1; //位移
//形状为圆台的范围
float posDis = sqrt((_ParticleBuffer[id.x].pos.y * _ParticleBuffer[id.x].pos.y) + (_ParticleBuffer[id.x].pos.z * _ParticleBuffer[id.x].pos.z));
float rangeDis = ((_radius_R - _radius_r) / _length_h) * _ParticleBuffer[id.x].pos.x;
float multNum = rangeDis - posDis;
if (multNum < 0)
{
_ParticleBuffer[id.x].pos = float3(_ParticleBuffer[id.x].pos.x, rangeDis * sin(_randomDegree), rangeDis * cos(_randomDegree));
}
if (_ParticleBuffer[id.x].pos.x > _length_h)
{
_ParticleBuffer[id.x].pos = float3(1, 0.1 * sin(_randomDegree), 0.1 * cos(_randomDegree));
}
}
Noise计算
//
// Noise Shader Library for Unity - https://github.com/keijiro/NoiseShader
//
// Original work (webgl-noise) Copyright (C) 2011 Ashima Arts.
// Translation and modification was made by Keijiro Takahashi.
//
// This shader is based on the webgl-noise GLSL shader. For further details
// of the original shader, please see the following description from the
// original source code.
//
//
// Description : Array and textureless GLSL 2D/3D/4D simplex
// noise functions.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : ijm
// Lastmod : 20110822 (ijm)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
//
float3 mod289(float3 x)
{
return x - floor(x / 289.0) * 289.0;
}
float4 mod289(float4 x)
{
return x - floor(x / 289.0) * 289.0;
}
float4 permute(float4 x)
{
return mod289((x * 34.0 + 1.0) * x);
}
float4 taylorInvSqrt(float4 r)
{
return 1.79284291400159 - r * 0.85373472095314;
}
float snoise(float3 v)
{
const float2 C = float2(1.0 / 6.0, 1.0 / 3.0);
// First corner
float3 i = floor(v + dot(v, C.yyy));
float3 x0 = v - i + dot(i, C.xxx);
// Other corners
float3 g = step(x0.yzx, x0.xyz);
float3 l = 1.0 - g;
float3 i1 = min(g.xyz, l.zxy);
float3 i2 = max(g.xyz, l.zxy);
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
float3 x1 = x0 - i1 + C.xxx;
float3 x2 = x0 - i2 + C.yyy;
float3 x3 = x0 - 0.5;
// Permutations
i = mod289(i); // Avoid truncation effects in permutation
float4 p =
permute(permute(permute(i.z + float4(0.0, i1.z, i2.z, 1.0))
+ i.y + float4(0.0, i1.y, i2.y, 1.0))
+ i.x + float4(0.0, i1.x, i2.x, 1.0));
// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
float4 j = p - 49.0 * floor(p / 49.0); // mod(p,7*7)
float4 x_ = floor(j / 7.0);
float4 y_ = floor(j - 7.0 * x_); // mod(j,N)
float4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0;
float4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0;
float4 h = 1.0 - abs(x) - abs(y);
float4 b0 = float4(x.xy, y.xy);
float4 b1 = float4(x.zw, y.zw);
//float4 s0 = float4(lessThan(b0, 0.0)) * 2.0 - 1.0;
//float4 s1 = float4(lessThan(b1, 0.0)) * 2.0 - 1.0;
float4 s0 = floor(b0) * 2.0 + 1.0;
float4 s1 = floor(b1) * 2.0 + 1.0;
float4 sh = -step(h, 0.0);
float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
float4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
float3 g0 = float3(a0.xy, h.x);
float3 g1 = float3(a0.zw, h.y);
float3 g2 = float3(a1.xy, h.z);
float3 g3 = float3(a1.zw, h.w);
// Normalise gradients
float4 norm = taylorInvSqrt(float4(dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3)));
g0 *= norm.x;
g1 *= norm.y;
g2 *= norm.z;
g3 *= norm.w;
// Mix final noise value
float4 m = max(0.6 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
m = m * m;
float4 px = float4(dot(x0, g0), dot(x1, g1), dot(x2, g2), dot(x3, g3));
return 42.0 * dot(m, px);
}
float3 snoise_grad(float3 v)
{
const float2 C = float2(1.0 / 6.0, 1.0 / 3.0);
// First corner
float3 i = floor(v + dot(v, C.yyy));
float3 x0 = v - i + dot(i, C.xxx);
// Other corners
float3 g = step(x0.yzx, x0.xyz);
float3 l = 1.0 - g;
float3 i1 = min(g.xyz, l.zxy);
float3 i2 = max(g.xyz, l.zxy);
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
float3 x1 = x0 - i1 + C.xxx;
float3 x2 = x0 - i2 + C.yyy;
float3 x3 = x0 - 0.5;
// Permutations
i = mod289(i); // Avoid truncation effects in permutation
float4 p =
permute(permute(permute(i.z + float4(0.0, i1.z, i2.z, 1.0))
+ i.y + float4(0.0, i1.y, i2.y, 1.0))
+ i.x + float4(0.0, i1.x, i2.x, 1.0));
// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
float4 j = p - 49.0 * floor(p / 49.0); // mod(p,7*7)
float4 x_ = floor(j / 7.0);
float4 y_ = floor(j - 7.0 * x_); // mod(j,N)
float4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0;
float4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0;
float4 h = 1.0 - abs(x) - abs(y);
float4 b0 = float4(x.xy, y.xy);
float4 b1 = float4(x.zw, y.zw);
//float4 s0 = float4(lessThan(b0, 0.0)) * 2.0 - 1.0;
//float4 s1 = float4(lessThan(b1, 0.0)) * 2.0 - 1.0;
float4 s0 = floor(b0) * 2.0 + 1.0;
float4 s1 = floor(b1) * 2.0 + 1.0;
float4 sh = -step(h, 0.0);
float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
float4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
float3 g0 = float3(a0.xy, h.x);
float3 g1 = float3(a0.zw, h.y);
float3 g2 = float3(a1.xy, h.z);
float3 g3 = float3(a1.zw, h.w);
// Normalise gradients
float4 norm = taylorInvSqrt(float4(dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3)));
g0 *= norm.x;
g1 *= norm.y;
g2 *= norm.z;
g3 *= norm.w;
// Compute gradient of noise function at P
float4 m = max(0.6 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
float4 m2 = m * m;
float4 m3 = m2 * m;
float4 m4 = m2 * m2;
float3 grad =
-6.0 * m3.x * x0 * dot(x0, g0) + m4.x * g0 +
-6.0 * m3.y * x1 * dot(x1, g1) + m4.y * g1 +
-6.0 * m3.z * x2 * dot(x2, g2) + m4.z * g2 +
-6.0 * m3.w * x3 * dot(x3, g3) + m4.w * g3;
return 42.0 * grad;
}
Shader
Shader "shader test/particle"
{
Properties
{
[HDR]_Color("Color",color) = (1,1,1,1)
_MainTex("_MainTex", 2D) = "white" {}
_Size("Size",float) = 1.6
[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", Float) = 4
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull Mode", Float) = 0
}
SubShader
{
Tags { "Queue" = "Transparent+300" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" }
ZTest[_ZTest]
Cull[_Cull]
Blend One One, SrcAlpha OneMinusSrcAlpha
Lighting Off
ZWrite Off
Fog{ Mode Off }
LOD 200
Pass
{
CGPROGRAM
#pragma target 4.5
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct Particle
{
float3 pos; //起始位置
float3 newPos; //更新位置
};
StructuredBuffer<Particle> _ParticleBuffer;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _Size;
float4x4 _GameobjectMatrix;
struct appdata {
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f {
float4 pos:SV_POSITION;
float4 texcoord:TEXCOORD1;
};
float4x4 GetModelToWorldMatrix(float3 pos)
{
float4x4 transformMatrix = float4x4(
_Size,0,0,pos.x,
0,_Size,0,pos.y,
0,0,_Size,pos.z,
0,0,0,1
);
return transformMatrix;
}
float3x3 GetRotMatrix_X(float cosQ, float sinQ)
{
float3x3 rotMatrix_X = float3x3(
1, 0, 0,
0, cosQ, -sinQ,
0, sinQ, cosQ
);
return rotMatrix_X;
}
float3x3 GetRotMatrix_Y(float cosQ, float sinQ)
{
float3x3 rotMatrix_Y = float3x3(
cosQ, 0, sinQ,
0, 1, 0,
-sinQ, 0, cosQ
);
return rotMatrix_Y;
}
float3x3 GetRotMatrix_Z(float cosQ, float sinQ)
{
float3x3 rotMatrix_Z = float3x3(
cosQ, -sinQ, 0,
sinQ, cosQ, 0,
0, 0, 1
);
return rotMatrix_Z;
}
v2f vert(appdata v,uint instanceID :SV_INSTANCEID)
{
v2f o;
Particle particle = _ParticleBuffer[instanceID];
float4x4 WorldMatrix = GetModelToWorldMatrix(particle.pos.xyz);
WorldMatrix = mul(_GameobjectMatrix,WorldMatrix);
v.vertex = mul(WorldMatrix, v.vertex);
o.pos = mul(UNITY_MATRIX_VP,v.vertex);
o.texcoord.xy = v.texcoord.xy;
o.texcoord.zw = 0;
return o;
}
fixed4 frag(v2f i) :SV_Target
{
float2 uvMainTex = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
fixed3 col = tex2D(_MainTex, uvMainTex).rgb;
col = col * _Color;
return float4(col.rgb,1);
}
ENDCG
}
}
FallBack Off
}