ShaderToy shader快速移植实现
如何将ShaderToy Shader转换成Unity可用的Shader
做图形开发的同学没有不知道ShaderToy网站的,它是由大神 iq - Inigo Quilez创建的。
ShaderToy网站包含了大量效果精彩绝伦的shader,但是这些shader是用WebGL Shader语言编写的,不适用于Unity ShaderToy。
那么如何方便的在项目中方便的使用ShaderToy Shader这个问题就直接怼脸上了。
下面就以Chimera's Breath来说明网站的玩法
一、了解ShaderToy网站的基本用法
-
- 网站界面说明,首先打开https://www.shadertoy.com/view/4tGfDW 就可以得到下图的界面:

- 左侧图像显示区域就是这个Shader最终呈现的效果,在区域下方的控制条中可以暂停/继续运行
- 上图中的Shader Inputs和Unity中的Shader输入没什么区别,只是在Unity中的名称不同,将在下文中详细阐述
- 右侧的文本框就是Shader代码的编辑区域,ShaderToy的Shader是由多个预定义用途的文件组成,它们分别是:
Common: 可理解成Unity中Shader需要引用的cginc/hlsl库文件, 这是可选文件Buffer A: 可理解成是最终的fragment shader需要的Texture输入,只不过这个Texture的内容是由这个文件中的shader代码动态计算得来,这是可选文件Buffer B: 同Buffer A,只是在一个Buffer不够用的时候,可以有更多的BufferBuffer C: 同Buffer A,只是在一个Buffer不够用的时候,可以有更多的BufferBuffer D: 同Buffer A,只是在一个Buffer不够用的时候,可以有更多的BufferSound: 用于生成声音的shader代码,理解傅立叶变换的同学都知道,复杂波形声音可以通过简单波形叠加产生,那么这里就可以用上了,这也是个可选文件Cubemap A: 用于生成Cubemap的shader代码,这是可选文件Image等同于Unity Shader中的fragment shader,左侧图像最终显示的内容也是由这个shader输出的
- 上述文件里除了
Common, 其他文件下方都会有4个黑色框并伴随iChannel0(就是下文提到的输入通道),这些可以理解成是当前文件的输入Texture,只不过不再是静态的图片,而是通过shader动态计算输出的图像内容
- 网站界面说明,首先打开https://www.shadertoy.com/view/4tGfDW 就可以得到下图的界面:
-
- 从网站界面说明中就可以看出,ShaderToy没有支持vertex shader,而fragment shader输出范围就是左侧的整个画布,这个是不同于Unity的,所以转成Unity Shader的时候就自然而然的考虑使用Render To Texture的技术方案。
===============================
如何转换可以从3个方面来考虑:
一. Shader 代码转换成 Unity URP 认的HLSL,这个可以参考这个博客中的方法
上文中提到的Shader Inputs转换:
iResolution: 视窗分辨率,以像素为单位- 对应Unity中的_ScreenParams
iTime: shader运行时间,以秒为单位- 对应Unity中的_Time.y
iTimeDelta: 当前帧渲染时间,以秒为单位- 对应Unity中的unity_DeltaTime.x
iFrame: shader运行帧数- 对应Unity中的Time.frameCount, 该API只能在C#中访问,所以需要额外将该参数传入
iChannelTime[4]: 各个通道运行时间,以秒为单位- Unity中没有对应API,但是在我们这个转换方案中,各个通道都是同时开始运行的,那么这个时间应该是等于
iTime,然后由于Unity中使用数组十分麻烦,所以我们直接对应Unity中的_Time.y ,但需要注意的是数组和数值的语法差异需要单独处理(所幸该属性用到的地方并不多:) )
- Unity中没有对应API,但是在我们这个转换方案中,各个通道都是同时开始运行的,那么这个时间应该是等于
iChannelResolution[4]: 各个通道分辨率,以像素为单位- Unity中没有对应API,但是在我们这个转换方案中,各个通道都是相同大小,那么这个时间应该是等于
iTime,所以对应Unity中的_ScreenParams, 而且又是个数组,处理方式同iChannelTime[4]
- Unity中没有对应API,但是在我们这个转换方案中,各个通道都是相同大小,那么这个时间应该是等于
iMouse: 鼠标像素坐标,xy:当前位置, zw:点击位置- 对应Unity中的Input.mousePosition, 该API只能C#中访问,而且格式不匹配ShaderToy,所以需要额外组装数据再传入shader
iChannel0..3: 输入通道,- 对应Unity中texture shader property,这个是我们可以自定义名字的,那么shader里面就定义成这些名字
iDate: 年,月,日,时间 四个值,以秒为单位- Unity中没有对应API,需要通过C#组装数据并传入
iSampleRate: 声音采样率- Unity中没有对应API,可以通过开放给用户,然后C#传入shader
我这里也把常用的关键字转换代码集合到了ShaderToyBase中
/********** ShaderToy Inputs ************/ #define iResolution _ScreenParams #define iTime _Time.y #define iChannelTime _Time.y #define iChannelResolution _ScreenParams #define iTimeDelta unity_DeltaTime.x /****************************************/ /****************************************/ #define PI 3.14159265358 #define mix lerp #define vec2 float2 #define vec3 float3 #define vec4 float4 #define fract frac #define mat2 float2x2 #define mat3 float3x3 #define mat4 float4x4 vec4 texelFetch(sampler2D samp,vec2 p, int lod) { return tex2Dlod(samp, float4(p, 0, lod)); } vec4 textureLod(sampler2D samp,vec2 p, float lod) { return tex2Dlod(samp, float4(p, 0, lod)); } /*****************************************/
需要注意的是:ShaderToy使用的GLES代码中矩阵乘法是直接用的乘法运算符(*), 而Unity中则需要使用
mul方法来实现
二. 完成了第一个步骤也只是解决了单 Channel(通道)类型的 Shader 的转换。如何对下图中多Channel类型的Shader进行转换呢?
首先我们考虑ShaderToy有默认的4个Channel类型,既然要转换这个类型的Shader,那么这个Channel信息及Channel之间的链接信息是必须也要转换到Unity的,所以我们有:
1. 保存Channel结构
[Serializable] public class ChannelBuffer { /// <summary> /// 该通道名,对应的是ShaderToy中的A,B,C,D和Image通道 /// </summary> public ChannelEnum bufferName = ChannelEnum.none; /// <summary> /// 该通道的输入通道信息 /// </summary> public ChannelEnum[] inputBufferNames; /// <summary> /// 该通道渲染所需Material,其上绑定了对应Channel 的 Shader /// </summary> public Material material; /// <summary> /// 该通道的RenderTexture 格式,按需选择降低性能消耗 /// </summary> public RenderTextureFormat format = RenderTextureFormat.ARGBHalf; /// <summary> /// 该通道开放的参数 /// </summary> public ArgumentItem[] arguments; }
2. 如何保存Channel 之间的链接信息
让我们来梳理一下Chimera's Breath的链接信息得到下图
根据上图中的信息:
给Channle A 添加输入参数
Channel C如下图:

给Channle B 添加输入参数
Channel A如下图:

给Channle C 添加输入参数
Channel B如下图:

给Channle D 添加输入参数
Channel A B D如下图:

给Channle image 添加输入参数
Channel D如下图:

数据设置已经完成,那么代码里面是如何建立Channel之间的链接关系的呢,如下代码所示,
其中各个Channel会依次调用该Channel的Connect方法public void Init() { if (bufferA.bufferName != ChannelEnum.none) { bufferA.Connect(new ChannelBuffer[] {bufferA, bufferB, bufferC, bufferD, image}); } if (bufferB.bufferName != ChannelEnum.none) { bufferB.Connect(new ChannelBuffer[] {bufferA, bufferB, bufferC, bufferD, image}); } if (bufferC.bufferName != ChannelEnum.none) { bufferC.Connect(new ChannelBuffer[] {bufferA, bufferB, bufferC, bufferD, image}); } if (bufferD.bufferName != ChannelEnum.none) { bufferD.Connect(new ChannelBuffer[] {bufferA, bufferB, bufferC, bufferD, image}); } image.Connect(new ChannelBuffer[] {bufferA, bufferB, bufferC, bufferD, image}); } // 对应的Connect方法 public void Connect(ChannelBuffer[] buffers) { if (bufferName == ChannelEnum.none) { return; } channels = new ChannelBuffer[inputBufferNames.Length]; for (int i = 0; i < inputBufferNames.Length; i++) { var name = inputBufferNames[i]; if (name == ChannelEnum.none) continue; // 将Channel name 和 Channel instance 对应上 channels[i] = buffers.First(t => { #if UNITY_EDITOR Debug.Log(t.bufferName + " --> " + name); #endif return t.bufferName == name; }); } for (int i = 0; i < channels.Length; i++) { var c = channels[i]; if (c != null && c.bufferName != ChannelEnum.none) { if (c == this) { // 如果输入Channel中有自己这个Channel,则需要使用 double rendertexture swapping。 // 因为unity不支持在一个Graphic.Blit方法中,将同一个RenderTexture当作参数,又当作渲染目标 _rt1 = CreateRenderTexture(bufferName.ToString() + "_swap"); material.SetTexture("iChannel" + i, _rt1); } else { // 设置Shader中的对应Channel的RenderTexture material.SetTexture("iChannel" + i, c.RenderTexture); } } } // 设置常用参数 material.SetFloat("iScreenRatio", RenderTexture.height * 1f / RenderTexture.width); material.SetVector("iScreenParams", new Vector4(RenderTexture.width, RenderTexture.height, RenderTexture.width * 1f / RenderTexture.height, 1)); isConnected = true; }
3. 根据上文中提到的Shader转换方式得到各个Channel Shader, 然后设置各个Channel的material

4. 上述操作完成之后,运行Unity,该框架就会自动处理底层Material,RenderTexture之间的链接关系。
渲染过程,核心就是调用Unity提供的API:Graphics.Blit
- 其中的参数说明:
source: shader中有自己的逻辑,没有_MainTex属性,对应Material上有RenderTexture的引用,所以这个参数无用,填个Texture2D.whiteTexture 不报错就行。dest: render target,这里就是各个通道的RenderTexturemat: 使用的shader对应的material
- 代码如下:
public void Render() { if (!isConnected) { return; } if (bufferName == ChannelEnum.none) { return; } if (_rt1) { // 如果有双Buffer swap,在这里渲染更新替换 Graphics.Blit(RenderTexture, _rt1); } Graphics.Blit(Texture2D.whiteTexture, RenderTexture, material); }
- 其中的参数说明:
这样我们就得到了
Chimera's Breath的最终效果了

三、最后一个步骤就是处理用户的输入
具体实现方式跟Unity Shader引入鼠标输入的方式一样,这里就不详细说明了
核心代码:ShaderToyRendererInput.cs
结语
代码已经在github开源:
https://github.com/LazyunGame/ShaderToyToUnity
