之前我们在说分段函数的时候,画过一个二维棋盘,然而当这个二维棋盘放在三维空间中的时候,便会出现极远处的棋盘格采样失真的问题。
接下来咱们就先画一个三维棋盘格,然后解决其采样失真问题。
1-三维棋盘格
整体代码如下:
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 相机视点位
#define CAMERA_POS vec3(0, 2, 0)
// 视点动画
// #define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(2, 4, 0)-vec3(0,0,-4))+vec3(0,0,-4)
// 相机目标点
#define CAMERA_TARGET vec3(0, 0, -4)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 光线推进的起始距离
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 512
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001
// 点光源位置
#define LIGHT_POS vec3(3,4, -1)
// 相邻点的抗锯齿的行列数
#define AA 3
// 栅格图像的z位置
#define SCREEN_Z -1.
// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 水平面的SDF模型
float SDFPlane(vec3 coord) {
return coord.y;
}
// 计算球体的法线
vec3 SDFNormal(in vec3 p) {
const float h = 0.0001;
const vec2 k = vec2(1, -1);
return normalize(k.xyy * SDFPlane(p + k.xyy * h) +
k.yyx * SDFPlane(p + k.yyx * h) +
k.yxy * SDFPlane(p + k.yxy * h) +
k.xxx * SDFPlane(p + k.xxx * h));
}
// 视图旋转矩阵
mat3 RotateMatrix() {
//基向量c,视线
vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
//基向量a,视线和上方向的垂线
vec3 a = cross(CAMERA_UP, c);
//基向量b,修正上方向
vec3 b = cross(c, a);
//正交旋转矩阵
return mat3(a, b, c);
}
// 光线推进数据的结构体
struct RayMarchData {
vec3 pos;
bool crash;
};
// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd) {
float d = RAYMARCH_NEAR;
// 光线推进次数
RayMarchData rm;
rm = RayMarchData(ro, false);
for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
// 光线推进后的点位
vec3 p = ro + d * rd;
// 光线推进后的点位到模型的有向距离
float curD = SDFPlane(p);
// 若有向距离小于一定的精度,默认此点在模型表面
if(curD < RAYMARCH_PRECISION) {
rm = RayMarchData(p, true);
break;
}
// 距离累加
d += curD;
}
return rm;
}
// 棋盘格
float checkers(in vec2 uv) {
vec2 grid = floor(uv);
return mod(grid.x + grid.y, 2.);
}
// 渲染
vec3 Render(vec2 coord) {
// 从相机视点到当前片元的射线
RayMarchData rm = RayMarch(CAMERA_POS, normalize(RotateMatrix() * vec3(coord, SCREEN_Z)));
// 片元颜色
vec3 color = vec3(0);
// 如果光线推进到SDF模型上
if(rm.crash) {
color = vec3(checkers(rm.pos.xz));
}
return color;
}
// 抗锯齿 Anti-Aliasing
vec3 Render_anti(vec2 fragCoord) {
// 初始颜色
vec3 color = vec3(0);
// 行列的一半
float aa2 = float(AA / 2);
// 逐行列遍历
for(int y = 0; y < AA; y++) {
for(int x = 0; x < AA; x++) {
// 基于像素的偏移距离
vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
// 投影坐标位
vec2 coord = ProjectionCoord(fragCoord + offset);
// 累加周围片元的颜色
color += Render(coord);
}
}
// 返回周围颜色的均值
return color / float(AA * AA);
}
/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 光线推进
vec3 color = Render_anti(fragCoord);
// vec3 color = Render(ProjectionCoord(fragCoord));
// 最终颜色
fragColor = vec4(color, 1);
}
解释一下其绘图步骤。
1.建立一个无限延展,高度为0的水平面的SDF模型。
float SDFPlane(vec3 coord) {
return coord.y;
}
2.准备一个棋盘格纹理。
float checkers(in vec2 uv) {
vec2 grid = floor(uv);
return mod(grid.x + grid.y, 2.);
}
3.在渲染的时候,为水平面着棋盘格纹理。
vec3 Render(vec2 coord) {
// 从相机视点到当前片元的射线
RayMarchData rm = RayMarch(CAMERA_POS, normalize(RotateMatrix() * vec3(coord, SCREEN_Z)));
// 片元颜色
vec3 color = vec3(0);
// 如果光线推进到SDF模型上
if(rm.crash) {
color = vec3(checkers(rm.pos.xz));
}
return color;
}
效果如下:
当前这个棋盘格在远处已经出现了采样失真问题,其失真原理就棋盘格的采样频率跟不上棋盘格的黑白变换频率。
2-优化棋盘格
优化棋盘格的原理很简单,只要将远处的棋盘格模糊即可,如下图所示:
棋盘格模糊的原理咱们之前说过,对一个三角形的分段函数求导即可。
// 三角形分段函数
vec2 tri(in vec2 p) {
vec2 h = fract(p * .5) - .5;
return 1. - 2. * abs(h);
}
// 棋盘
float checkers(in vec2 p) {
// 精度
vec2 w = vec2(.9);
// 求导
vec2 i = (tri(p + 0.5 * w) - tri(p - 0.5 * w)) / w;
return 0.5 - 0.5 * i.x * i.y;
}
然而,棋盘格模糊的力度w是一个变量,它应该由近到远越来越大,
我们可以将栅格图像里的像素距离转化为棋盘格水平空间内的距离,以此来控制模糊力度。
又上图可知,相机俯视水平面时,越往上的像素,其对应的棋盘平面中的距离就越大,我们可以基于此距离来控制棋盘格模糊的力度。
整体代码如下:
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 相机视点位
#define CAMERA_POS vec3(0, 2, 0)
// 视点动画
// #define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(2, 3, 0)-vec3(0,0,-4))+vec3(0,0,-4)
// 相机目标点
#define CAMERA_TARGET vec3(0, 0, -4)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 光线推进的起始距离
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 512
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001
// 点光源位置
#define LIGHT_POS vec3(3,4, -1)
// 相邻点的抗锯齿的行列数
#define AA 3
// 栅格图像的z位置
#define SCREEN_Z -1.
// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 水平面的SDF模型
float SDFPlane(vec3 coord) {
return coord.y;
}
// 计算球体的法线
vec3 SDFNormal(in vec3 p) {
const float h = 0.0001;
const vec2 k = vec2(1, -1);
return normalize(k.xyy * SDFPlane(p + k.xyy * h) +
k.yyx * SDFPlane(p + k.yyx * h) +
k.yxy * SDFPlane(p + k.yxy * h) +
k.xxx * SDFPlane(p + k.xxx * h));
}
// 视图旋转矩阵
mat3 RotateMatrix() {
//基向量c,视线
vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
//基向量a,视线和上方向的垂线
vec3 a = cross(CAMERA_UP, c);
//基向量b,修正上方向
vec3 b = cross(c, a);
//正交旋转矩阵
return mat3(a, b, c);
}
// 光线推进数据的结构体
struct RayMarchData {
vec3 pos;
bool crash;
};
// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd) {
float d = RAYMARCH_NEAR;
// 光线推进次数
RayMarchData rm;
rm = RayMarchData(ro, false);
for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
// 光线推进后的点位
vec3 p = ro + d * rd;
// 光线推进后的点位到模型的有向距离
float curD = SDFPlane(p);
// 若有向距离小于一定的精度,默认此点在模型表面
if(curD < RAYMARCH_PRECISION) {
rm = RayMarchData(p, true);
break;
}
// 距离累加
d += curD;
}
return rm;
}
// 三角形分段函数
vec2 Triangle(in vec2 x) {
vec2 h = fract(x * .5) - .5;
return 1. - 2. * abs(h);
}
// 棋盘格
float CheckersGrad(in vec2 uv, in vec2 ddx, in vec2 ddy) {
// 模糊力度
vec2 w = max(abs(ddx), abs(ddy)) + .001;
// 强化模糊
// vec2 w = max(abs(ddx), abs(ddy))*2. + .001;
// 三角形分段函数的导数
vec2 i = (Triangle(uv + 0.5 * w) - Triangle(uv - 0.5 * w)) / w;
// xor
return 0.5 - 0.5 * i.x * i.y;
}
// 渲染
vec3 Render(vec2 coord, vec2 px, vec2 py) {
// 相机的旋转矩阵
mat3 rotateMatrix = RotateMatrix();
// 光线推进的方向
vec3 rd = normalize(rotateMatrix * vec3(coord, SCREEN_Z));
// 光线推进的数据
RayMarchData rm = RayMarch(CAMERA_POS, rd);
// 片元颜色
vec3 color = vec3(0);
// 如果光线推进到SDF模型上
if(rm.crash) {
//将px、py从像素坐标系变换至相机世界
vec3 rdx = normalize(rotateMatrix * vec3(px, SCREEN_Z));
vec3 rdy = normalize(rotateMatrix * vec3(py, SCREEN_Z));
//将栅格图像上一个像素的偏移向量转换为棋盘格水平空间内的向量
vec3 ddx = rd / rd.y - rdx / rdx.y;
vec3 ddy = rd / rd.y - rdy / rdy.y;
color = vec3(CheckersGrad(rm.pos.xz, ddx.xz, ddy.xz));
}
return color;
}
// 抗锯齿 Anti-Aliasing
vec3 Render_anti(vec2 fragCoord, vec2 px, vec2 py) {
// 初始颜色
vec3 color = vec3(0);
// 行列的一半
float aa2 = float(AA / 2);
// 逐行列遍历
for(int y = 0; y < AA; y++) {
for(int x = 0; x < AA; x++) {
// 基于像素的偏移距离
vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
// 投影坐标位
vec2 coord = ProjectionCoord(fragCoord + offset);
// 累加周围片元的颜色
color += Render(coord, px, py);
}
}
// 返回周围颜色的均值
return color / float(AA * AA);
}
/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
//栅格图像右偏移一个像素
vec2 px = ProjectionCoord(fragCoord + vec2(1., 0.));
//栅格图像左偏移一个像素
vec2 py = ProjectionCoord(fragCoord + vec2(0., 1.));
// 光线推进
vec3 color = Render_anti(fragCoord, px, py);
// 最终颜色
fragColor = vec4(color, 1);
}
效果如下:
解释一下其绘图过程。
1.在mainImage()方法中先获取当前片元在x、y方向的两个偏移向量。
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
//栅格图像右偏移一个像素
vec2 px = ProjectionCoord(fragCoord + vec2(1., 0.));
//栅格图像左偏移一个像素
vec2 py = ProjectionCoord(fragCoord + vec2(0., 1.));
// 光线推进
vec3 color = Render_anti(fragCoord, px, py);
// 最终颜色
fragColor = vec4(color, 1);
}
2.在渲染方法中,将之前的偏移向量转换为棋盘格水平空间内的两个向量。
// 渲染
vec3 Render(vec2 coord, vec2 px, vec2 py) {
……
if(rm.crash) {
//将px、py从像素坐标系变换至相机世界
vec3 rdx = normalize(rotateMatrix * vec3(px, SCREEN_Z));
vec3 rdy = normalize(rotateMatrix * vec3(py, SCREEN_Z));
// 将栅格图像上一个像素的偏移向量转换为棋盘格水平空间内的向量
vec3 ddx = rd / rd.y - rdx / rdx.y;
vec3 ddy = rd / rd.y - rdy / rdy.y;
color = vec3(CheckersGrad(rm.pos.xz, ddx.xz, ddy.xz));
}
return color;
}
上面的ddx、ddy便是一个像素在棋盘格上的偏移距离,如下图所示:
在上图中,随着棋盘格的越来越远,ddy 就越来越长,ddx 也是同理。
3.基于ddx、ddy对棋盘格进行动态模糊。
float CheckersGrad(in vec2 uv, in vec2 ddx, in vec2 ddy) {
// 模糊力度
vec2 w = max(abs(ddx), abs(ddy)) + .001;
// 强化模糊
// vec2 w = max(abs(ddx), abs(ddy))*4. + .001;
// 三角形分段函数的导数
vec2 i = (Triangle(uv + 0.5 * w) - Triangle(uv - 0.5 * w)) / w;
// xor
return 0.5 - 0.5 * i.x * i.y;
}
在上面我取了ddx、ddy 的最大值作为模糊系数。
我们还可以提高模糊的强度:
vec2 w = max(abs(ddx), abs(ddy))*4. + .001;
效果如下:
关于三维棋盘格的绘制,我们就说到这,下一章咱们会说一下多物体着色。
参考链接: