求菱形十二面体的精确有符号距离函数(SDF)
嘿,这问题挺有意思的——菱形十二面体的SDF确实因为它的对称性和凸多面体特性,比普通立方体体素要费点功夫。先给你梳理下:你的近似SDF其实是正确的凸多面体SDF,但它的梯度在边和顶点处不连续,所以渲染出来的边缘会显得很锐利。要做精确SDF(带圆角),核心是分区域计算点到面、边、顶点的最近距离,再用平滑组合实现圆角。
先给你补全你未完成的顶点区域计算,再优化整个代码,最后加上圆角效果:
完整精确SDF(分区域计算)
float Sdf(Voxel voxel, vec3 position) { position = position - voxel.Position; const float sqrt2 = sqrt(2.0); const float sqrt3 = sqrt(3.0); const float voxelSize = voxel.voxelSize; // 利用八面体对称性,将点映射到 x ≥ y ≥ z ≥ 0 的子空间,减少计算分支 position = abs(position); if (position.x < position.y) position.xy = position.yx; if (position.x < position.z) position.xz = position.zx; if (position.y < position.z) position.yz = position.zy; // 定义6组归一化的面法向量(对应菱形十二面体的6组相对面) vec3 normals[6] = { vec3(1.0, 0.0, 0.0), vec3(0.5, 0.0, sqrt3/2.0), vec3(0.5, 0.0, -sqrt3/2.0), vec3(0.0, -sqrt2/sqrt3, 1.0/sqrt3), vec3(0.5, sqrt2/sqrt3, sqrt3/6.0), vec3(0.5, -sqrt2/sqrt3, -sqrt3/6.0) }; float dists[6]; float signs[6]; // 计算到每组面的带符号距离:dot(n,p) - voxelSize,符号表示点在面的哪一侧 for (int i = 0; i < 6; i++) { float dotProd = dot(normals[i], position); signs[i] = sign(dotProd); dists[i] = dotProd - voxelSize; } // 手动找出前三个最大的距离和对应法向量,比循环排序更高效 float d0 = dists[0], d1 = dists[1], d2 = dists[2]; vec3 n0 = normals[0], n1 = normals[1], n2 = normals[2]; float s0 = signs[0], s1 = signs[1], s2 = signs[2]; for (int i = 3; i < 6; i++) { if (dists[i] > d0) { d2 = d1; n2 = n1; s2 = s1; d1 = d0; n1 = n0; s1 = s0; d0 = dists[i]; n0 = normals[i]; s0 = signs[i]; } else if (dists[i] > d1) { d2 = d1; n2 = n1; s2 = s1; d1 = dists[i]; n1 = normals[i]; s1 = signs[i]; } else if (dists[i] > d2) { d2 = dists[i]; n2 = normals[i]; s2 = signs[i]; } } // 情况1:最近点在面上(仅一个面的距离为正,其余为负/零) if (d1 <= 0.0) { return d0; } // 情况2:最近点在边上(两个面的距离为正,第三个为负/零) else if (d2 <= 0.0) { vec3 norm0 = n0 * s0; vec3 norm1 = n1 * s1; // 求解两个面的交线(边)上的最近点 float dotNN = dot(norm0, norm1); float det = 1.0 - dotNN * dotNN; vec3 edgeDir = normalize(cross(norm0, norm1)); vec3 proj = position - dot(position, edgeDir) * edgeDir; float t0 = (voxelSize - dot(norm1, proj) * dotNN) / det; float t1 = (voxelSize - dot(norm0, proj) * dotNN) / det; vec3 closestPoint = t0 * norm0 + t1 * norm1; return length(position - closestPoint); } // 情况3:最近点在顶点上(三个面的距离为正) else { vec3 norm0 = n0 * s0; vec3 norm1 = n1 * s1; vec3 norm2 = n2 * s2; // 求解三个面的交点(顶点) mat3 faceMatrix = mat3(norm0, norm1, norm2); vec3 closestPoint = inverse(faceMatrix) * vec3(voxelSize); return length(position - closestPoint); } }
加入圆角效果(平滑组合)
要实现圆角,我们可以用smoothmin函数把面、边、顶点的距离平滑融合,避免生硬的边缘。先定义平滑最小函数:
// k 是平滑半径,值越大圆角越明显 float smoothmin(float a, float b, float k) { float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0); return mix(b, a, h) - k*h*(1.0 - h); } float smoothmin3(float a, float b, float c, float k) { return smoothmin(smoothmin(a, b, k), c, k); }
然后修改SDF的返回逻辑,把三种情况的距离用smoothmin组合:
// 预计算所有可能的距离 float faceDist = d0; // 计算边距离 vec3 norm0 = n0 * s0; vec3 norm1 = n1 * s1; float dotNN = dot(norm0, norm1); float det = 1.0 - dotNN * dotNN; vec3 edgeDir = normalize(cross(norm0, norm1)); vec3 proj = position - dot(position, edgeDir) * edgeDir; float t0 = (voxelSize - dot(norm1, proj) * dotNN) / det; float t1 = (voxelSize - dot(norm0, proj) * dotNN) / det; vec3 edgeClosest = t0 * norm0 + t1 * norm1; float edgeDist = length(position - edgeClosest); // 计算顶点距离 vec3 norm2 = n2 * s2; mat3 faceMatrix = mat3(norm0, norm1, norm2); vec3 vertexClosest = inverse(faceMatrix) * vec3(voxelSize); float vertexDist = length(position - vertexClosest); // 平滑组合三种距离,k 是圆角半径(可根据需求调整) const float roundRadius = 0.1 * voxelSize; return smoothmin3(faceDist, edgeDist, vertexDist, roundRadius);
一些优化建议
- 预计算逆矩阵:因为法向量是固定的,你可以预先计算好三个法向量组合的逆矩阵,避免在SDF计算中实时求逆,提升光线步进的效率。
- 对称性优化:我们已经用了
x≥y≥z的对称映射,这能大幅减少分支判断的数量,建议保留。 - 精度调整:如果光线步进时出现精度问题,可以适当调整距离计算中的浮点精度,比如用
double类型替代float(如果平台支持)。
这样修改后,你的SDF就能输出平滑的边缘,加上圆角后效果会更自然。如果后续还有问题,欢迎更新内容继续探讨!
内容的提问来源于stack exchange,提问作者selimbat
相关产品推荐
相关产品推荐

