shaderToy转glsl

1,590 阅读7分钟

shaderToy就是glsl ,本文是小白文,标题可能有点不对。 上次翻译了半篇教程关于shaderToy的使用,这次主要是补充一些细节,如何手动把shaderToy的代码拿来使用。自动的大概有大佬已经写过了,这里更笨拙一些。

最终结果是完全原生的glsl ,没有需要借助外部编译的。

1 本地环境准备

当然是copy代码到自己的编辑器上,建议命名文件为 .frag ,这样会有良好的语法检查。如果没有,参考这篇glsl调试。 mac也是一样的。语法检查一定要有,虽然在字符串之前加上注释 /*glsl*/也可以获得高亮效果,但是无法检查错误。 如果你本地已经有了很好glsl编辑环境,那就很好了。

多说一句,frag 和 vert的语法检查大概比webgl可用的glsl还要严格一些,如果他们检测没问题,那在webgl中也没问题,反过来不一定成立。

image.png

如果,你发现已经通过语法检查了,但是webgl中仍然报错,那么就是 vert 和 frag不匹配,常见的情况就是在fragment中使用了 varing(in) ,但是在vert中没有相应的声明 varing(out)。

2 确定版本

glsl有两个大版本 1.00 和3.00 。这里我建议直接使用3.00,因为shaderToy本身使用的就是3.00,3.00新增了一些函数。 如果你想使用1.00的语法,参考threeJS的处理如下,就是把新老版本的关键字和函数名一一对应起来了。

//vert 
#define attribute in
#define varying out
#define texture2D texture

//frag 
#define varying in
layout(location = 0) out highp vec4 pc_fragColor;
#define gl_FragColor pc_fragColor
#define gl_FragDepthEXT gl_FragDepth
#define texture2D texture
#define textureCube texture
#define texture2DProj textureProj
#define texture2DLodEXT textureLod
#define texture2DProjLodEXT textureProjLod
#define textureCubeLodEXT textureLod
#define texture2DGradEXT textureGrad
#define texture2DProjGradEXT textureProjGrad
#define textureCubeGradEXT textureGrad

3 补足顶点着色器

shaderToy帮我们隐去了顶点着色器,我们要做的也就是一个常规的确定顶点的顶点着色器,如果你想额外添加attribute也可以的。

下面是我常用的,因为一般会把这个效果用到某个平面上,那就直接用uv。如果只要完全还原,那么下面的 a_Color a_Uv;相关的都可以删掉

#version 300 es 
#define  attribute in
#define  varying  out 

// layout(location = 0)in  vec4 a_Position ; 
attribute vec4 a_Position ;
// layout(location = 1)in  vec3  a_Color ; 
attribute vec3 a_Color ;
attribute vec2 a_Uv;
varying highp vec2 v_Uv;
varying vec3 v_Color ;
// 3.0 没有 attribute varying  但是可以通过预编译指令兼容
void main(){
    gl_Position = a_Position ;
    gl_PointSize = 2.0;
    v_Uv = a_Uv;
    v_Color= a_Color;
}

删掉之后,就是下面的了。 如果你是想转three的shaderMatrial ,后面会说。

#version 300 es 
#define  attribute in
#define  varying  out 
attribute vec4 a_Position ;
void main(){
    gl_Position = a_Position ;
}

4 进入正题 fragment

shaderToy绘图的那个着色器就是image那一栏对应的。 它的格式是这样的,原生至少是有一个main函数的,你可以直接把它这个函数放到main函数里去执行,我一般直接改造为main 函数。

void mainImage( out vec4 fragColor, in vec2 fragCoord ){
fragColor = vec4(0);
}

改造后如下 。这里只输出一个vec4,所以直接这么写,如果输出多个,需要用 layout关键字指定索引。 如果用了预编译声明了gl_FragColor,就可以使用它。

out highp vec4 fragColor ;
void main(  ){
    vec2 fragCoord = gl_FragCoord.xy;
    vec2 iResolution = u_CanvasSize;
    fragColor = vec4(0);
}

uniform

首先还是先把shader上面的着色器输入copy下来,然后一个个来处理。

image.png

iResolution的xy分量直接就是对应canvas的width height,至于z分量像素高宽比,目前没看到有哪里使用过。

iTime的单位是s ,而requestFrameAnimation的默认参数是毫秒。

iTimeDelta 如果你要用这个,那就是在你更新上面的iTime的时候,减去之前的值。 iFrameRate 平均帧数 fps ,这个就是 把累积时间 乘一个系数变成1分钟除以累积帧数,一般应该不会用到。  iChannelTime[n]:视频或声音中的当前时间,和iTime一样更新就行了,我也没用过,具体单位注意一下。

以下是伪实现。

function updateShaderdata(t,delta) {
            iTime && gl.uniform1f(iTime, t)
            iTimeDelta && gl.uniform1f(iTimeDelta, delta)
            iFrame && gl.uniform1i(iFrame, ++iFrame);

        }
        
        function ani(t = 0) {
        //节流 真正的节流 也就是减少帧数,降低刷新频率
            if (t - start > 1000 / 60 * 2) {
                start = t;
                delta= t- start;
                updateShaderdata(t,delta)
                draw()
            }
            requestAnimationFrame(ani)
        }

