Loading... ## 如何将ShaderToy Shader转换成Unity可用的Shader > 做图形开发的同学没有不知道[ShaderToy](www.shadertoy.com)网站的,它是由大神 [iq - Inigo Quilez](shadertoy.com/user/iq)创建的。 > ShaderToy网站包含了大量效果精彩绝伦的shader,但是这些shader是用WebGL Shader语言编写的,不适用于Unity ShaderToy。 > 那么如何方便的在项目中方便的使用ShaderToy Shader这个问题就直接怼脸上了。 ### 下面就以[Chimera's Breath](shadertoy.com/view/4tGfDW)来说明网站的玩法 #### 一、了解ShaderToy网站的基本用法 - 1. 网站界面说明,首先打开[https://www.shadertoy.com/view/4tGfDW](https://www.shadertoy.com/view/4tGfDW) 就可以得到下图的界面: ![](https://res.lazyun.cn/typecho/2022/05/21/shadertoy_website_ui.jpg?x-oss-process=style/compress) - 左侧图像显示区域就是这个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不够用的时候,可以有更多的Buffer - `Buffer C`: 同`Buffer A`,只是在一个Buffer不够用的时候,可以有更多的Buffer - `Buffer D`: 同`Buffer A`,只是在一个Buffer不够用的时候,可以有更多的Buffer - `Sound`: 用于生成声音的shader代码,理解傅立叶变换的同学都知道,复杂波形声音可以通过简单波形叠加产生,那么这里就可以用上了,这也是个`可选文件` - `Cubemap A`: 用于生成Cubemap的shader代码,这是`可选文件` - `Image` 等同于Unity Shader中的fragment shader,左侧图像最终显示的内容也是由这个shader输出的 - 上述文件里除了`Common`, 其他文件下方都会有4个黑色框并伴随`iChannel0`(就是下文提到的`输入通道`),这些可以理解成是当前文件的输入Texture,只不过不再是静态的图片,而是通过shader动态计算输出的图像内容 - 2. 从网站界面说明中就可以看出,ShaderToy没有支持vertex shader,而fragment shader输出范围就是左侧的整个画布,这个是不同于Unity的,所以转成Unity Shader的时候就自然而然的考虑使用[Render To Texture](blog.csdn.net/leonwei/article/details/54972653)的技术方案。 =============================== ### 如何转换可以从3个方面来考虑: #### 一. Shader 代码转换成 Unity URP 认的HLSL,这个可以参考[这个博客中的方法](https://blog.csdn.net/qq_28299311/article/details/104897851) - 上文中提到的Shader Inputs转换: - `iResolution` : 视窗分辨率,以像素为单位 - 对应Unity中的[_ScreenParams](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) - `iTime` : shader运行时间,以秒为单位 - 对应Unity中的[_Time.y](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) - `iTimeDelta` : 当前帧渲染时间,以秒为单位 - 对应Unity中的[unity_DeltaTime.x](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) - `iFrame` : shader运行帧数 - 对应Unity中的[Time.frameCount](https://docs.unity3d.com/ScriptReference/Time-frameCount.html), 该API只能在C#中访问,所以需要额外将该参数传入 - `iChannelTime[4]` : 各个通道运行时间,以秒为单位 - Unity中没有对应API,但是在我们这个转换方案中,各个通道都是同时开始运行的,那么这个时间应该是等于`iTime`,然后由于Unity中使用数组十分麻烦,所以我们直接对应Unity中的[_Time.y](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html) ,但需要注意的是数组和数值的语法差异需要单独处理(所幸该属性用到的地方并不多:) ) - `iChannelResolution[4]` : 各个通道分辨率,以像素为单位 - Unity中没有对应API,但是在我们这个转换方案中,各个通道都是相同大小,那么这个时间应该是等于`iTime`,所以对应Unity中的[_ScreenParams](https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html), 而且又是个数组,处理方式同`iChannelTime[4]` - `iMouse` : 鼠标像素坐标,xy:当前位置, zw:点击位置 - 对应Unity中的[Input.mousePosition](https://docs.unity3d.com/ScriptReference/Input-mousePosition.html), 该API只能C#中访问,而且格式不匹配ShaderToy,所以需要额外组装数据再传入shader - `iChannel0..3` : 输入通道, - 对应Unity中texture shader property,这个是我们可以自定义名字的,那么shader里面就定义成这些名字 - `iDate` : 年,月,日,时间 四个值,以秒为单位 - Unity中没有对应API,需要通过C#组装数据并传入 - `iSampleRate` : 声音采样率 - Unity中没有对应API,可以通过开放给用户,然后C#传入shader - 我这里也把常用的关键字转换代码集合到了[ShaderToyBase](ShaderToyRenderer/Runtime/Shaders/ShaderToyBase.hlsl)中 > ```c++ > /********** 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结构 > ```c# > [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](shadertoy.com/view/4tGfDW)的链接信息得到下图 > ![](https://res.lazyun.cn/typecho/2022/05/21/chimerabreath.jpg?x-oss-process=style/compress) 根据上图中的信息: - 给Channle A 添加输入参数`Channel C`如下图: ![](https://res.lazyun.cn/typecho/2022/05/21/channela.jpg?x-oss-process=style/compress) - 给Channle B 添加输入参数`Channel A`如下图: ![](https://res.lazyun.cn/typecho/2022/05/21/channelb.jpg?x-oss-process=style/compress) - 给Channle C 添加输入参数`Channel B`如下图: ![](https://res.lazyun.cn/typecho/2022/05/21/channelc.jpg?x-oss-process=style/compress) - 给Channle D 添加输入参数`Channel A B D`如下图: ![](https://res.lazyun.cn/typecho/2022/05/21/channeld.jpg?x-oss-process=style/compress) - 给Channle image 添加输入参数`Channel D`如下图: ![](https://res.lazyun.cn/typecho/2022/05/21/channelimage.jpg?x-oss-process=style/compress) - 数据设置已经完成,那么代码里面是如何建立Channel之间的链接关系的呢,如下代码所示, 其中各个Channel会依次调用该Channel的`Connect`方法 ```c# 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 ![](https://res.lazyun.cn/typecho/2022/05/21/materials.jpg?x-oss-process=style/compress) ##### 4. 上述操作完成之后,运行Unity,该框架就会自动处理底层Material,RenderTexture之间的链接关系。 - 渲染过程,核心就是调用Unity提供的API:[Graphics.Blit](https://docs.unity3d.com/ScriptReference/Graphics.Blit.html) - 其中的参数说明: - `source`: shader中有自己的逻辑,没有`_MainTex`属性,对应Material上有RenderTexture的引用,所以这个参数无用,填个Texture2D.whiteTexture 不报错就行。 - `dest`: render target,这里就是各个通道的RenderTexture - `mat` : 使用的shader对应的material - 代码如下: > ```c# > 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` 的最终效果了 ![](https://res.lazyun.cn/typecho/2022/05/21/final.jpg?x-oss-process=style/compress) #### 三、最后一个步骤就是处理用户的输入 > 具体实现方式跟Unity Shader引入鼠标输入的方式一样,这里就不详细说明了 > 核心代码:[ShaderToyRendererInput.cs](https://github.com/LazyunGame/ShaderToyToUnity/blob/master/Assets/ShaderToyRenderer/Runtime/ShaderToyRendererInput.cs) ### 结语 代码已经在github开源: **https://github.com/LazyunGame/ShaderToyToUnity** 最后修改:2022 年 05 月 21 日 © 禁止转载 赞 0