文件 | 语言 | 大小 | 版本 | 开发者 | 最后更新 | |
lands/frag production | JavaScript | 11.8KB | v1.0.0 | YB | 2024-10-16 | 查看 |
lands/frag source code | JavaScript | 25.3KB | v1.0.0 | YB | 2024-10-16 | 查看 |
const canvas = await Lan.frag(900, 600, {options...}); // canvas width, height and rendering options
const canvas = await Lan.frag(900, {options...}); // auto set canvas height with aspect ratio 3/2
const canvas = await Lan.frag({options...}); // canvas dimensions default to 900 x 600
因为 frag 是蓝紫的核心渲染引擎之一,可以直接通过全局变量 Lan 进行访问,请参考向导程序:域翘曲。
import {frag} from "https://ddzeb.com/lands/frag";
await frag(canvas, {progcode:'shader source', ...});
canvas.render();
const {frag} = await import("https://ddzeb.com/lands/frag");
await frag(canvas, {progcode:'shader source', ...});
canvas.render();
第三方集成的时候,可以单独使用,比如:Lands frag Rendering Engine 这样,动画一直渲染。也可以引入蓝紫任务管理器 runner 进行调度管理,比如:Lands runner Demo。
frag 是蓝紫非常重要的一种渲染引擎,它满足如下渲染范式:
color(p) = f(px, py)
也就是说,我们的渲染程序需要根据画布上每一个点的坐标给出颜色。这样的渲染模型看起来就是一个二维渲染范式,但实际上,我们可以很容易的实现三维渲染,前提是我们手动放置相机以及处理投射关系。
在 frag
背后,使用的是 WebGL 2.0 Fragment Shader 进行渲染,
使用 frag 渲染引擎有两种方式,一种是使用蓝紫调度器进行调用,在蓝紫集成开发环境编写程序都是采用这种方式:
const canvas = await Lan.frag(width, height, {options});
另一种是手动调用 frag 引擎,这时候我们需要手动将 fragment shader 代码通过 optins.progcode 传进去:
const {frag} = await import("https://ddzeb.com/lands/frag");
await frag(canvas, {progcode:'shader code', ...});
无论采用哪种方式,在初始化画布之后,除了常规的画布接口,我们得到的 canvas 对象还具有如下几个额外接口:
该接口为渲染接口,通常我们会调用 Lan.loop(canvas.render)
来进行持续的动画渲染,当然,我们也可以直接调用 canvas.render
来渲染某一帧画面。loop
参数与 Lan.loop
需要的参数一致,同样是一个 JSON 带有如下属性:
loop
开始到现在经过的秒数(float
),去掉了程序暂停时间;loop
开始到现在的帧数,从 0
开始计数。当我们没有传递 loop 参数的时候,frag 会自动生成一个属于自己的 loop 参数并在每次渲染的时候更新参数内容。参见:Lands frag Rendering Engine,在这个程序中,我们调用 canvas.render 的时候没有传任何参数进去。
属性。我们可以通过 run[x]
访问 @run
函数,比如:canvas.run0
访问第 0
个 @run
函数。返回的对象有如下属性或方法:
int
,@run
函数编号,从 0
开始;@run
函数是否活跃,不活跃的 @run
函数将不会运行;@run
函数渲染目标,参考 3.1 节的 @target
标记。使用示例参见 Gaussian Kernel。
更新纹理数据,其中 w 以及 h 为可选参数。
更新 @run 函数输出纹理,data 为 null 时相当于执行清除操作。
进行销毁和清理工作,在蓝紫集成开发环境中不需要调用此函数,我们会自动调用。
在蓝紫集成开发环境中,编写 $FS
程序,即 Fragment Shader,实现渲染。或者在第三方集成的时候通过 options.progcode 将 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
数据,参见示例:域翘曲;@run
函数直接渲染到屏幕之后,意味着渲染流程结束,渲染结果不会进入到 outcome
缓冲,所有后续渲染操作都会停止。示例参见 光线追踪。当没有渲染到屏幕的 @run
函数时,我们会将最后一个 @run
函数的输出渲染到屏幕。
float
,从程序运行起经过的秒数;int
,帧编号(计数),从 0
开始;float
,3.14159265358979323846
float
,PI / 2;float
,PI / 4;float
,PI * 2;float
,图像宽高比(DX / DY);float
,图像宽度;float
,图像高度;vec2
,图像宽度及高度;float
,图像宽度的一半,即 X
中心位置;float
,图像高度的一半,即 Y
中心位置;vec2
,图像宽度及高度的一半,即 XY
中心位置;ivec2
,当前点像素坐标(整数),即 ivec2(gl_FragCoord.xy)
;vec2
,当前点像素坐标(带 0.5 小数),即 gl_FragCoord.xy
;sampler2D
,当前 @run
函数输出的数据,即上一帧数据;sampler2D
,第 x
个 @run
函数输出的数据,每一个 @run
函数渲染完成后数据都会进入到对应的缓冲区;sampler2D
或 samplerCube
类型,第 x
个纹理数据,有可能是图像、视频、数据或者 Cubemap 纹理。[0, 1)
之间的均匀分布随机数;[0, 1]
范围内,不考虑 ASPECT,图像中心点为 [0.5, 0.5]
;X
方向归一化到 [-0.5, +0.5]
,即宽度为 1
,Y
方向按 ASPECT
适配,图像中心点为 [0, 0]
;Y
方向归一化到 [-0.5, +0.5]
,即高度为 1
,X
方向按 ASPECT
适配,图像中心点为 [0, 0]
;X
方向归一化到 [-1.0, +1.0]
,即宽度为 2
,Y
方向按 ASPECT
适配,图像中心点为 [0, 0]
;Y
方向归一化到 [-1.0, +1.0]
,即高度为 2
,X
方向按 ASPECT
适配,图像中心点为 [0, 0]
;p
点的上一帧颜色;x
个 @run
输出的颜色;p
点在第 x
个 @run
函数输出的颜色;c
写入到下一帧;c
后写入到下一帧,alpha
代表累加次数,在显示的时候实际渲染的颜色为 vec4(color.rgb / color.w, 1)
,也就是用 color.w
对 rgb
颜色进行归一化处理。参见向导程序 路径追踪。在 $FS 代码中,可通过 #include 指令调取任意 GLSL 代码库。
$fs
#include util, ray, color
多个扩展库可使用半角逗号和或空格进行分隔,也可以采用多个 #include 指令来完成。除了导入蓝紫的 GLSL 标准库,也可以导入第三方的外部库,只需要指定绝对全局地址即可。引入 GLSL 开发库的 #include 指令不一定非要写在程序开头,但不能写在函数体内。我们还可以通过如下指令一次性导入全部蓝紫 GLSL 标准库:
$fs
#include all
通过在 options 参数里指定 textures 可以加载纹理,可以有如下几种方式:
textures : 'T01,T02'
textures : {source:'T01', width:50, flipy:false, wrap:'clamp', filter:'nearest'}
textures : ['T01', {source:video, mute:false}]
缺省情况下,图片纹理都会将 y 方向进行翻转,除非设置了 flipy:false 参数。frag 内置的纹理支持只允许图片(包括 HDRI 以及 Cubemap)、Float32Array 以及 Uint8Array 三种,需要加载音频、视频的时候可以引入 media 库进行处理。比如,音频加载为纹理,可以参考示例程序:Audio Visualization:
const media = (await Lan.import('media')).media();
const audio = await media.audio('A13');
const canvas = await Lan.frag(900, 600, {
textures : [{source:audio.fft_buffer()}, 'T1']
});
对于数据纹理,当我们可以使用 Float32Array 作为数据纹理源,通常需要设置好宽度(width)或高度(height),不设置尺寸的时候,认为宽度为整个数据长度:
const data = new Float32Array([1,2,3,4,5,6]);
textures : {source:data, width:2}
这样就得到了一个 2x3
大小的数据纹理。关于数据纹理的使用,请参考示例程序:Gaussian Kernel。
蓝紫内置资源中有颜色桶支持,我们可以使用 media 库加载颜色桶,使用代码请参考官方 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
。我们可以通过 canvas.outcome 函数直接写入输出缓冲数据。具体例子请参见示例程序:Paint Flow。
另外,当输出缓冲的数据被渲染到屏幕的时候,如果发现 alpha 值大于 1,我们会将 rgb 值除以 alpha,同时将 alpha 设置为 1,即:
if (color.w > 1) {
color.rgb /= color.w;
color.w = 1.0;
}
也就是说,frag 天然支持渐近渲染。如果我们在运行过程中,因为 Uniform 值产生了改变,需要重新进行渐近渲染累积,这时候可以使用 canvas.outcome 函数清除之前的累积数据:
canvas.outcome(0, null);
在渐进渲染静态图像的时候,如果渲染流程特别耗时,我们可以采用分块渲染来提高帧率,不至于让 WebGL 崩溃而无法渲染。分块渲染参数为 tilesize,通过该参数指定分块大小:
tilesize : 300 // 300 x 300
tilesize : '300x200' // 300 x 200
tilesize : '400,1' // 400 x 1
当只有一个数字的时候,我们按照正方形处理。当同时指定宽高的时候,可以使用任意非数字符号分割宽高数字。详细使用例子,请参考向导程序:路径追踪。