iMouse 四维分量,xy就是位置,zw代表点击的状态。 伪实现如下,已经考虑了canvas的位置因素,但是没考虑缩放,这里认为canvas的画布尺寸和css尺寸是一样的。


        let mouseState = [0, 0, 0, 0]
        function handleMove({ clientX, clientY }) {
            const { left, bottom } = canvas.getBoundingClientRect()
            let x = clientX - left, y = bottom - clientY;
            gl.uniform2fv(u_Mouse, [x, y]);
            mouseState[0] = x;
            mouseState[1] = y;
        }
        function mouseDown(e) {
            mouseState[2] = 1 - mouseState[2];
            mouseState[3] = mouseState[2];
            const i = setTimeout(() => {
                mouseState[3] = 0;
                clearTimeout(i);
            })
        }

纹理

纹理就是图,常规的图,只要下载就行了,至于怎么下载,作为一个前端,打开devtool的网络就能下载原图。

如果你参考的demo使用了键盘纹理,建议直接自定义键盘事件和对应的uniform ,比如直接输入键盘事件的ASSIC码,如果你使用了很多键位,可以自定义一个结构体,把它当对象使用,按下就是1,抬起就是0 ,也可以更复杂,结合实际需求改写。

struct Keys{ 
     float a ;
     float Esc ;
    float Shift; 
};

输出缓冲区

会用到这个的最典型的就是投影贴图。 它能够输出,就是在最后一次绘制之前的绘制。buffer是对应一套着色器的,它渲染的结果会存到buffer里,而使用的时候就是一个纹理。

原生的话就是FrameBuffer,伪实现如下, 绘制结束后别忘了把绑定的缓冲区置空。

 //纹理对象  用于封装wegl绘图结果
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    /*  用来激活指定的纹理单元。最后一个数字代表索引*/
    gl.activeTexture(gl.TEXTURE0)
    /* 创建纹理 */
    let texture = gl.createTexture()
    /* 把纹理对象绑定到目标对象 TEXTURE_2D上 */
    gl.bindTexture(gl.TEXTURE_2D, texture);
    /* 设置纹理参数  */
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    /* target, 
      level, 图像级别
     internalformat, 颜色格式
      width, 
       height,
        border,边界宽度 必须为0
         format, 颜色格式 webgl1必须和前面的一样
         type, 纹理数据类型
         ArrayBufferView? 
         pixels  纹理像素源
    
    设置纹理的图像源
    这个函数不少重载 */
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)


    // 帧缓冲区
    const framebuffer = gl.createFramebuffer();
    /* 把帧缓冲区绑定到目标对象上   目标对象用于收集渲染图像时所需的颜色、透明度、深度和模具缓冲数据*/
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
    /** target,  
    * 将纹理对象添加到帧缓冲区
    * attachment,  将纹理对象绑定到哪个缓冲区中。下面的是颜色缓冲区 还有深度 模板。
    * textarget, 纹理的目标对象
    *  texture,
    *  level 图像级别
    */
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        texture,
        0
    )

image.png

如果你使用threejs

先来大致看一下 Shadermaterial 和 RawShaderMaterial的预处理 。 three 把shader分成了 version 、prefix、 shader这三部分,

const vertexGlsl = versionString + prefixVertex + vertexShader; 
const fragmentGlsl = versionString + prefixFragment + fragmentShader;

RawShaderMaterial虽说是完全未加工的,但是其实还是处理了version,所以版本声明不要重复写。 其他的就没啥了。

ShaderMaterial会有默认的前缀处理,你只要写main函数里面的即可。如果你new了一个shaderMaterial ,什么也不填,那就是下面这样。 如果你写了fragment,那么需要是完整的main函数。

*FRAGMENT* #version 300 es
#define varying in
layout(location = 0) out highp vec4 pc_fragColor;
#define gl_FragColor pc_fragColor
#define gl_FragDepthEXT gl_FragDepth
#define texture2D texture
#define textureCube texture
#define texture2DProj textureProj
#define texture2DLodEXT textureLod
#define texture2DProjLodEXT textureProjLod
#define textureCubeLodEXT textureLod
#define texture2DGradEXT textureGrad
#define texture2DProjGradEXT textureProjGrad
#define textureCubeGradEXT textureGrad
precision highp float;
precision highp int;
#define HIGH_PRECISION
#define SHADER_NAME ShaderMaterial
#define DOUBLE_SIDED
#define USE_SHADOWMAP
#define SHADOWMAP_TYPE_PCF
uniform mat4 viewMatrix;
uniform vec3 cameraPosition;
uniform bool isOrthographic;
vec4 LinearToLinear( in vec4 value ) {
  return value;
}
vec4 LinearTosRGB( in vec4 value ) {
  return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
}
vec4 linearToOutputTexel( vec4 value ) { return LinearTosRGB( value ); }

void main() {
  gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );
}
//three.module.js:18591 

在three里用Buffer就比较简单了,代码示意如下。

// 准备好绘制Buffer
renderer.setRenderTarget( renderTarget  );
renderer.render( BufferScene, BufferCamera );
renderer.setRenderTarget( null );
// 解绑Buffer后绘制到屏幕

结语

以上所说,基本上够用了,只是原版照搬而已。

当然,最好是看明白代码里写了什么,这样好灵活运用。