shaderToy就是glsl ,本文是小白文,标题可能有点不对。 上次翻译了半篇教程关于shaderToy的使用,这次主要是补充一些细节,如何手动把shaderToy的代码拿来使用。自动的大概有大佬已经写过了,这里更笨拙一些。
最终结果是完全原生的glsl ,没有需要借助外部编译的。
1 本地环境准备
当然是copy代码到自己的编辑器上,建议命名文件为 .frag ,这样会有良好的语法检查。如果没有,参考这篇glsl调试。
mac也是一样的。语法检查一定要有,虽然在字符串之前加上注释 /*glsl*/也可以获得高亮效果,但是无法检查错误。 如果你本地已经有了很好glsl编辑环境,那就很好了。
多说一句,frag 和 vert的语法检查大概比webgl可用的glsl还要严格一些,如果他们检测没问题,那在webgl中也没问题,反过来不一定成立。
如果,你发现已经通过语法检查了,但是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下来,然后一个个来处理。
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
)
如果你使用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后绘制到屏幕
结语
以上所说,基本上够用了,只是原版照搬而已。
当然,最好是看明白代码里写了什么,这样好灵活运用。