上一篇我们把 19 根钻孔插值成了一整块地层,结尾埋了个话头:
这条线刚开了个头。CASE_LIST 里的下一站是三维体素-地层体素数据(#78)——本篇的体素是"插值出来的层号",下一篇要处理的是"体素数据本身":当每个体素带的不是一个层号,而是一组属性时,渲染和查询该怎么组织。换个数据形态,咱们下回再拆。
顺带回顾一下:#77 的做法是一个层号数组 + 五个VoxelPrimitive当掩码——每层一个 primitive,只点亮属于自己的体素,其余全填 alpha = 0。这样换来了"每层独立显隐、能像抽屉一样拉开",代价是五倍存储。
今天换一种组织方式:只用一个VoxelPrimitive,让每个体素自己带上颜色和透明度。数据形态一变,架构、开销、能力边界全都跟着变。
💡 本文涉及的全部代码均已开源,完整的仓库地址见文末,可自行取用、随意魔改。
先给结论:同一个地层问题,两条路都能走通,但它们适合的数据不一样。
- 需要分层控制(单独隐藏某层、层间拉开看剖面)→ 走 #77 的"多 primitive + 掩码";
- 只关心体素本身的属性(这里是什么岩性、什么颜色、多透明)→ 走本篇的"单 primitive + 自带属性"。
本篇的 demo 属于后者,而且是最简单的一种:层界是水平的,没有插值。这看似是退化,其实是把"数据组织"这件事从噪声里剥出来单讲——先把骨架看清,再往里填 IDW、填起伏、填真实钻孔。
一、先说清楚:本篇与 #77 的分野
把两篇摆在一起看,差别一目了然:
| #77 钻孔建模 | #78 体素数据 | |
|---|---|---|
VoxelPrimitive个数 | 5(每层一个) | 1 |
| 体素里存什么 | 层号(先在Int32Array里算好,再分散到 5 份颜色) | 颜色 + alpha(VEC4),就一份 |
| 同样 2304 个有效体素的存储 | 180 KB(5 份) | 36 KB(1 份) |
| 层界怎么定 | IDW 插值面,起伏 | 深度阈值,水平 |
| 每层独立显隐 / 展开 | 支持 | 不支持(只能整体显隐) |
| 拾取时能问出什么 | 岩性 + 深度 + 层位 | 岩性 + 深度 + 层号 |
为什么本篇更"像体素该有的样子"?因为体素数据的本质就是每个采样点自带一组属性——颜色、密度、岩性编码、孔隙度、含水率……你想带几个就带几个。Cesium 的VoxelProvider正是按这个模型设计的:names是一个属性名数组,types和componentTypes逐个声明类型。
#77 把"层号"这种分类信息硬塞进渲染管线,就得为每个类别造一份完整数据(掩码),是拿存储换控制力。当你的属性是连续的(温度场、浓度场、密度场),掩码这条路干脆走不通——你不可能为一万个温度值造一万个 primitive。那时只有本篇这条路:一个 primitive,一个属性数组,颜色在 shader 里查表或算出来
再往上一层说:什么时候该用体素,什么时候该用几何网格?判据是你要表达的东西有没有明确的界面。地层界面是明确的(砂土到粘土就那一米之间),用几何面(PolygonGeometry、WallGeometry)能画出锐利的界、还能贴纹理、能做拾取和量算——#77 的钻孔柱状体就是这么干的。但当地层是渐变的(风化壳、污染羽流、温度场、浓度场),界面根本不存在,你只能说"这里的值是多少"——这时几何网格无从下手,体素才是唯一自然的表达。
本篇虽然画的是"有明确界面的地层",却故意用体素来做,用意正在于此:先把体素这套管线跑通(provider 怎么写、数据怎么排、变换怎么配),等你要表达连续场时,换的只是填进数组的那些数字,架构一行都不用改。这也是为什么本篇的层界是水平面——把插值从画面里拿掉,好让管线本身看得清楚。
。
代价也很明确:本篇丢了"分层控制"。你没法单独隐藏"粘土层",也没法把地层拉开当抽屉看——因为所有层都在同一个 primitive 里,只有一份show。这个取舍要想清楚再选。
那本篇想要"分层展开"怎么办?不是做不到,但得换个思路:既然只有一个 primitive,就在 shader 里做——把层号作为属性带进来(上面说的 SCALAR 那招),再用一个 uniform 数组控制每层的可见性与 Z 偏移,在fragmentMain里对该隐藏的层直接discard、该偏移的层改fsInput.voxel.travelDistance或直接改采样坐标。控制力从"primitive 级"下沉到"着色器级",换来的是只存一份数据。代价是 shader 变复杂、调试变难——#77 那个"五个 primitive 各管一份"虽然笨,但改起来直观得多。简单场景用多 primitive,规模上来再下沉到 shader,这个顺序别反。
二、数据怎么写进体素:一张岩性表,一次按深度赋色
本篇的数据生成比 #77 朴素得多。先看那张表:
constLITHOLOGY_TABLE:Record<number,{name:string;color:string;alpha:number}>={1:{name:'砂土层',color:'#D2B48C',alpha:0.8},2:{name:'粘土层',color:'#8FBC8F',alpha:0.7},3:{name:'基岩层',color:'#696969',alpha:0.9}}注意alpha是跟着岩性走的,不是统一的。基岩 0.9 最实、粘土 0.7 最透——这是有意的:让你能透过浅层看到深部,同时基岩又保留足够的"实心感"。
然后是requestData里的生成循环,逻辑是逐切片定岩性、整片赋色:
for(letz=0;z<gz;z++){constglobalZ=1.0-z/(gz-1)// 自顶向下的归一化深度:1 = 最深,0 = 最浅constdepth=globalZ*scaleZ.valueletlitho