这是蓝紫非常重要的一种渲染引擎,它满足如下渲染范式:
color(p) = f(px, py)
也就是说,我们的渲染程序需要根据画布上每一个点的坐标给出颜色。这样的渲染模型看起来就是一个二维渲染范式,但实际上,我们可以很容易的实现三维渲染,前提是我们手动放置相机以及处理投射关系。
canvasfs
使用 WebGL 2.0 Fragment Shader 进行渲染,
canvasfs
的程序由两部分组成,首先是一段 $JS
程序作为主控程序,另外就是运行在 GPU 上的 $FS
(Fragment Shader)程序。JS 主控程序负责初始化画布,并调取可能使用到的纹理等等资源。
const canvas = await Lan.canvasfs(width, height, options);
一个典型的 JS 主控程序如下:
const canvas = await Lan.canvasfs(1800, 1200, {
preludes : 'base,cmpx',
replaces : {from:'to'},
textures : ['T02', {source:'T03', mipmap:true, flipy:false}],
texflipy : true,
painters : 'P1,P2',
tilesize : 300,
});
await Lan.loop(canvas.render);
初始化画布之后,除了常规的画布接口,我们得到的 canvas 对象还具有如下几个额外接口:
该接口为渲染接口,通常我们会调用 Lan.loop(canvas.render)
来进行持续的动画渲染,当然,我们也可以直接调用 canvas.render
来渲染某一帧画面。loop
参数与 Lan.loop
需要的参数一致,同样是一个 JSON 带有如下属性:
loop
开始到现在经过的秒数(float
),去掉了程序暂停时间;float
)增量;loop
开始到现在的帧数,从 0
开始计数。属性。我们可以通过 run[x]
访问 @run
函数,比如:canvas.run0
访问第 0
个 @run
函数。返回的对象有如下属性或方法:
int
,@run
函数编号,从 0
开始;@run
函数是否活跃,不活跃的 @run
函数将不会运行;@run
函数渲染目标,参考 2.1 节的 @target
标记。使用示例参见 Gaussian Kernel。
编写 $FS
程序,即 Fragment Shader,实现渲染。
在 $FS
程序里可以编写一个或多个按照顺序执行的 @run
函数,编号从 0
开始,它们是 Fragment Shader 的渲染函数。每一个 @run
函数渲染结束后都会将数据写入到对应的输出纹理(outcome
),比如,第 0
个 @run
函数输出到 outcome0
。屏幕(画布)上显示的结果是最后一个被执行的 @run
函数的输出。
@run
函数基本形式如下:
@run {
vec3 color = vec3(0);
// do some calculations to get a correct color
FLUSH(color);
}
大概意思是,在 Fragment Shader 的 @run
函数里做一些计算,然后输出颜色。我们还可以使用 @active
来决定其是否活跃,以及给 @run
提供一个注释性质的名字或描述:
@run @active(false) post process {
// code ...
}
默认的 @active
值为 true
,表示正常运行该函数。此外,当我们需要极致渲染性能的时候,可以使用 @target
告诉渲染引擎此 @run
函数的渲染目标:
@run @target(screen) this would be the LAST rendering action {
// code ...
}
渲染目标的可能取值如下:
outcome
纹理中;outcome
纹理中,好处是节省了数据从缓冲拷贝到纹理的过程,但因纹理被写锁定,@run
函数不能读取自己的 outcome
数据,参见示例:域翘曲;outcome
纹理中进行累加,特别适合渐进渲染。与 texture 情况类似,@run
函数不能读取自己的 outcome
数据。具体使用示例请参见:折射;@run
函数直接渲染到屏幕之后,意味着渲染流程结束,渲染结果不会进入到 outcome
缓冲,所有后续渲染操作都会停止。示例参见 光线追踪。当没有渲染到屏幕的 @run
函数时,我们会将最后一个 @run
函数的输出渲染到屏幕。
float
,从程序运行起经过的秒数;float
,自上一帧到本次渲染经过的秒数;int
,帧编号(计数),从 0
开始;{curx, cury, oldx, oldy, hitx, hity, down, amid}
,其中的坐标(前六个参数)均为 float
,-2e10
代表 undefined
。down
以及 amid
为 int
,取值 0 和 1。float
,3.1415926535897932
float
,1.5707963267948966
float
,6.2831853071795864
float
,图像宽高比(DX / DY);float
,图像宽度;float
,图像高度;vec2
,图像宽度及高度;float
,图像宽度的一半,即 X
中心位置;float
,图像高度的一半,即 Y
中心位置;vec2
,图像宽度及高度的一半,即 XY
中心位置;{int x, int y, ivec2 xy, int id}
,当前点的坐标(像素坐标),其中 id = y * DX + x
;vec2
,当前点的坐标(像素坐标),其值与 COORD.xy
相等;vec2 UV11 = UV / DXY
,当前点坐标归一化到 [0, 1]
范围内,不考虑 ASPECT,图像中心点为 [0.5, 0.5]
;vec2 UVx1 = (UV - CXY) / DX
,当前点坐标 X
方向归一化到 [-0.5, +0.5]
,即宽度为 1
,Y
方向按 ASPECT
适配,图像中心点为 [0, 0]
;vec2 UVy1 = (UV - CXY) / DY
,当前点坐标 Y
方向归一化到 [-0.5, +0.5]
,即高度为 1
,X
方向按 ASPECT
适配,图像中心点为 [0, 0]
;vec2 UVx2 = (UV - CXY) / CX
,当前点坐标 X
方向归一化到 [-1.0, +1.0]
,即宽度为 2
,Y
方向按 ASPECT
适配,图像中心点为 [0, 0]
;vec2 UVy2 = (UV - CXY) / CY
,当前点坐标 Y
方向归一化到 [-1.0, +1.0]
,即高度为 2
,X
方向按 ASPECT
适配,图像中心点为 [0, 0]
;sampler2D
,当前 @run
函数输出的数据,即上一帧数据;sampler2D
,第 x
个 @run
函数输出的数据,每一个 @run
函数渲染完成后数据都会进入到对应的缓冲区;sampler2D
或 samplerCube
类型,第 x
个纹理数据,有可能是图像、视频、数据或者 Cubemap 纹理。[0, 1)
之间的均匀分布随机数;uint
(0 ~ 0xFFFFFFFF)随机数;p
点的上一帧颜色;x
个 @run
输出的颜色;p
点在第 x
个 @run
函数输出的颜色;c
写入到下一帧;c
后写入到下一帧,alpha
代表累加次数,在显示的时候实际渲染的颜色为 vec4(color.rgb / color.w, 1)
,也就是用 color.w
对 rgb
颜色进行归一化处理。参见向导程序 路径追踪。我们可以通过 preludes
加载扩展库,蓝紫内置了几个常用的扩展库,用于一些特定的场景。比如,base 提供一些常用操作,cmpx 提供二维复数操作。某些扩展库里有模版函数,相关情况请参考博客:蓝紫 GLSL 模版函数。加载扩展库可以有如下几种方式:
preludes : 'base,pale,/lib/url'
preludes : ['base,pale', '/lib/url']
以下是蓝紫内置的 GLSL
扩展库,可直接通过名称导入,名称不区分大小写:
通过在 options 参数里指定 textures 可以加载纹理,可以有如下几种方式:
textures : 'T01,T02'
textures : {source:'V01', mute:false, wrap:'mirrored'}
textures : ['T01', {source:'T02', mipmap:true, blur:8, scale:2}]
其中的 mipmap
、blur
以及 scale
是专门针对图片纹理的选项,参见向导程序:KIFS 分形。加载 HDRI 或 Cubemap 也是同样的方式:
textures : 'H01,C01'
除了可以将图像、视频加载为纹理外,还可以将音频加载为纹理:
textures : 'A01'
textures : {source:'.....', audio:true, loop:true}
当 source 设置为 'camera' 时,我们会试着打开设备的照相机:
textures : 'camera'
textures : {source:'camera'}
我们还可以将一个 HTMLCanvasElement 作为纹理源,当 live 为 true 时,会实时更新,具体情况请参考示例程序:Canvas Texture。
textures : {source:canvas_element, live:true}
最后,是数据纹理,我们可以使用 Float32Array 作为数据纹理源,设置好宽度(width)或高度(height),不设置尺寸的时候,认为宽度为整个数据长度:
const data = new Float32Array([1,2,3,4,5,6]);
textures : {source:data, width:2}
这样就得到了一个 2x3
大小的数据纹理。关于数据纹理的使用,请参考示例程序:Gaussian Kernel。
蓝紫内置资源中有颜色桶支持,我们可以在 options 中指定颜色桶(painters),然后通过 color 附加库中的 painter 函数获取颜色。使用代码请参考官方 Demo:WebGL Painters。
在 options
里指定 replaces
项,可在 WGSL 代码中进行字符串替换操作。
{
replaces : {NUMBER1:1000},
}
通过这样的设定,GLSL 代码中所有的 NUMBER1
都将被替换为 1000
。使用例子请参考向导程序:二维域翘曲渲染。
通过字符替换,我们可以将 JS 中的值“代入”到 Shader 代码中,这样的好处是快速,Shader 会将那些代入的值作为常量。但这样就无法在程序运行过程中将 JS 中计算得到的数据传入 Shader 程序中。我们提供的 uniforms
就可以解决这类问题。通过在 options
声明:
{
uniforms : { UniformName : InitialValue, ... },
}
这样我们就可以在 Shader 程序中使用 UniformName
了。要在 JS 中更新,只需要这样:
canvas.uniforms.UniformName = new_value;
需要注意的是,UniformName 必须以大写字母开头,并且目前只提供 float
类型的 uniform 变量支持。详细使用例子,请参考向导程序:光线追踪。
我们在 $FS
程序中通过 @run
函数进行渲染并输出到自己的缓冲 outcome
中,比如,第 0
个 @run
函数输出到 outcome0
。在 @run
函数渲染过程中,有可能会用到之前渲染输出的数据,我们通过 PRIOR
函数可以很方便的取得。但在 @run
函数首次运行的时候,这些数据都是空的,即,RGBA
全部为 0
。我们可以通过 options.initfill
来为第 0
个 @run
函数提前准备数据。
initfill : (x, y) => {
var r, g, b, a;
// calculate ...
return [r, g, b, a];
}
具体例子请参见示例程序:Paint Flow。
在渐进渲染静态图像的时候,如果渲染流程特别耗时,我们可以采用分块渲染来提高帧率,不至于让 WebGL 崩溃而无法渲染。分块渲染参数为 tilesize,通过该参数指定分块大小:
tilesize : 300 // 300 x 300
tilesize : '300x200' // 300 x 200
tilesize : '400,1' // 400 x 1
当只有一个数字的时候,我们按照正方形处理。当同时指定宽高的时候,可以使用任意非数字符号分割宽高数字。详细使用例子,请参考向导程序:路径追踪。
缺省情况下,我们都是采用 highp
精度的数据,可以通过在 options
中配置 bitlevel
调整数据精度:
另外,输出纹理的精度,缺省情况下为 32 位精度,缺少 EXT_float_blend
扩展的时候(大部分手机上)为 16 位精度。我们可以通过设置 force16f
将精度强制设置为 16 位。大部分情况下,我们不需要对精度进行调整,精度调整对程序性能的影响不是很显著。