Shader学习-从Shader新手到简单2d实时全局光照#
1. 背景#
学过一些初级的图形学/渲染。
折腾bevy途中碰到一个2d全局光照例子,看着还挺唬人的。希望搞懂其原理。
zaycev/bevy-magic-light-2d
其中shader早就有大概的了解,但一直没进一步学习,知道涉及很多trick和数学。
希望更进一步,搞懂shadertoy的基本玩法和一些特效的原理,搞懂bevy的基本shader流程。
步骤
搞清楚shadertoy和基本的glsl shader是咋回事儿
初步学习games202<<高质量实时渲染>>课程
WebGPU初步学习
看懂Bevy的一个2d全局光照项目
山寨一个sdf全局光照插件
2. 资料#
shader
https://www.shadertoy.com/
https://thebookofshaders.com/
https://www.youtube.com/watch?v=f4s1h2YETNY
https://www.youtube.com/@TheArtofCodeIsCool/videos
https://clauswilke.com/art/post/shaders
https://iquilezles.org/articles/
计算器
https://www.desmos.com/calculator
https://www.desmos.com/3d
games202
https://sites.cs.ucsb.edu/~lingqi/teaching/games202.html
https://www.bilibili.com/video/BV1YK4y1T7yY
3. 基础#
shader有不同的类型和框架。
最简单的一种fragment shader可以理解为一个回调函数。
常见流程是先摆好场景,渲染出图,把图喂给你这个shader。
在shader里处理图中的每一个像素。
例如在shader里把所有像素都乘0.5,整个图就变暗。
诸如此类,可以做出各种特效。
也可以无输入,纯用shader画各种酷炫的图。
shadertoy有大量这种。
直接开始在 https://www.shadertoy.com/ 写代码
看这个聪明绝顶教程
https://www.youtube.com/@TheArtofCodeIsCool/videos
看他最老的几个基本的教学,涵盖了一些基本的画图套路。
有耐心的可以一直看下去。
看这个最简单的sdf
https://www.shadertoy.com/view/3ltSW2
学到一些画图技巧
自己再找找资料学学。
这样折腾两天,搞清楚基本的套路,可以搞一些图了。
https://www.shadertoy.com/view/wfl3z8
https://www.shadertoy.com/view/M3GBDt
https://www.shadertoy.com/view/MXyfDt
4. Ray Marching#
4.1 初步#
以前我们在学pbr的体渲染时已经学过ray marching。
在有些无法轻易精确计算的场合,直接模拟光的推进并采样,能快速得到一个不完美但足够好的结果。
见 https://youtu.be/PGtv-dBi2wE?t=274
总体就是光从相机p1
以某个方向a
往场景里打,每次前进一个距离,直到碰到物体或者超过限制的前进次数放弃。
计算目前光的位置(初始为
p1
)到场景中物体的最短距离d
。如果d
小于某个很小的阈值,就认为现在光正好打到了某个物体表面,结束。否则,这个距离是一个安全的距离,我们可以把光在
a
方向往前推进d
到达p2
,而绝不会穿过任何物体。
如此不断循环推进,正常情况会推进到物体和光的接触点。
理解原理后可以尝试不看他代码自己写一个大概的。
用ray marching算出每条光与场景的交点,并想办法画出来。
https://www.shadertoy.com/view/wfXGR4
4.2 简单光照#
定一个光源,计算交点的normal,再计算normal方向的分量,作为光强度的因子,再加点衰减啥的,可以得到基本的光照。
normal的计算见 https://iquilezles.org/articles/normalsSDF/
结合聪明绝顶的视频和代码。
我最容易理解的normal近似计算方法
自己画一下图就很清楚。
normal对应的是交点处的微小平面。
交点实际位于a的沿normal方向的距离d处。d是4.1算法中的d。
从交点处往xyz轴延申一个微小的距离得到三个点。
计算这个三个点到场景的距离d1/d2/d3。
normal的近似值其实就和d1/d2/d3统一减去d后的比值对应。
那么normal大概就是这种形式
float d = dist_to_scene(p);
vec2 e = vec2(0.01, 0);
vec3 normal = vec3(dist_to_scene(p + e.xyy) - d,
dist_to_scene(p + e.yxy) - d,
dist_to_scene(p + e.yyx) - d);
normal = normalize(normal);
简单的阴影需要再做一次ray marching,从交点往光源推进。
如果距离小于交点到光源的距离,说明中间碰到了物体,就是在阴影中。
简单乘个0.1之类即可。
5. SDF(Signed Distance Function/Field)#
https://en.wikipedia.org/wiki/Signed_distance_function
sdf是一个函数,在一个场景中,给出任何一个点,sdf可以得出这个点到场景中最近的一个微小表面的距离。
如果值是正数,那么点在面的外部
,如果是负数,点在面的内部
。
需要把场景分成各种基本形状,每种基本形状都有自己的sdf。
之前看过这个 https://www.shadertoy.com/view/3ltSW2
是最简单的圆形的sdf。
我的 https://www.shadertoy.com/view/wfl3z8
各种基本形状的sdf总结见 https://iquilezles.org/articles/distfunctions2d/
我们要搞懂长方形的sdf。
先看无位移无转动的长方形。
推导见
https://numbersmithy.com/
https://www.youtube.com/watch?v=62-pRVZuS5c
其中的max挺搞的,vec2 max(vec2 a, float b)
这种max是用b分别去比a的xy然后得到新的vec2。
不知道的话有点看不懂。
自己实现一下
得到 https://www.shadertoy.com/view/tfl3Dr
只要在 https://www.shadertoy.com/view/wfl3z8 基础上实现一下长方形的sdf替换圆的sdf即可,非常优雅。
对于有位移有旋转的长方形,先做两个操作,把长方形移到(0,0),用矩阵把它转正。
再把请求的位置也做一样的变换。
最后求标准的sdf即可。
得到 https://www.shadertoy.com/view/WffGWH
5.1 sdf-raymarching#
简单结合sdf和raymarching就能搞出非常唬人的效果。
https://iquilezles.org/articles/raymarchingdf/
软阴影
https://iquilezles.org/articles/rmshadows/
https://www.shadertoy.com/view/lsKcDD
2d软阴影
https://www.shadertoy.com/view/4dfXDn
sdf/采样/全局光照
https://www.shadertoy.com/view/lldcDf
6. games 202#
这一节其实不用看。不过对shader学习很有帮助。
因为开始不清楚各种关系,所以学了一下。
其实上一节最后的sdf全局光照基本就是我们最开始的学习目标了。
也就是这个项目的基本原理
zaycev/bevy-magic-light-2d
而games202主要走的是预计算,跟我们的目标是两个方向。
看完基本的shadertoy和sdf后,对于实时全局光照仍然是一头雾水的。
只是听说实时全局光照会用到sdf,但是完全不懂怎么联系起来的。
想起来前几年学过一个课程叫games101,讲了初步的渲染。然后学了pbrt离线光线追踪。
games101后面还有一个games202课程,讲的正是实时渲染,会涵盖我们的问题。
当时学离线的光线追踪人已经麻了,没有再继续学实时。
现在得补起来。
https://sites.cs.ucsb.edu/~lingqi/teaching/games202.html
6.1 作业0#
就是熟悉一下整体流程。
大概就是:
three.js加载模型,加载材质,加载shader。
three.js调opengl接口画图。
我们写shader,实现最简单的Blinn–Phong。
Blinn–Phong非常简陋。但是粗看也能像那么回事儿,作为教学很ok了。
就是简单模拟ambient/diffuse/specular三个光,加起来。
需要修改的代码都在作业pdf中,看懂没问题。
不需要看细节,看懂大致流程和shader即可。
运行方法也在pdf中。
有时候刷新不显示模型。
https://games-cn.org/forums/topic/zuoye0-jieguobuwendingyoushimoxingxianshibuquan/
6.2 硬阴影#
shadow mapping
https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
原始的shadow mapping是硬阴影。
步骤
以光源的视角渲染整个场景,不计算颜色,只保存每个像素对应的3d坐标的z值z1。每个像素也对应一条光。
保存这张图,即shadow map。坐标离光源越远,z值越大。把shadow map显示出来可以看到越远处越亮。
以正常的相机视角渲染,对于每个像素,对应到一个3d坐标a,手动计算a在光源视角下的当前z值z2。
比较z2和z1。如果z2大于z1,说明在这条光的方向上,存在一个离光源更近的3d坐标,即当前坐标被某个物体遮挡了,即在阴影中。
加bias解决明显的波纹。
但是会造成浮空,误杀了边界处阴影。
6.3 软阴影#
https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf
https://www.realtimeshadows.com/sites/default/files/sig2013-course-softshadows.pdf
完美的点光源和平行光没有软阴影。
面光源产生软阴影。
软阴影更自然,现实中看到的基本都是软阴影。
PCF(percentage closer filtering)
在上述比较z2和z1时,同时比较该点周围,做平均。即filtering。
例如跟shadow map中当前点比较,在阴影得到0。跟左边一个像素比较,不在阴影得到1。平均就能得到0.5。
可以取周围3x3或7x7之类的平均值。
这样在硬阴影的边界处能得到一堆0/1之间的数。
相比硬阴影的非0即1,就得到了软阴影。
具体怎么取周围的点,可以写最简单的周边3x3之类,效果简陋。
可以用各种采样函数,水就很深了。
教程里提供了经典的泊松圆盘采样和uniform采样。
一用上pcf,那种实时阴影颗粒的感觉就出来了。
PCSS(percentage closer soft shadows)
有物理现象,物体距离障碍物越近的地方,产生的阴影越硬。
拿个笔立在桌面就能看到,靠近桌面的部分产生的阴影很硬,远端的阴影逐渐散开。
对不同距离的阴影做不同大小的filtering可以做出这种效果。
shadow map严格来说是点光源。
软阴影是面光源,使用点光源的shadow map来近似。
所以shadow map中的像素对应的一个3d坐标,实际上可能是光源多条光线的blocker。
variance soft shadow mapping
6.4 作业1#
要求实现shadow mapping/pcf和pcss。
6.4.1 shadow mapping#
单纯的Blinn–Phong是不能把阴影投到别的模型上的。
shadow map可实现最简单的阴影投射。
loadOBJ会指定PhongMaterial,起material和shadowMaterial两个材质。
对应phongShader和shadowShader两个shader,即模型本身材质的渲染和阴影渲染。
engine.js里会初始化光源数据。
光源是固定的,只有一个光源。
复习mvp矩阵
http://www.xiongchen.cc/blog/PBRT.html#transformation
DirectionalLight.js中
用webgl的api创建mvp矩阵
mat4.ortho/mat4.lookAt
在WebGLRenderer.js的render()中会先做shadow pass,对应ShadowMaterial,渲染shaodow map。
也就是对应了shadow的shader。
然后camera pass,对应PhongMaterial,也就对应phong的shader。
vertex shader在fragment shader之前执行
shadowVertex.glsl中
aVertexPosition是顶点的模型坐标数据。
uLightMVP * vec4(aVertexPosition, 1.0);
这样mvp变换到光源的clip空间。
uLightMVP是上面算好的光源的mvp变换。
shadowFragment.glsl中
总之得把基本流程看熟,opengl基本数据要搞清楚,那几个空间变换要搞清楚。
gl_FragCoord的xy是从0开始,单位是窗口的像素,比如范围为[0, 1024]。
gl_FragCoord.z的默认范围是[0, 1]
pack函数把z值编码到一个vec4中。
opengl的float类型是IEEE 754单精度浮点数,32bit。
gl_FragColor默认填入的范围是[0, 1]。
texture默认是rgba格式,每个通道8bit,也就是[0, 255]。
那么如何存这个[0, 1]的z值?如果直接放到一个通道,那么float会变成[0, 1]之间的256级的某个数,精度退化到不可接收。所以要利用4个通道一共32bit来手动存这个z值。
他的具体原理我也没看懂,先不管了,直接用。
搜pack float to rgba
能找到一些资料。
https://aras-p.info/blog/2009/07/30/encoding-floats-to-rgba-the-final/把ShadowMaterial初始化的light.fbo给去掉,把WebGLRenderer中的Camera pass去掉,可以调试shadow pass的结果。直到可以画出正确的shadow map。
mvp变换后得到的xyz值还需手动除以w,才能得到范围[-1, 1]。如果传给gl_Position是不需要手动normalize的,非常坑。
手动normalize后的z值和gl_FragCoord.z是相等的。正因如此我们才能手动算z值跟shadow map比大小。
https://gamedev.stackexchange.com/questions/66110/when-is-the-z-coordinate-normalized-in-glsl
phongVertex.glsl
vFragPos = (uModelMatrix * vec4(aVertexPosition, 1.0)).xyz;
这样乘上uModelMatrix得到世界坐标。
计算blinnPhong就是用统一的世界坐标。gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);
这样mvp变换到camera的clip空间。
phongFragment.glsl
把顶点做光源的mvp变换并手动normalize。再从[-1, 1]手动映射到[0, 1]。
用去xy查shadow map,再用z去比较即可。
6.4.2 pcf#
phongFragment.glsl
打开PCF,调用poissonDiskSamples生成采样点。
生成的采样点需要按实际情况调参缩放,放的越大,阴影越软。
采样函数里的参数也可以调,找一组舒服的数值即可。
对于每个采样点,对比shadow map,得到0/1。
最后取平均即可
6.4.2 pcss#
phongFragment.glsl
打开PCSS,调用poissonDiskSamples生成采样点。
对于每个采样点,如果在阴影里,累计z值。最后算出平均z值,也就是d_blocker。
按相似三角形算出半影的宽度,对应阴影的软硬程度。
计算宽度用的是z值,这里按我理解z值并不是线性的,所以宽度也不是线性的,并不是真的相似三角形。
其中有多个参数可调,效果天差地别,需要费劲调试,搞懂各个步骤,搞懂各个数值的范围和效果。
最后用这个软硬程度值调用PCF即可。
到此基本的fragment shader已经有点会了。
6.5 实时环境光照#
IBL(Image based lighting)
环境光shading。
渲染方程的近似,把Li项拿到外面。
Li的积分可以预计算。
环境光的阴影
precomputed radiance transfer
频域和滤波
基函数
spherical harmonics
https://en.wikipedia.org/wiki/Spherical_harmonics
是定义在球面上的一系列2d函数,样子和傅立叶变换的结果很相似。
https://cseweb.ucsd.edu/~ravir/prtsurvey.pdf
大致的原理估计就是利用球谐函数,类似傅立叶变换那样,可以有效地分解还原光照相关的数据。
然后可以快速应用在实时光照的预计算上。
到此暂停games202。因为了解到此时是跟sdf走的两条路了。
后续有缘再见。
7. WebGPU#
https://en.wikipedia.org/wiki/WebGPU
https://www.w3.org/TR/webgpu/
https://sotrh.github.io/learn-wgpu/
gfx-rs/wgpu
https://www.bilibili.com/video/BV12i4y1d7KQ
https://webgpufundamentals.org/
WebGPU感觉内容巨多但资料稀少,慢慢学吧。
要搞清楚各种概念和他们的关系。
WebGPU是全新的一套设计和api。
WebGPU是基于Vulkan这些底层api的,它最终会调用Vulkan/Dx等的api。
WebGPU有多种语言实现,wgpu是WebGPU的rust实现,重量级的项目。
浏览器基本都有js的原生支持,非常方便。官方文档就是js版本。
可以找个js版本的教程或书快速过一遍WebGPU基本套路。
WGSL是WebGPU唯一支持的shader语言。但wgpu支持了其他常见的shader语言,会把他们转成WGSL。
7.1 WGSL#
https://www.w3.org/TR/WGSL
https://google.github.io/tour-of-wgsl/
https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl.html
内容多但资料少
多用最新的ai搜一下,有非常不错的资料。
https://grok.com/chat/d4e9cb6b-a501-4a76-84af-06f63c7881fc
group(0)到底啥意思?
写0画不出来,写1崩溃,写2正常。
https://chatgpt.com/c/67cea5fb-6dd0-800e-8256-5a87491d723b
#import
不是标准wgsl。是bevy的扩展。
import的是shader代码而不是rust代码。
#import bevy_pbr::mesh_view_bindings::globals
这个globals是mesh_view_bindings.wgsl里的
@group(0) @binding(11) var<uniform> globals: Globals;
Globals的更新在bevy_render/src/globals.rs,总体在RenderPlugin下。
但是绑定写死为0,不一定能用?
7.2 compute shader#
https://www.w3.org/TR/WGSL/#compute-shader-workgroups
https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html
compute shader可以并行执行任务。
workgroup
指一组并行调用。
workgroup grid
指一组调用里的所有点,每个点是一个三维的形式。
@compute @workgroup_size(8, 8, 1)
workgroup_size指定xyz三个维度大小。默认值为1。
这样每个group会计算8x8x1=64次。
内置变量local_invocation_id的值就是(x, y, z)。
所有workgroup又做成一个三维的结构,每个workgroup里是三维的一个个调用。
整个是两层三维结构。
内置变量workgroup_id,是外层的三维坐标。
内置变量global_invocation_id是每个调用的id。
由workgroup_id,workgroup_size和local_invocation_id算得。
pass.dispatchWorkgroups(4, 3, 2)
如果这样调用,就是执行4x3x2个8x8x1的group,一共会有4x3x2x8x8x1=768次调用。
内置变量num_workgroups是调用dispatchWorkgroups传的参数值。
大概就是本次计算的规模,workgroup每个维度的数量。
在GPUDevice.limits中各种硬件最大值。
例如maxComputeWorkgroupSizeX:256
。。。
8. Bevy shader#
首先会发现一个问题,把glsl的fragCoord.x作为color返回,显示的渐变不均匀。
立即会怀疑bevy对color空间有什么魔改。
后来看到这个,果然是的。需要加个色彩空间的转换。
bevyengine/bevy#8937
bevy-magic-light-2d这个项目最后的shader里也实现了lin_to_srgb这个函数。
我的一个最简例子
实现 https://www.shadertoy.com/view/lldcDf 这样的场景
做一个长方形
起一个material,绑定参数。
material指定fragment_shader
shader移植之前学到的sdf-raymarching。
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
sprite::{Material2d, Material2dPlugin},
};
fn main() {
App::new()
.add_plugins((DefaultPlugins,
Material2dPlugin::<SimpleMaterial>::default(),))
.add_systems(Startup, setup)
.add_systems(Update, update_material)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<SimpleMaterial>>,
) {
// Spawn a 2D entity with the mesh and material
commands.spawn((
Mesh2d(meshes.add(Rectangle::default())),
MeshMaterial2d(materials.add(SimpleMaterial {
color: LinearRgba::WHITE, //LinearRgba::BLUE,
time: 0.0,
})),
Transform::default().with_scale(Vec3::splat(512.)),
));
// Add a camera
commands.spawn(Camera2d);
}
// Define the shader material
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
//#[uniform(0)]
struct SimpleMaterial {
#[uniform(0)]
pub color: LinearRgba,
#[uniform(0)]
pub time: f32,
}
impl Material2d for SimpleMaterial {
fn fragment_shader() -> ShaderRef {
// The WGSL file that contains our shader code.
"shader/sdf_light_1.wgsl".into()
}
}
fn update_material(time: Res<Time>, mut materials: ResMut<Assets<SimpleMaterial>>) {
// Update all TimeMaterials with the current time in seconds.
for material in materials.iter_mut() {
material.1.time = time.elapsed_secs();
}
}
// sdf_light_1.wgsl
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
struct SimpleMaterialUniform {
color: vec4<f32>,
time: f32,
};
// Define a uniform with our color.
// todo:group到底怎么配?
@group(2) @binding(0)
// var<uniform> u_color: vec4<f32>;
var<uniform> my_material: SimpleMaterialUniform;
var<private> W = 1.2 / 2.0; // 10.0;
var<private> H = 0.8 / 2.0; // 6.0;
var<private> X = 0.0;
var<private> Y = 0.0;
var<private> ROTATE = 0.0;
fn sdf_circle(p: vec2<f32>, r:f32 ) -> f32 {
return length(p) - r;
}
fn sdf_box(p: vec2<f32> ) -> f32 {
// 放到0,0
var pp = p - vec2(X, Y);
// 转正
pp = mat2x2f(cos(ROTATE),-sin(ROTATE), sin(ROTATE),cos(ROTATE)) * pp;
let q = abs(pp) - vec2(W/2.0, H/2.0);
// > 0
let d_outside = length(
vec2(max(q.x, 0.0), max(q.y, 0.0))
);
// < 0
let d_inside = max(q.x, q.y);
return d_outside + min(d_inside, 0.0);
}
// 如果此obj离得更近,标记为此obj。
fn check_obj(
dist: ptr<function, f32>, color: ptr<function, vec3<f32>>,
dist_to_obj: f32, obj_color: vec3<f32>) {
if(dist_to_obj < *dist){
*dist = dist_to_obj;
*color = obj_color;
}
}
fn query_scene(pos: vec2<f32>, color: ptr<function, vec3<f32>>, dist: ptr<function, f32>) {
*dist = 1e9;
*color = vec3(0.0);
check_obj(dist, color, sdf_box(pos), vec3(0.0, 0.0, 0.0));
check_obj(dist, color,
sdf_circle(pos - vec2(0.1* sin(my_material.time), 0.2* cos(my_material.time)), 0.1),
vec3(0.1, 0.1, 0.2));
check_obj(dist, color,
sdf_circle(pos - vec2(0.6, 1.4* cos(my_material.time + 0.2)), 0.2),
vec3(0.0, 0.1, 0.3));
check_obj(dist, color,
sdf_circle(pos - vec2(-0.6, .2* sin(my_material.time + 0.3)), 0.2),
vec3(0.2, 0.5, 0.3));
check_obj(dist, color,
sdf_circle(pos - vec2(-2.0, .33* cos(my_material.time + 0.3)), 0.2),
vec3(0.9, 0.0, 0.0));
}
fn ray_march(pos: vec2<f32>, dir: vec2<f32>, color: ptr<function, vec3<f32>>) {
var p = pos;
let org_pos = pos;
for(;;) {
var dist = 0.0;
// 走到p,得到离p最近的物体的颜色。
query_scene(p, color, &dist);
// 如果离物体非常近,视为碰到物体,返回。
if dist < 1e-2 {
// 这里可以做一些魔改
// *color *= 1.1 / pow(length(p - org_pos), 2.0);
*color *= clamp(4.1 / pow(length(p - org_pos), 2.0), 0.0, 3.0);
return;
}
// 如果迟迟碰不到物体,返回黑色。
if dist > 1e4 {break;}
// 走一步
p += dir * dist;
}
*color = vec3(0.02);
}
fn random (st: vec2<f32>) -> f32 {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))*
43758.5453123);
}
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
// [0, screen_width] norm to [-1, 1]
// vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y;
var uv = (mesh.uv * 2.0 - 1.0) * 1.0;
uv *= 2.0;
// 动起来
W *= abs(sin(my_material.time * 0.1));
H *= abs(cos(my_material.time * 0.1 + 1.0));
X = sin(my_material.time);
Y = cos(my_material.time) * 0.6;
ROTATE = sin(my_material.time * 0.2) * 3.14;
let samples = 128;
var color = vec3(0.0);
// 每个sample取一个随机方向
for(var i = 0; i < samples; i++) {
let t = (f32(i) + random(uv + f32(i) + my_material.time)) / f32(samples) * 6.283;
var c = vec3(0.0);
ray_march(uv, vec2(cos(t), sin(t)), &c);
color += c;
}
color = color / f32(samples);
return vec4(color, 1.0);
}
9. Bevy RenderGraph#
如果对webgpu不熟,先需要找本书把webgpu大概的结构过一遍。
对于如何画三角形,如何起pipeline,如何起shader这些东西混个眼熟。
可以直接找js版本的课程,非常方便。rust搞起来更费劲。
然后就要看bevy是怎么整合这些东西。
https://docs.rs/bevy_render/0.15.3/bevy_render/render_graph/struct.RenderGraph.html
一堆渲染node组成一个DAG,形成一个RenderGraph。
节点之间用edge连接,形成数据的传输。
render_graph::Node
是基本的node,每帧都运行。
render_graph::ViewNode
会针对每个view/camera来运行,适用于后处理。
render模块和游戏世界(ECS)是两个不同的app,两者之间需要一个传数据的机制,无法随意获取对方数据。
9.1 看代码#
研究bevy源码中custom_post_processing.rs这个例子。
起一个PostProcessPipeline struct。对其实现FromWorld的from_world,因为需要用ecs世界中的数据来初始化。
这里大概就是走一遍图形api的常见初始化流程。只会跑一次。
create_bind_group_layout
create_sampler
读取shader文件
获取PipelineCache,bevy的pipeline容器。
以现有的数据创建一个RenderPipelineDescriptor,包括pipeline的各种配置。
我们不用显示创建pipeline,应该是bevy会在某个时间点创建。
用queue_render_pipeline把这个RenderPipelineDescriptor插入PipelineCache,获取其id。
一些有用的数据存在PostProcessPipeline中备用,例如创建的pipeline的id。新起一个plugin,在plugin中获取RenderApp,把我们的PostProcessPipeline添加为资源。
起一个PostProcessNode struct作为node。对其实现ViewNode。
实现ViewQuery。每次run时会用这个获取ECS的数据。实现run。每一帧run都会跑。
可以获取PostProcessPipeline和PipelineCache。
可以用PostProcessPipeline我们准备好的id从PipelineCache获取pipeline的实体数据。
view_target.post_process_write获取后处理通道。
RenderContext包含render的主要数据例如device。
create_bind_group做一些数据的绑定。
begin_tracked_render_pass开始render pass。
set_render_pipeline设置pipeline实体数据。
最后draw。plugin中用add_render_graph_node把PostProcessNode插入RenderGraph。
add_render_graph_edges创建edge。
再看compute_shader_game_of_life.rs这个例子,差不多的结构。
10. sdf全局光照#
看论文
https://arxiv.org/pdf/2007.14394
https://www.youtube.com/watch?v=iY15xhuuHPQ
11. bevy实现#
看这个项目的代码
zaycev/bevy-magic-light-2d
其光照的原理大概就是上面的论文应用到2d。
整体结构
创建普通的2d mesh,作为游戏中的物体。
创建对应的Occluder,作为光的遮挡物。
起一个camera,这个camera会显示正常的mesh。
但是render target设置为一个独立的Image。
这个Image在最后阶段和别的光照数据组合为最终的图像。创建sdf的pipeline,配置各种数据。
在RenderApp的render阶段,把计算sdf所需的world数据更新到wgpu的各种buffer。
写sdf的compute shader,根据输入的数据计算sdf。
sdf输出到一个Image叫sdf_target。
重复4-7,可以创建多个pass,实现各种光照流程。
在bind group的设置中,可以通过共用的Image把各个pass的输入输出串起来。
实现一个render_graph::Node,在run()中设置和执行各个pass。
把Node插入RenderGraph,放到CameraDriverLabel后面。
起一个PPMaterial,里边包括所有之前阶段的结果Image。
写PPMaterial的shader,整合之前阶段的结果。
起一个quad承载最终的画面,使用PPMaterial。
起一个普通的Camera2d,显示这个quad。
probe的方式
例如图像大小为512x512=262144个点
compute shader中统一
@compute @workgroup_size(8, 8, 1)
。即每一组算64个点。对于sdf,可以算所有点,或者算256x256之类。
对于光的probe,把图像分成例如8x8像素的格子。每个格子作为probe点。
dispatch_workgroups的xy参数传8x8。
这样一共计算8x8x8x8=4096个点,计算量是原始512x512的1/64。
shader中的光的计算步骤
感觉跟光子映射非常相似!
算点的sdf值,是一个f32。
对于每个probe点,遍历所有光源,用raymarching检查是否被阻挡。把所有直接光贡献加起来。结果存为一个texture。
对于每个probe点,计算间接光(反弹的光)。规定一个最大反射距离,用raymarching采样多个反射距离之内的表面。把交点处对应的直接光,根据距离和其他配置处理一下,做成间接光。把直接光和间接光加起来存为一个texture,输入到下一步。
blend。大概是记录每一帧的camera位置。与相机的老位置的光照做一些blend?达到一种延迟和柔和的效果?如果不做这个事情,会出现不柔和和闪烁卡顿,具体待研究。
filter。这里计算所有512x512。进行光的smooth,根据附近的probe点进行插值。
如果不做,可以看出光是一格一格的,不是均匀的。
但如果是像素风格,其实不算很大的影响。后处理。组合正常的mesh颜色和光照。
texture的格式有说法。
R16Float在shader中存vec4的话只能存入第一个值,比较坑。
存颜色要用Rgba16Float之类。
花时间看懂bevy-magic-light-2d这个项目的基本原理。
之后就可以按需山寨一个自己的全局光照了。