shadertoy 移植到本地(1):具体实现

程序架构

html + js
使用typescript 工程构建 js

步骤一

写一个空的 html 程序




    
    shadertoy player



    
        plass use a browser that supports "canvas"
    



步骤二

用canvas通过webglAPI 绘制全屏像素

  • 获取 webgl渲染上下文对象
//shadertoy 的shader 使用的gl es 300 的语法,需要使用webgl2.
let webgl2 = canvas.getContext("webgl2");
  • 着色器程序
//顶点着色器 字符串
let baseVS = `#version 300 es
        in vec2 a_Position; //顶点 二维坐标 
        void main() {
            gl_Position = vec4(a_Position.xy, 0.0 , 1.0);
        }`;
//片元着色器 字符串
let baseFS = `#version 300 es
        out vec4 color;
        void main(){
            color =  vec4(1.0 , 0.0 , 0.0 , 1.0);  //所有坐标像素输出红色
        }`;
//用着色器字符串,创建 gl的着色器对象

//创建 顶点、片元 着色器对象
let vs = gl.createShader(gl.VERTEX_SHADER);
let fs = gl.createShader(gl.FRAGMENT_SHADER);
//上传着色器的代码文本
gl.shaderSource(vs, baseVS );
gl.shaderSource(fs, baseFS );
//编译着色器
gl.compileShader(vs);
gl.compileShader(fs);
//创建 gl程序
let program = gl.createProgram();
//将 着色器 绑定到 gl程序 ,并链接, 着色器到GPU准备工作的最后一步
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
//指定当前 使用的gl程序
gl.useProgram(program);
  • 一个三角形顶点数据
//准备顶点数据
//什么只有一个三角形? 我们只需像素渲染覆盖全屏(一个大三角形足以),只需要使用 gl_FragCoord + iResolution 来算定位像素UV。
//        0
//      /   \
//     /     \
//   2 ------- 1
//
let posArr = [0, 3, 2, -1, -2, -1];  //三个顶点坐标,两个为一组二维顶点。
//创建 缓冲区对象
let glPosBuffer= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, glPosBuffer);  //指定当前被操作的 缓冲区对象
//给当前缓冲区对象(GPU显存),上传顶点位置数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posArr), gl.STATIC_DRAW);            

//为着色器 Attrib 字段分配 ,指定上面的顶点位置缓冲区作为输入数据
//先获取 Attrib 的 名为 "a_Position" 字段的地址
let aPositionAddr = gl.getAttribLocation(program, "a_Position");
gl.bindBuffer(gl.ARRAY_BUFFER, glPosBuffer);//指定当前被操作的 缓冲区对象
//告诉GPU,"a_Position" 字段,如何从缓冲区中读取数据
gl.vertexAttribPointer(aPositionAddr, 2, gl.FLOAT, false, 0, 0);  
//激活启用 设置Attrib 字段的设置。
gl.enableVertexAttribArray(aPositionAddr);

  • 绘制渲染
//请求GPU 安当前状态进行绘制
gl.drawArrays(gl.TRIANGLES, 0, 3);  //三角形类 ,从 buffer0 索引位置开始,绘3个长度的顶点数据.

得到一个全屏单红色绘制画面

全屏单色渲染.png

步骤三

ok,基础的准备工作完成了,接下来就可以,将shadertoy 的着色片段插入到我们的 片元着色中,然后渲染就可以得到,理论上与shadertoy一致的效果。

  • 修改 片元着色器 代码
//片元着色器 字符串
let baseFS = `#version 300 es
        out vec4 color;
        //下面是 uniform 字段定义部分 (仅教程,这里只实现两个基础 字段)
        uniform vec3      iResolution;
        uniform float     iTime;

        //下面一行作为插入位置,它是一段特定的注释,作为识别并替换成 shadertoy片段 代码用。
        //=#*INSERT_LOCATION*#=

        void main(){
            vec4 col = vec4(0.0 , 0.0 , 0.0 , 1.0);
            mainImage(col , gl_FragCoord.xy);     //改函数是 shadertoy 固定接口,它会输出一个颜色。
            color = col ;
        }`;
  • 插入到片元着色器代码中
//shadertoy 的代码
let sToyTest= `
void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {
        // Normalized pixel coordinates (from 0 to 1)
        vec2 uv = fragCoord/iResolution.xy;
    
        // Time varying pixel color
        vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
    
        // Output to screen
        fragColor = vec4(col,1.0);
    }
`;
//片元着色器代码中 插入 shadertoy 代码
baseFS = baseFS.replace(`//=#*INSERT_LOCATION*#=`, sToyTest);

  • unifrom字段的输入
let totalTimeSec = 0;

//shaderToy 内置uniform 上传
//获取 uniform  字段名为  "iResolution  " 的地址
let iResolutionAddr = gl.getUniformLocation(program, "iResolution");
//给iResolutionAddr 字段设置数据 ,是canvas的像素宽高
gl.uniform3f(iResolutionAddr, gl.canvas.width, gl.canvas.height, 1);
//获取 uniform  字段名为  "iTime" 的地址 
let iTimeAddr = gl.getUniformLocation(program, "iTime");
//给iTimeAddr字段设置数据,是开始运行到当前的计时
gl.uniform1f(iTimeAddr, totalTimeSec);

得到一个全屏颜色交替的画面

shadertoy测试渲染画面.png

步骤四

上面只是进行了一次绘制,想要绘制动画效果,就需要在绘制完一帧后连续绘下一帧,并一致持续下去,所有我们需要一个循环。

//循环渲染
let time = Date.now();
let totalTimeSec = 0;
let loop = () => {
    let nowTime = Date.now();
    let dt = (nowTime - time) * 0.001;
    totalTimeSec += dt;
    time = nowTime;
    //uniform 更新
    //shaderToy 内置uniform 上传
    //获取 uniform  字段名为  "iResolution  " 的地址
    let iResolutionAddr = gl.getUniformLocation(program, "iResolution");
    //给iResolutionAddr 字段设置数据 ,是canvas的像素宽高
    gl.uniform3f(iResolutionAddr, gl.canvas.width, gl.canvas.height, 1);
    //获取 uniform  字段名为  "iTime" 的地址 
    let iTimeAddr = gl.getUniformLocation(program, "iTime");
    //给iTimeAddr字段设置数据,是开始运行到当前的计时
    gl.uniform1f(iTimeAddr, totalTimeSec);

    //请求GPU 安当前状态进行绘制
    gl.drawArrays(gl.TRIANGLES, 0, 3);  //三角形类 ,从 buffer0 索引位置开始,绘3个长度的顶点数据.
    //接下一次循环刷新,让loop 函数反复执行
    requestAnimationFrame(loop);    //注意:这里遇到一个坑,如果用setTimeout 作为循环泵,会有严重的卡顿情况
};

//第一次触发执行loop,启动循环
loop();

得到一个全屏颜色交替变化的画面

shadertoy测试GIF.gif

补充

shadertoy网站链接

已实现shadertoyNativePlayer播放器在 github上,可用于借鉴.
几个样例:

  • seascape
  • happyJumping
  • PlanetShadertoy
  • ProteanClouds
  • raymarchingPrimitives
cap01.png

cap02.png

cap03.png

cap04.png

cap05.png

你可能感兴趣的:(shadertoy 移植到本地(1):具体实现)