资讯详情

隐式曲面与显式曲面:几何表示底层逻辑与工程实践

📅 2026/10/16 22:56:05 | 华诺云谱 👁 阅读
隐式曲面与显式曲面:几何表示底层逻辑与工程实践
前阵子有位读者在后台问我既然图形学里啥都能用三角形网格表达为什么还要单独讲隐式曲面和显式曲面这个问题其实问到了点子上。三角形网格确实能表达几乎所有几何但网格本身也只是显式表示的一种而隐式曲面这种“用方程说话”的几何描述方式在求交、建模、物理模拟和程序化生成里有网格替代不了的优势。这篇文章就围绕隐式曲面代数形式、CSG、距离函数、分形几何与显式曲面把几何表示的底层逻辑和实际使用经验捋一遍适合刚学完变换和相机的读者也适合想搞懂“为什么场景里那么多东西不是手捏出来”的开发者。1. 几何表示要回答的三个问题存储、求交与编辑1.1 为什么图形学课程要把几何单独拎出来讲很多初学者会把“几何表示”误当成“建模”的一部分觉得反正最后都要变成三角形喂给GPU前面折腾一堆曲面公式纯属浪费时间。但实际做渲染、碰撞、物理、程序化生成时你会发现每一步都在跟几何表示打交道。一张高精度的地形网格可能有几百万三角形存内存、读磁盘、上传显存都不是免费而同一座地形用一张高度图或者一组隐式函数表达可能只需要几十KB。反过来一个用方程描述的完美球体想把它画出来最终又不得不切成三角形。所以几何表示真正要做好的事情本质上只有三件怎么紧凑地存怎么快速地求交怎么方便地编辑。在渲染里求交是最常见也最贵的操作。光线追踪要算光线和每个物体的交点显式网格求交是遍历三角形做重心坐标测试隐式曲面求交往往可以直接解方程或者用迭代逼近。在物理模拟里碰撞检测也需要几何表示提供“点在哪一侧、离表面多远”这类信息而这恰恰是隐式表示的天赋领域。编辑方面建模软件里拖一个控制柄改变曲线形状就是参数曲面在起作用用布尔运算挖一个洞本质上是CSG在起作用。可以说不把几何表示理解透彻后面所有阶段都会遇到莫名其妙的坑。1.2 隐式与显式的分水岭一个“判断内外”一个“罗列表面”隐式曲面和显式曲面的差别可以用一句话概括隐式表示不直接告诉你表面在哪而是给你一个函数让你自己判断空间里的每个点是在形状内部、外部还是正好在表面上显式表示则直接列出表面上的点或者告诉你通过参数计算表面上的点。典型的隐式曲面形式是 ( f(x,y,z)0 )。比如球体 ( x^2y^2z^2-r^20 )把任意点坐标代进去结果小于0说明点在球内大于0说明在球外等于0说明在球面上。显式表示则截然不同三角形网格直接把顶点坐标和连接关系说出来参数曲面给出一个从二维参数域到三维空间的映射函数 ( p(u,v) )你给一组(u,v)就能得到一个表面上的三维点。这两种思路决定了后续所有操作的方法论。判定点在隐式曲面内只需要一次求值但在显式网格上做同样的判定可能需要先找到最近的面再算投影距离。反过来隐式曲面想要知道表面上具体有哪些点你得去解方程或者跑一遍网格提取显式网格则天然自带表面采样点直接送去渲染即可。理解了这条分水岭再来逐个看代数形式、CSG、距离函数和分形几何就会清爽很多。2. 代数形式的隐式曲面用方程描述“内部”的古老思路2.1 从平面到球再到圆环几条最典型的代数隐式代数形式的隐式曲面是最直接也是最古老的一类。平面是最简单的例子( n \cdot p - d 0 )其中 ( n ) 是法向量( d ) 是到原点的带符号距离。这个形式在裁剪、阴影体积、平面反射里被反复用到因为它求交就是解一个线性方程代价极低。球体则把隐式的优点体现得很明显一个球心、一个半径四个浮点数就能存下一个球而且点到球表面的距离和内外判定都极其廉价。工程上经常出现“用球当代理体”的做法比如复杂角色的碰撞盒、光源影响范围、场景剔除的包围球本质上就是利用球作为代数隐式的极简代价。再往上走就是二次曲面和更高次的曲面。椭球、抛物面、双曲面都属于二次曲面圆环则是一个四次方程( (\sqrt{x^2y^2}-R)^2z^2-r^20 )。实际项目中真正频繁使用的代数隐式并不多因为人类直觉能直接写出来的方程就那么几种。但Cherry 引擎、某些程序化建模工具里代数隐式仍然是场景基础体素的重要组成部分。高次代数曲面带来的表达能力提升往往会被数值稳定性问题抵消这是后话。2.2 代数隐式在代码里的实际判定与采样把代数隐式写进渲染器最常见的方式是先在CPU或GPU上对空间进行采样得到一张标量场再从中提取等值面。所谓等值面就是场值等于某个阈值的点集对隐式曲面就是f值等于0的位置。以球体为例你可以对包围盒内的每个三维网格点求 ( f x^2y^2z^2-r^2 )得到一个密度场。然后对所有相邻网格点之间出现符号变化的位置用线性插值算出零点再把这些零点连接成三角形。这个过程听着复杂实际就是著名的Marching Cubes算法它把每个体素8个相邻采样点组成的立方体按8个顶点的正负号归类查表得到该体素内的三角形连接方式。整个算法最精巧的部分就是那张包含256种情况、可化简为15种基本拓扑的查找表。我实际做这一步时最大的感触是采样分辨率直接决定结果的平滑度和存储量。64³的采样网格大约产生几十万量级的三角形256³就可能上千万内存瞬间告急。所以工程上通常先用低分辨率采样粗网格再用法向量或梯度信息做细分平滑而不是一上来就追求高分辨率。2.3 代数隐式的典型坑数值稳定性与符号习惯代数隐式用起来简单但坑也不少。最经典的是“符号习惯不统一”。我见过几个渲染器里一个球体的方程有人写成 ( x^2y^2z^2-r^2 ) 为正代表内部有人写成 ( r^2-x^2-y^2-z^2 ) 为正代表内部。如果只是单个形状还好一旦做CSG布尔运算符号反了就会出现“挖洞变成叠加”的诡异效果。另一个坑是数值范围。把坐标代入 ( x^2y^2z^2 ) 时如果场景范围特别大比如坐标到1000平方就是一百万float精度很容易出现抖动。所以代数隐式的体素采样最好在局部坐标系做采样前把坐标平移到原点附近再缩放到一个合适的尺度。这个习惯能避免掉绝大多数“曲面表面出现麻点”的离奇问题。3. CSG构造实体几何把布尔运算变成几何雕刻刀3.1 CSG的核心思想与数学实现CSGConstructive Solid Geometry构造实体几何的思路特别直观用简单形状通过交、并、差三种集合运算组合出复杂形状。建模时你拖一个球和一个方块再用差运算让方块把球啃掉一块这就在做CSG。放到隐式曲面的语境里CSG的实现优雅得惊人。如果 ( f_1 ) 和 ( f_2 ) 分别表示两个形状的隐式函数并集就是 ( \min(f_1, f_2) )交集是 ( \max(f_1, f_2) )差集是 ( \max(f_1, -f_2) )。为什么这样回忆一下隐式函数的符号约定外部为正内部为负。两个形状的并集取空间每个点的内部/外部状态只要任意一个形状认为点在其内部并集就该认为点在内部对负值取min自然就实现了“只要有一个负数就是负数”。交集要求两个形状都认为点在内部正值的max保证了只有两个都为负数时结果才为负。差集则是把第二个形状内外翻转再与第一个求交。这套操作对应到代码里就是几行三元运算而且因为每个点都独立判断天然适合GPU并行。很多实时渲染里的“体素地形”“程序化建筑”底层都是这类隐式CSG。3.2 为什么CSG在CAD里不可替代但渲染时要小心CSG最成功的主场是CAD和工业建模。机械零件往往需要精确的布尔特征比如挖螺丝孔、切出凹槽这类操作在参数化建模里极其自然因为你保留的是“球”“方块”“圆柱”这些语义化实体随时可以调整参数重新求值。工业软件的内部表示长期同时保留CSG树和网格结果就是为了兼顾可编辑性与可渲染性。但CSG一旦进入实时渲染就有麻烦。直接渲染隐式CSG需要光线步进或体素化代价偏高而把CSG转成三角形网格后布尔边缘常常出现退化三角形、窄条面、甚至非流形边。我踩过的典型情况是用一个圆柱给大平面开孔布尔之后孔的边缘出现大量极细长的三角形面片数量突增光照在边缘上还闪烁。后来我强制对边界区域做网格优化并且拒绝让布尔操作产生距离小于某阈值的顶点才把这些问题压下去。如果只想渲染不想建模CSG未必是首选但如果你在做一个“可拆装零件编辑器”CSG几乎无可替代。3.3 实际经验CSG网格化的边界退化问题网格化CSG最常见的方法是Marching Cubes或者更精确的Dual Contouring。Marching Cubes简单但对尖锐边界的还原能力差布尔出来的锋利边缘经常被磨圆Dual Contouring则能利用梯度信息保留锐利特征代价是实现复杂度高不少。我在实际项目里用Dual Contouring做过一个组合家具编辑器布尔出的边缘确实能保持清晰的折线代价是每帧网格更新时都要重算体素场性能压力不小。建议是如果布尔物体是静态的就一次性烘焙成网格别每帧重算如果是动态参数调整尽量限制参与布尔运算的基本体数量并把体素分辨率控制在128³以内再配合分块更新。CSG在建模器里是神器在游戏运行时是奢侈品这个认知能帮你少走很多弯路。4. 距离函数SDF带有“梯度”信息的隐式曲面进阶形态4.1 距离函数与普通隐式方程的本质区别普通隐式函数 ( f(x,y,z)0 ) 只告诉你点在内部还是外部距离函数SDFSigned Distance Field则更进一步它的函数值表示点到曲面最近表面的距离并且带正负号。定义很简单如果点在外面返回正的距离在里面返回负的距离在表面返回0。这个差异带来的好处巨大。有了距离值我们就能知道“还要走多远才能碰到表面”这直接催生了光线步进Ray Marching渲染方式。SDF还有一个数学性质叫利普希茨连续性简单说就是函数变化不会太剧烈这保证我们可以用距离值作为步长安全地移动不会一步跨过表面。球体的SDF是 ( \text{length}(p) - r )平面的SDF是 ( \text{dot}(p, n) - d )这些式子看起来跟代数形式差不多但语义完全不同。前者只是判内外后者直接给出可用的距离量。这个差别在做碰撞检测时特别明显网格碰撞要算最近的三角形SDF碰撞只需要采样一个三维纹理或者做几次SDF求值。4.2 光线步进让SDF成为现代渲染器的宠儿SDF最经典的用法就是光线步进。原理极其简单从相机发出一条光线每次前进的距离等于当前点SDF的绝对值因为SDF告诉你“前方最近的表面离这里有多远”前进这么多就绝对不会穿过表面。当距离值小于某个阈值时认为光线击中了表面。GLSL代码往往是这样的float map(vec3 p) { float sphere length(p - vec3(0.0, 0.0, 5.0)) - 1.0; float plane p.y 0.5; return min(sphere, plane); // 球和平面做并集 } vec3 rayMarch(vec3 ro, vec3 rd) { float t 0.0; for (int i 0; i 128; i) { vec3 p ro t * rd; float d map(p); if (d 0.0001) return p; // 碰到表面 t d; // 沿光线前进 if (t 100.0) break; // 没碰到 } return vec3(-1.0); // 返回未命中 }这个循环里最关键的变量是步长 ( t )每次增加的是SDF求出的距离。理论上一根光线最多几千步实际场景通常几十步就能收敛。我起初不理解为什么不用二分后来明白了SDF的价值就在于用距离值自适应步长远处的空洞大步跨越近处精细结构自动放慢这是固定步长完全做不到的。代价是SDF复杂度过高时map函数耗时上涨每步都可能要跑几十条指令。所以实用SDF场景通常把参与运算的形状数量限制在几十个以内。4.3 从CSG到SDF的光滑过渡与操作技巧SDF天然继承CSG的布尔操作因为min/max操作仍然成立。但SDF世界还有一个CSG没有的神器光滑布尔。只要把min换成平滑最小值就能让两个形状的相交处变得圆润。标准smoothmin公式是这样的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); }k值控制圆角半径k越大过渡越缓和。这个trick在手办建模、地形融合、软体模拟可视化里特别常见。我做过一个流体融合效果demo把几滴水分子的SDF用smoothmin合并k设成0.2左右瞬间就出现了液滴粘连的质感比手动做网格变形简单太多了。SDF还有各种域变形玩法比如把整个空间做旋转扭曲之后再算SDF就能得到类似拧麻花的形状。但要注意域变形会破坏SDF的严格距离性质光线步进时可能因为步长过大而穿透薄壁。我的处理方式是变形幅度大时把最大步长乘以0.5或0.3作为安全系数宁多走几步别穿模。4.4 SDF的常见坑符号、尺度与精度之间如何平衡写SDF踩得最多的坑有两个。第一个是忘了保证“内负外正”。如果形状B用正数表示内部而形状A用负数表示内部一次min操作就会得到完全相反的结果。所以所有SDF函数必须统一符号约定建议直接把内部设为负并在每个形状函数的注释里写明。第二个坑跟尺度有关。光线步进的阈值一般设成很小的值比如0.0001但如果场景被缩放到很大的坐标范围这个阈值在屏幕上的实际误差就会被放大。我建议在SDF场景里始终用相对尺度把最大形状半径归一化到1距离阈值取0.001量级这样渲染质量最稳。如果场景里有精细结构和巨大空间共存比如一棵树的每片叶子都用SDF表示那就要考虑多层SDF或者自适应步长优化否则性能会很难看。5. 分形几何递归定义的隐式世界5.1 先纠正一个写法分形不是“分型”标题里写了“分型几何”实际上数学和图形学里的标准叫法是“分形几何”Fractal。“分型”更多是医疗影像里说的“分型分类”容易弄混。分形几何的核心特征是自相似整体放大后局部看起来像整体的缩小版。自然界的云、山脉、海岸线都表现出近似自相似这也是分形在图形学里如此重要的原因。分形几何跟隐式曲面有什么联系很多分形并不是直接给出 ( f(x,y,z)0 ) 的闭式解而是通过一个迭代过程隐式地定义某个点是否属于集合取决于一个迭代序列是否会发散。这仍然是一种“隐式”的成员判定关系只是判定方式从一次求值变成了多次迭代。5.2 逃逸时间算法从二维经典分形到三维体素最经典的分形是芒德布罗集对每个复数 ( c )迭代 ( z_{n1} z_n^2 c )从0开始。如果迭代序列不趋向无穷就认为 ( c ) 属于集合而颜色则由“经过多少次迭代才超过逃逸半径”决定。朱利亚集则是固定 ( c )变化初始 ( z_0 )同样的逃逸逻辑。把同样的思路搬进三维最直接的做法是对空间每个体素中心点计算某种四元数迭代或者三维逃逸公式得到每个位置的迭代次数再把这组值当作隐式场用Marching Cubes提取网格。我做过一个四元数分形地形实验对每个采样点做四元数迭代逃逸时间映射成高度场渲染出的岩石纹理自带无限细节感完全不需要额外贴图。代价是采样计算量巨大256³的体素往往要跑很久所以实际项目里可以只在局部区域做分形体素化并设置最大迭代次数为8次左右超过就认为是集合内部。5.3 分形地形与分形噪声程序化生成里的日常用法比芒德布罗集更实用的分形工具是分形噪声Fractal Brownian Motion简称fbm。它把多张不同频率、不同振幅的噪声叠加在一起低频噪声决定大尺度起伏高频噪声叠加细节。地形生成、云层模拟、水波纹、木纹纹理到处都是fbm。实现上就是循环采样Perlin噪声或Value噪声float fbm(vec2 p) { float value 0.0; float amplitude 0.5; float frequency 1.0; for (int i 0; i 5; i) { value amplitude * noise(p * frequency); frequency * 2.0; amplitude * 0.5; } return value; }这种“频率翻倍、振幅减半”的叠加本质也是自相似所以它生成的地形无论放大多少倍看起来都有合理的起伏。很多人以为程序化地形是纯随机其实核心算法就是这段循环。5.4 渲染分形时的现实问题无限细节与像素噪声分形几何渲染最迷人的地方是无限细节但这恰恰也是最大的坑。显式网格采样时细节总是受限于分辨率而分形函数是连续的、可无限迭代的理论上你无限放大它能一直给出新结构。问题是有限分辨率渲染时这些高频细节会变成像素上的高频噪声导致严重的锯齿和闪烁。处理办法我总结三条一是限制最大迭代次数把逃逸阈值设在合理范围避免采样点把过深层次细节全算出来二是做抗锯齿比如每像素多条光线或者对颜色做时间累积三是给分形几何加一个“细节距离”概念距离相机远时提前终止迭代牺牲细节换取稳定性。我自己的项目里通常把分形细节的迭代上限控制在16次以内再配合TAA抗锯齿画面就能兼顾细节和稳定不至于每帧都像雪花屏。6. 显式曲面把表面点直接摆到台面上6.1 参数曲面显式但不依赖“采样网格”的优雅形式显式曲面并不只有三角形网格。参数曲面是更早也更精准的显式形式给定二维参数 ( (u,v) )通过基函数加权控制点直接算出三维点坐标。贝塞尔曲线和贝塞尔曲面是最好的入门例子。贝塞尔曲面的公式可以理解为“对控制点做两轮线性插值”程序里通常用递推的de Casteljau算法实现。NURBS则更进一步引入权值和节点向量是CAD工业的标准曲面表示。它的优势是局部控制能力强、曲面光滑度高、而且同一套数学既能表示精确圆弧也能表示自由曲面。我在做曲面建模工具时发现NURBS最大的价值是“参数域天然对应纹理域”你不需要单独处理UV因为曲面本来就有 ( (u,v) )。参数曲面的代价是求交往往需要解方程做碰撞检测不如SDF直接显示渲染时还是得离散成小四边形或三角形。所以它在实时渲染管线里注定是被“化整为零”的那一个。6.2 多边形网格你每天在用的显式表示绝大多数游戏和实时渲染场景用三角形网格因为GPU原生支持三角形光栅化。一个网格由顶点数组、索引数组和顶点属性法线、UV、颜色等组成。索引的存在很关键多个三角形共享一个顶点时不需要在内存里重复存顶点坐标GPU缓存效率也更高。更进阶的网格数据结构是半边结构Half-Edge它把每条边拆成两条方向相反的半边每条半边记录起点、终点、相邻面、下一条半边从而支持从任一面快速遍历其邻接面和邻接边。做网格简化、细分、参数化、拓扑修改时半边结构几乎是必须的。哪怕你只是想实现一个“选中某个面片并让它向外挤出”的编辑器功能没有邻接信息也得先遍历所有三角形性能差到没法用。三角形的另一个重要性质是平面性一个三角形必然在一个平面内重心坐标插值法线和UV都很自然。但显式网格表示的缺点也很明显拓扑修改麻烦要处理非流形边、采样密度不灵活远面片和近面片一样密集、点在外判定的代价高。6.3 点云与细分曲面另外两类常见的显式形式点云是显式但“没有连接关系”的表示每个点独立记录三维坐标和属性。激光扫描、深度相机输出、物理模拟的粒子都天然是点云。点云渲染可以用圆点或小方块splatting实现表面重建则要借助Poisson重建或移动最小二乘。它的优点是获取容易缺点是缺少拓扑信息求最近邻要建空间索引碰撞检测也麻烦。细分曲面则是“从粗糙网格生成光滑曲面”的显式方法。最经典是Loop细分给每个三角形加入新的顶点按固定权重更新顶点位置迭代几次之后粗糙的多面体就变成光滑曲面。我在角色建模时经常用这个思路先用极低面数的笼子网格控制大形再用两次细分得到平滑表面既能保持可控性又能提供渲染所需的光滑结果。6.4 显式表示为什么是渲染管线的宠儿原因其实很朴素现代GPU的光栅化管线就是为三角形而生的。三角形网格可以直接进顶点缓冲区和索引缓冲区经过顶点着色器、光栅化、片段着色器一气呵成。你不需要像SDF那样做循环步进也不需要像分形那样迭代逃逸。工程上实时渲染部分永远默认用三角形网格交换数据而隐式曲面只是“生成网格之前的中间层”。这并不意味着显式在精度上优于隐式。网格曲面始终是一阶近似的平面拼合要表示光滑曲面必须靠高密度顶点而隐式方程和参数曲面在数学上可以做到无限光滑。所以实际系统的典型做法是编辑和建模阶段用隐式或参数表示最终渲染阶段烘焙成三角形网格两者并不对立而是流水线上不同阶段的不同工具。7. 隐式与显式的正面交锋选型决策与实际经验7.1 一张表看清隐式与显式的优劣把前面几章的内容压成一张表最直观维度隐式曲面含SDF、CSG、分形显式曲面网格、参数曲面、点云点在内外判定快一次求值或迭代即可慢需要寻找最近面或三角化处理求交球体、平面快复杂形状可做光线步进需要遍历三角形BVH加速后仍偏重内存紧凑度极高几个参数就能存一个复杂形状低细节密度决定顶点/面数量拓扑编辑困难尤其是局部编辑局部编辑方便但拓扑修改易出错表面采样需要Marching Cubes等提取算法天然有顶点可直接渲染纹理映射难需要单独设计UV或投影容易尤其参数曲面自带参数域GPU友好度适合着色器内动态求交不适合直接光栅化原生匹配光栅化管线典型应用碰撞代理、程序化建模、云/地形距离场角色、场景、工业零件渲染这张表说的是一般规律实际每个维度都有例外。比如参数曲面在纹理映射上完胜隐式但在求交问题上又不如简单隐式。选型时最重要的是先想明白你最常做的操作是什么。7.2 我的选型经验建模、渲染、物理要分开看我的习惯是分阶段做决定。建模阶段优先考虑可编辑性所以会大量使用参数曲面和CSG因为一个零件改了长度参数整个布尔树会自动重新求值这种体验网格很难给。渲染阶段则无条件切换到三角形网格因为实时性能第一位所有SDF、参数曲面都要在离线或加载阶段烘焙成网格。物理和碰撞检测我会反过来优先用隐式代理。一个角色胶囊体横过来就是一条线段外扩半径碰撞检测只需要算线段到平面的最近距离一个破碎关卡里的石头用几个球体SDF组合做代理比用高模网格做碰撞性能好一个数量级。很多老练的物理工程师会把“显式画面、隐式碰撞”当成默认方案我实际跑下来确实如此画面表现靠高模网格物理合理性靠SDF近似两者各司其职。7.3 混合使用常见的落地路径最后分享一条我目前最常用的落地路径。场景里的静态地形和建筑先用SDF做CSG布尔设计再用Marching Cubes提取成网格配合LOD把远处面片简化角色和生物体用NURBS或细分做建模最终烘焙成高模网格所有动态碰撞对象一律用简单的SDF代理体并在内存里保存一份低分辨率SDF三维纹理用于快速采样。这条路径的好处是每个环节都用了最擅长该环节的表示方法坏处是需要维护两套几何数据偶尔出现“网格和SDF不同步”的问题所以我还会加上一条校验规则每次导出时自动用SDF对网格表面做一遍偏差检查误差超过阈值就触发重新烘焙。这套流程不一定是标准答案但它是我在几个项目里踩过足够多的坑之后沉淀下来的。几何表示这件事没有银弹理解隐式和显式的本质区别比背下任何单一算法都更有价值。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