作为一个非移动端开发的美术,性能?消耗?不存在的,统统要给效果让步,除非程序拿着刀把我的脸按在键……%¥&……(*&SDJPAIFLKAS( &) )(i (+ ) &^%& ^$gfkljhl:"km:lhnk
最近看了一下 ShaderToy ,找到了一个无脑将 ShaderToy 搬运到 UE4 材质的方法,虽然通用性较差,但是胜在简单、快速、学习成本低。
Shadertoy BETA 这个网站就不介绍了,网上有一大堆教程
正文开始
一、准备阶段
首先,你需要有个 梯¥%……*&& ,能上 ShaderToy,
然后下载 VScode,并安装 VScode 插件 "ShaderToy" 和 "HLSL Preview"
二、GLSL 转 HLSL
首先,需要明确的是,Shadertoy 是用 GLSL 语法的代码,但是 UE4 的CustomNode 只支持 HLSL 的代码,我们要先转换一下。GLSL 和 HLSL 差异不大,只有类型和某些函数命名不一样,坐标系一个左手一个右手。
对于着色器语言,语法其实都很简单,函数也不多,但是难的不是语法,而是用这种语法将数学转换为图形。
HLSL 的学习,可以参考微软的文档 —— High-level shader language (HLSL)
GLSL 的学习,可以看一下 OpenGL 的文档 —— learnopengl.com/Getting-sta…
但是,这都不重要,重要的是应用。
以这个 shader 为例 —— www.shadertoy.com/view/4tyfDm
(1)将代码复制到 VS code 中,并 Ctrl+Shift+P 调出控制台执行 ShaderToy 这个插件,然后就能在 VScode 中显示效果了
(2)使用 VScode,新建 HLSL 文件,将代码复制进去,然后Ctrl+Shift+P 调出控制台执行 PreviewHLSL ,
(3) 这个时候,你会出现一大堆报错,不要慌,只需要改一下就行了。
Ctrl+H 使用批量替换,将 GLSL 的关键字全部替换成 HLSL 的关键字
比如说,GLSL中 4 维变量叫 vec4,但是 HLSL 中叫 float4,这里可以参考微软的 GLSL to HLSL 文档,需要将类型、部分函数的关键字,主函数全部替换掉
GLSL-to-HLSL reference - UWP applications
-
代码报错
-
内置函数替换
-
主函数替换
除了上面这些,ShaderToy 中还有一些专有的写法,需要根据情况替换掉,比如一般 ShaderToy 代码中都有这些。
- 转换成 HLSL 代码
由于 HLSL 中没有 time 这个内置函数,所以只能把这个参数暴露出去。
三、HLSL 转 UE4 CustomNode
因为 UE4 的中有 CustomNode 的材质 Debug 异常困难,所以,如果我们的代码以 HLSL 跑起来,那么进 UE4 也就不会有代码问题了。
UE4 的 CustomNode 表面上 是 不支持 多函数模式的,但是 本质是支持的,
这里需要用到结构体来实现多函数,可以参考以下文章
- 将函数写在结构体中,然后再实例化调用
新建一个 HLSL,将子函数放置到结构体中
-
4 个子函数
csqr、iSphere、map、raymarch -
将子函数放到结构体中
-
将结构体实例化,然后主函数去掉,函数调用使用结构体,并注释掉参数声明
这里需要注意一点,CustomNode 中不能不赋值声明参数,如果有的话,直接使用,然后在 Inputs 界面中添加.(代码中 不用声明、不用声明、不用声明)。有一些可以调效果的参数也可以直接拿出来,比如我把 ro 参数、以及颜色参数添加进了 Inputs 界面。
- 比如
Time\UV这些节点,可以直接 Inputs,不用在CustomNode中声明
结束~
总结一下
- 将 Shadertoy 的 GLSL 转 HLSL
- 将 HLSL 利用
Struct的方法转成 UE4 的CustomNode代码 - 根据需求,将一些 变量 提取出来,作为动态调节参数
其实,上面这一套流程都是固定的,写个程序,完全可以一键转 UE,有比较闲的大佬可以写个小脚本
最后附上代码
HLSL 代码
float2 csqr( float2 a )
{
return float2( a.x*a.x - a.y*a.y, 2.*a.x*a.y );
}
float2 iSphere( in float3 ro, in float3 rd, in float4 sph )//from iq
{
float3 oc = ro;// - sph.xyz;
float b = dot( oc, rd );
float c = dot( oc, oc ) - sph.w*sph.w;
float h = 1.0; //b*b - c;
return float2(-b-h, -b+h );
}
float map(in float3 p, float2 sctime)
{
float res = 0.;
float3 c = p;
c.xy = c.xy * sctime.x + float2(c.y, c.x) * sctime.y;
for (int i = 0; i < 10; ++i)
{
p =.7*abs(p)/dot(p,p) -.7;
p.yz= csqr(p.yz);
p=p.zxy;
res += exp(-19. * abs(dot(p,c)));
}
return res/2.;
}
float3 raymarch( in float3 ro, float3 rd, float2 tminmax , float2 sctime)
{
//tminmax += float2(1.,1.) * sin( iTime * 1.3)*3.0;
float3 one3 = float3(1.,1.,1.);
float3 t = one3 * tminmax.x;
float3 dt = float3(.07, 0.02, 0.05);
float3 col= float3(0.0,0.0,0.0);
float3 c = one3 * 0.;
for( int i=0; i<64; i++ )
{
float3 s = float3(2.0, 3.0, 4.0);
t+=dt*exp(-s*c);
float3 a = step(t,one3*tminmax.y);
float3 pos = ro+t*rd;
c.x = map(ro+t.x*rd, sctime);
c.y = map(ro+t.y*rd, sctime);
c.z = map(ro+t.z*rd, sctime);
col = lerp(col, .99*col+ .08*c*c*c, a);
}
float3 c0 = float3(0.4,0.3,0.99);
float3 c1 = float3(0.9,0.7,0.0);
float3 c2 = float3(0.9,0.1,0.2);
return c0 * col.x + c1 * col.y + c2 * col.z;
}
float time;
float3 ro;
float4 main(in float2 uv:TEXCOORD0):SV_TARGET
{
// float time = 1;
// float2 q = fragCoord.xy / iResolution.xy;
float2 p = -1.0 + 2.0 * uv;
// p.x *= iResolution.x/iResolution.y;
float2 m = float2(0.0,0.0);
// float3 ro = float3(15.0,0.0,0.0);
float3 ta = float3( 0.0 , 0.0, 0.0 );
float3 ww = normalize( ta - ro );
float3 uu = normalize( cross(ww,float3(0.0,1.0,0.0) ) );
float3 vv = normalize( cross(uu,ww));
float3 rd = normalize( p.x*uu + p.y*vv + 4.0*ww );
float2 tmm = iSphere( ro, rd, float4(0.,0.,0.,2.) );
// raymarch
float3 col = raymarch(ro,rd,tmm, float2(sin(time), cos(time)));
// shade
col = .5 *(log(1.+col));
col = clamp(col,0.,1.);
return float4( col, 1.0 );
}
UE4 CustomNode
struct Functions
{
float2 csqr( float2 a )
{
return float2( a.x*a.x - a.y*a.y, 2.*a.x*a.y );
}
float2 iSphere( in float3 ro, in float3 rd, in float4 sph )//from iq
{
float3 oc = ro;// - sph.xyz;
float b = dot( oc, rd );
float c = dot( oc, oc ) - sph.w*sph.w;
float h = 1.0; //b*b - c;
return float2(-b-h, -b+h );
}
float map(in float3 p, float2 sctime) {
float res = 0.;
float3 c = p;
c.xy = c.xy * sctime.x + float2(c.y, c.x) * sctime.y;
for (int i = 0; i < 10; ++i)
{
p =.7*abs(p)/dot(p,p) -.7;
p.yz= csqr(p.yz);
p=p.zxy;
res += exp(-19. * abs(dot(p,c)));
}
return res/2.;
}
float3 raymarch( in float3 ro, float3 rd, float2 tminmax , float2 sctime)
{
//tminmax += float2(1.,1.) * sin( iTime * 1.3)*3.0;
float3 one3 = float3(1.,1.,1.);
float3 t = one3 * tminmax.x;
float3 dt = float3(.07, 0.02, 0.05);
float3 col= float3(0.0,0.0,0.0);
float3 c = one3 * 0.;
for( int i=0; i<64; i++ )
{
float3 s = float3(2.0, 3.0, 4.0);
t+=dt*exp(-s*c);
float3 a = step(t,one3*tminmax.y);
float3 pos = ro+t*rd;
c.x = map(ro+t.x*rd, sctime);
c.y = map(ro+t.y*rd, sctime);
c.z = map(ro+t.z*rd, sctime);
col = lerp(col, .99*col+ .08*c*c*c, a);
}
// float3 c0 = float3(0.4,0.3,0.99);
// float3 c1 = float3(0.9,0.7,0.0);
// float3 c2 = float3(0.9,0.1,0.2);
return c0 * col.x + c1 * col.y + c2 * col.z;
}
};
Functions f;
// float time;
// float3 ro;
float2 p = -1.0 + 2.0 * uv;
// p.x *= iResolution.x/iResolution.y;
float2 m = float2(0.0,0.0);
// float3 ro = float3(15.0,0.0,0.0);
float3 ta = float3( 0.0 , 0.0, 0.0 );
float3 ww = normalize( ta - ro );
float3 uu = normalize( cross(ww,float3(0.0,1.0,0.0) ) );
float3 vv = normalize( cross(uu,ww));
float3 rd = normalize( p.x*uu + p.y*vv + 4.0*ww );
float2 tmm = f.iSphere( ro, rd, float4(0.,0.,0.,2.) );
// raymarch
float3 col = f.raymarch(ro,rd,tmm, float2(sin(time), cos(time)));
// shade
col = .5 *(log(1.+col));
col = clamp(col,0.,1.);
return float4( col, 1.0 );
CustomNode Input 参数和材质节点
其实,这种做法只适用于一些 2D效果。通用性比较差,可以说仅仅是还原了一张纹理
真正的在 UE4 中还原 ShaderToy 里面的效果,需要理解其 函数的意义 和背后的 数学原理 ,并将参数替换成 UE4 里面的节点,才能展现代码的灵魂所在。
比如上面这个效果,里面用到了 Ray Marching \ Ray Intersection \ fractal,但是单纯的抄到 UE4 中,仅仅成为了一个动态纹理,徒有其表,下一篇讲一下,如何将 ShaderToy 的效果在 UE 中赋予 灵魂。
补一个视频操作~