要绘制长方体,依然要从距离场着手。
为了循序渐进,我先说长方形的SDF模型。
1-长方形的SDF模型
已知:
- 长方形的中心点在原点,其宽高的一半分别为a、b
- 任意一点P
求:点P到长方形的距离
解:
这个问题要从三种情况考虑。
设点p为点P的绝对值,即(|P.x|,|P.y|)
设向量d=p-(a,b)
则:
- 当(p.x<a&&p.y>b)||(p.x>a&&p.y<b) 时,点P到长方形的距离为d分量中的最大值
- 当p.x>a&&p.y>b 时,点P到长方形的距离为d的长度
- 当p.x<a&&p.y<b 时,点P到长方形的距离为d分量中的最大值
其代码实现如下:
float SDFRect(vec2 coord, vec2 size) {
vec2 d = abs(coord) - size;
return length(max(d, 0.)) + min(max(d.x, d.y), 0.);
}
- length(max(d, 0.)) 计算的是采样点在矩形外时的距离,当采样点在矩形内时其距离为0
- max(d, 0.) 是让d中的分量大于等于0
- min(max(d.x, d.y), 0.)计算的是采样点在矩形内时的距离,去d分量中的极大值
我们可以在之前绘制圆形的代码中,将圆形的SDF模型直接替换成这个矩形的SDF模型:
// 坐标系缩放
#define ProjectionScale 1.
// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
return ProjectionScale * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 矩形的sdf模型
float SDFRect(vec2 coord, vec2 size) {
vec2 d = abs(coord) - size;
return length(max(d, 0.)) + min(max(d.x, d.y), 0.);
}
// 显示距离场
vec3 SdfHelper(float cd) {
vec3 color = 1. - sign(cd) * vec3(0, 0.5, 1);
color *= 1. - exp(-3. * abs(cd));
color *= .8 + .2 * sin(150. * cd);
color = mix(color, vec3(.7, .7, 0), smoothstep(.01, 0., abs(cd)));
return color;
}
// 鼠标选择测试
void selectTest(out vec3 color, vec2 curCoord) {
// iMouse.z > 0.对应鼠标按下事件
if(iMouse.z > 0.) {
// 鼠标的投影坐标位
vec2 mouseCoord = ProjectionCoord(iMouse.xy);
// 鼠标到圆形的有向距离
float md = SDFRect(mouseCoord, vec2(0.3, 0.2));
// 当前片元到鼠标的距离
float a = length(curCoord - mouseCoord);
//鼠标到圆形的有向距离的绝对值
float b = abs(md);
// 以有向距离为半径显示一个以鼠标位中心的圆形
color = mix(color, vec3(.7, 1, .5), smoothstep(0.007, 0., abs(a - b)));
}
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 投影坐标
vec2 coord = ProjectionCoord(fragCoord);
// 当前片元到矩形的有向距离
float cd = SDFRect(coord, vec2(0.3, 0.2));
// 当有向距离小于0时,绘制矩形
vec3 color = SdfHelper(cd);
// 鼠标选择测试
selectTest(color, coord);
// 最终的颜色
fragColor = vec4(color, 1.0);
}
效果如下:
当我们理解了绘制长方形的SDF模型,我们便可以以同样的原理绘制长方体的SDF模型。
2-长方体的SDF模型
长方体的SDF模型如下:
float SDFRect(vec3 coord) {
vec3 d = abs(coord - RECT_POS) - RECT_SIZE;
return length(max(d, 0.)) + min(max(d.x, max(d.y, d.z)), 0.);
}
上面的代码我就不再解释了,其原理和长方形的SDF模型一样。
我们可以把之前绘制的球体替换成长方体看看:
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 长方体的中心位置
#define RECT_POS vec3(0, 0, -2)
// 长方体的尺寸
#define RECT_SIZE vec3(.8,.4,.5)
// 长方体的漫反射系数
#define RECT_KD vec3(1)
// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(1, 1, 0)-RECT_POS)+RECT_POS
// 相机目标点
#define CAMERA_TARGET vec3(0, 0, -2)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 光线推进的起始距离
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 40
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001
// 点光源位置
#define LIGHT_POS vec3(1, 4, 1)
// 相邻点的抗锯齿的行列数
#define AA 3
// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 长方体的的SDF模型
float SDFRect(vec3 coord) {
vec3 d = abs(coord - RECT_POS) - RECT_SIZE;
return length(max(d, 0.)) + min(max(d.x, max(d.y, d.z)), 0.);
}
// 计算长方体的法线
vec3 SDFNormal(in vec3 p) {
const float h = 0.0001;
const vec2 k = vec2(1, -1);
return normalize(k.xyy * SDFRect(p + k.xyy * h) +
k.yyx * SDFRect(p + k.yyx * h) +
k.yxy * SDFRect(p + k.yxy * h) +
k.xxx * SDFRect(p + k.xxx * h));
}
// 打光
vec3 AddLight(vec3 positon) {
// 当前着色点的法线
vec3 n = SDFNormal(positon);
// 当前着色点到光源的方向
vec3 lightDir = normalize(LIGHT_POS - positon);
// 漫反射
vec3 diffuse = RECT_KD * max(dot(lightDir, n), 0.);
// 环境光
float amb = 0.15 + dot(-lightDir, n) * 0.2;
// 最终颜色
return diffuse + amb;
}
// 视图旋转矩阵
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);
}
// 光线推进
vec3 RayMarch(vec2 coord) {
float d = RAYMARCH_NEAR;
// 从相机视点到当前片元的射线
vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
// 片元颜色
vec3 color = vec3(0);
for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
// 光线推进后的点位
vec3 p = CAMERA_POS + d * rd;
// 光线推进后的点位到长方体的有向距离
float curD = SDFRect(p);
// 若有向距离小于一定的精度,默认此点在长方体表面
if(curD < RAYMARCH_PRECISION) {
color = AddLight(p);
break;
}
// 距离累加
d += curD;
}
return color;
}
// 抗锯齿 Anti-Aliasing
vec3 RayMarch_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 += RayMarch(coord);
}
}
// 返回周围颜色的均值
return color / float(AA * AA);
}
/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 光线推进
vec3 color = RayMarch_anti(fragCoord);
// 最终颜色
fragColor = vec4(color, 1);
}
效果如下:
扩展
我们可以直接使用长方体的SDF模型,在常规的顶点建模的项目里,做基于长方体的边界碰撞检测。