three.js FaceFrame 全解:摩天楼立面生成器中“沿边、朝上、朝外”局部坐标架的构建与复用
FaceFrame 是 three.js 示例代码 examples/jsm/generators/city/SkyscraperGenerator.js 中用于程序化生成摩天楼立面的一座“局部坐标架”:每个塔楼立面都被抽象为一组 (u 沿边、v 向上、n 朝外) 的世界空间正交基,搭配一个原点与边长,从而让立面上的柱子、窗洞、拱廊、挑檐等构件全部在平面的 (u, v) 局部坐标里排版,再通过一次矩阵烘焙到任意朝向的世界立面(含 45° 切角斜边)。读完本文,你将掌握 FaceFrame 的数据结构、.bays() 与 .matrix() 两个核心方法的计算逻辑、从建筑平面轮廓生成全部面框架的完整管线,以及它在摩天楼与整座城市生成器中的实际调用方式。
FaceFrame 是什么:立面排版从“三维世界”到“二维平面”的降维
在官方面框架说明中,FaceFrame 被定义为 “A face's local ( u along edge, v up, n outward ) frame in world space”,即一个立面在世界空间中的局部坐标系:
- u:沿该立面边缘(水平、沿着墙面)的方向;
- v:竖直向上的方向;
- n:垂直立面、朝外的方向。
在源代码中,这一结构用于把复杂的立面排版变成纯 2D 问题:塔楼主体被读取为一个平面多边形(footprint,位于 XZ 平面),多边形的每条边都派生出一个 FaceFrame;随后窗户、柱子、窗下墙带、檐口、女儿墙、拱廊等所有立面构件都先写在平面的 (u, v) 二维坐标里,由框架统一转换到世界空间。注释对此的概括是:
Builds a face frame per footprint edge. Each frame is an orthonormal basis ( u along the edge, v up, n outward ) plus an origin and length, so all facade layout can happen in flat ( u, v ) space and bake to world with one matrix — the same authored piece then instances onto every face, including the diagonal chamfer.
从源码结构看,FaceFrame 是 SkyscraperGenerator 的内部辅助类——该文件结尾的导出语句只包含 SkyscraperGenerator, createSkyscraperMaterial, buildingPalette, pickBuildingColor(见导出行),FaceFrame 本身并不对外导出。它主要由 buildFaces() 工厂创建,供生成器内部各 addXxx 函数消费。
字段与构造函数:origin / u / v / n / length
FaceFrame 的构造实现位于源代码 553–595 行:
/** A face's local ( u along edge, v up, n outward ) frame in world space. */
class FaceFrame {
constructor( origin, u, v, n, length ) {
this.origin = origin;
this.u = u;
this.v = v;
this.n = n;
this.length = length;
}
// ...
}
五个字段的含义分别是:
| 字段 | 类型 | 含义 |
|---|---|---|
origin |
Vector3 |
框架原点,取多边形边上“u 正方向背离的端点”(见下文 buildFaces),实际是立面底边的一个端点 |
u |
Vector3 |
单位化的沿边方向(水平、贴墙面) |
v |
Vector3 |
向上的单位向量,通常即 ( 0, 1, 0 ) |
n |
Vector3 |
朝外的单位法向量 |
length |
Number |
该立面的边长(以米为单位的世界长度),用于 .bays() 的分隔计算 |
官方文档页中构造函数写为 new FaceFrame() 且未列出形参,这是文档抽取的结果;实际构造时需按 origin, u, v, n, length 五个参数传入(见源码构造器)。
值得注意的是,框架坐标系全部建立在世界空间内(不是挂在某个 Object3D 下的本地空间),因此 build() 烘焙出的矩阵可以直接与世界坐标换算。此外,虽然文档只列出 u / v / n 三轴,origin 同样是关键信息——matrix() 与 point() 的定位都以它为基准。
核心方法一:.bays() 将立面切分为规整的“开间”
文档对 .bays() 的描述为:“How many bays of bayWidth fit, with the remainder split into end margins.”——即求一条边内能容纳多少个宽度为 bayWidth 的 bay(开间),余量对称均分到两端的 margin(边距)。其实现在源码 586–593 行:
/** How many bays of `bayWidth` fit, with the remainder split into end margins. */
bays( bayWidth ) {
const count = Math.max( 1, Math.floor( this.length / bayWidth ) );
const margin = ( this.length - count * bayWidth ) / 2;
return { count, margin, width: bayWidth };
}
其数学逻辑可以概括为三行公式:
- 开间数
count = max( 1, floor( length / bayWidth ) ):向下取整保证所有 bay 宽度一致,Math.max(1, …)保证极端情况下(边比 bay 还短)至少排一个; - 端部边距
margin = ( length − count × bayWidth ) / 2:剩余长度在首尾两端对称平分,使构件列在视觉上居中、两端对称; - 返回对象
{ count, margin, width: bayWidth },其中width恒等于传入的bayWidth,便于调用方统一取用。
以默认参数为例:当塔楼某条立面(footprint 宽度 34 减去切角后的完整边)长约 34 m、bayWidth = 2.6 m 时,count = floor( 34 / 2.6 ) = 13,margin = ( 34 − 13 × 2.6 ) / 2 = 0.1 m,即 13 个均匀开间、两端各留 0.1 m。
bays() 是整栋楼“立面网格”的源头,下面这些位置都直接消费它的返回值(每个调用处的 count / margin / width 均来自同一个框架):
addPiers()沿每个 bay 分界线放置竖向壁柱(L765–779);addWindows()在每个 bay 中心放置窗框与玻璃(L781–840);addArcade()为基座拱廊按拱宽archWidth开洞(L716–761);addFinials()在压顶宝塔饰(finial)的排布上也按同一开间走(L842–856)。
正因为窗口、壁柱、墙带全部共享同一组 { count, margin, width },立面才能在横向“一像素不差”地对齐成完整网格。
核心方法二:.matrix() 把“规范局部坐标”里的构件放到面上
文档对 .matrix() 的描述为:“Places a piece authored in the canonical local frame ( x across, y up, z outward ).”——把以规范局部坐标(x 横向、y 向上、z 朝外)写成的构件放置到世界空间立面上。实现位于源码 577–583 行:
/** Places a piece authored in the canonical local frame ( x across, y up, z outward ). */
matrix( u, v, w ) {
return new Matrix4()
.makeBasis( this.u, this.v, this.n )
.setPosition( this.point( u, v, w, _point ) );
}
它由两步组成:
Matrix4.makeBasis( u, v, n ):用框架的三个轴向单位向量构造一个纯旋转矩阵(因buildFaces保证三轴构成右手正交系,见下节),它把“x 横向 → u、y 向上 → v、z 朝外 → n”的规范局部坐标映射到世界朝向;setPosition( point(u, v, w) ):设置平移分量。这里的定位点来自同样未在文档列出、但被matrix()依赖的.point()方法(源码 566–574 行):
point( u, v, w, target = new Vector3() ) {
return target
.copy( this.origin )
.addScaledVector( this.u, u )
.addScaledVector( this.v, v )
.addScaledVector( this.n, w );
}
也就是 point = origin + u·u + v·v + n·w,参数 u / v / w 分别表示沿框架三个轴的有符号偏移量。对作者而言,w 取负值即可把玻璃、墙等“凹进”立面(例如 addWindows() 中用 frame.matrix( cx, cy, - p.windowReveal ) 把玻璃放到窗洞进深内侧),w 取正值则让挑檐、窗式空调外机凸出墙面。
.matrix() 返回一个全新的 Matrix4。在 SkyscraperGenerator.build() 中,这类矩阵被批量收集进 windows、glass、piers 等数组,最终由 bakeGroups() 一次性烘焙进单个非索引 BufferGeometry,从而让“同一个构件几何体 + 每个实例一个放置矩阵”的高效实例化成为可能(见烘焙实现)。
上游管线:buildFaces() 如何把平面轮廓变成一组 FaceFrame
FaceFrame 由 buildFaces() 逐个生成(源码 516–551 行):
function buildFaces( points ) {
const faces = [];
const up = new Vector3( 0, 1, 0 );
for ( let i = 0; i < points.length; i ++ ) {
const a = points[ i ];
const b = points[ ( i + 1 ) % points.length ];
// outward normal: perpendicular to the edge, pointing away from the
// origin (the footprint is centred there)
const n = new Vector3( b.y - a.y, 0, - ( b.x - a.x ) ).normalize();
const mid = new Vector3( ( a.x + b.x ) / 2, 0, ( a.y + b.y ) / 2 );
if ( n.dot( mid ) < 0 ) n.negate();
// right-handed basis: u = v × n, so makeBasis( u, v, n ) is a pure rotation
const u = new Vector3().crossVectors( up, n ).normalize();
const pa = new Vector3( a.x, 0, a.y );
const pb = new Vector3( b.x, 0, b.y );
const length = pa.distanceTo( pb );
// the edge end that u points away from becomes the origin
const origin = pb.clone().sub( pa ).dot( u ) > 0 ? pa : pb;
faces.push( new FaceFrame( origin, u, up.clone(), n, length ) );
}
return faces;
}
几个值得展开的实现细节:
- 输入是平面多边形:
buildFaces的输入来自buildFootprint()(L468–507),后者在 XZ 平面生成一个“矩形切掉一个 45° 角”的轮廓(以Vector2(x, z)的有序列表表示)。塔楼的退缩层(crown,退台后)使用同样函数生成一个更小的内缩轮廓,从而得到第二组更短的面框架。 - 朝外法线的判定:先按边向量求平面内法线,再用“法线与轮廓中心点积为负则取反”来确保
n指向轮廓外侧(footprint 以原点为中心)。 - 右手正交基:
u = cross(v, n)且v = (0,1,0),于是u, v, n构成右手正交系——这正是.matrix()里makeBasis能作为“纯旋转”的前提。注释明确写道:right-handed basis: u = v × n, so makeBasis( u, v, n ) is a pure rotation。 - 原点的选取:两条候选端点中,让
u向量“背离”的那一端被选作origin,保证后续所有u偏移都从立面一端正向累积。
这套管线允许同一个构件几何体复用在不同朝向的面上:四个正交面加一条 45° 切角斜边各自拥有独立框架,但排布逻辑(柱子、窗、拱)完全相同。正因为 FaceFrame 承担了方向差异,SkyscraperGenerator 的立面部装填代码从不关心“这面墙朝哪个方向”。
在 SkyscraperGenerator 中的实际调用关系
build() 方法把整个建筑分成**基座(base)/ 塔身(shaft)/ 冠部(crown)**三段,塔身与冠部复用同一套立面逻辑(L344–360):
const tiers = [
{ faces, bottom: baseTop, height: shaftHeight, pierHeight: shaftHeight, ac: acUnits },
{ faces: crownFaces, bottom: shaftTop, height: crownHeight, pierHeight: crownHeight - crownCornice, ac: null }
];
for ( const t of tiers ) {
for ( const frame of t.faces ) {
addWindows( frame, windows, glass, glassRooms, t.ac, t.bottom, t.height, p );
addWall( backWalls, frame, t.bottom, t.bottom + t.height, 0.8, - 0.6 );
addSpandrelBands( bands, frame, t.bottom, t.height, p );
addPiers( frame, t.bottom, t.pierHeight, p, addPier );
}
}
也就是说,生成一栋楼会得到两组面框架(完整 footprint 与内缩后的冠部 footprint),每层立面代码遍历各自的面框架执行一次完整的“楼层 × 开间”双循环:
addWindows()内层循环:frame.bays( p.bayWidth )得到列索引,floorHeight计算行索引,于是每个窗口的 (行, 列) 网格坐标只需两次坐标运算就能定位,通过frame.matrix( cx, cy, 0 )生成窗框实例矩阵,通过frame.matrix( cx, cy, - p.windowReveal )生成玻璃矩阵;addPier()回调按高度分桶存矩阵(相同高度的连续壁柱共享一份几何体,见 L316–322),并在“跳过末端的最后一个 bay 分界线”以避免与相邻立面转角处的壁柱重叠(L769–777);- 与几何体严格对应的是砖石模数:
build()先把floorHeight、windowHeight、bayWidth、pierWidth全部对齐到BRICK = { height: 0.3, length: 0.6 }的砖模数(L278–285),再由同一个 TSL 材质按世界坐标读出砖缝,因此几何网格线与程序化砖墙永远对齐。FaceFrame 提供的frame.origin还被用作“按楼层+面散列”的种子(见floorHash(),L202–207),使每扇窗选择“看进哪间房”的决定在四面墙上互不相同且稳定。
另外,TSL 材质侧的室内映射(interior mapping)片段着色器也沿用了与 FaceFrame 相同的坐标约定——从几何法线重建 uAxis = cross(up, n) 的面框架来投射线到虚拟房间(L991–1003),保证了 CPU 几何与 GPU 着色两套系统对角度的理解完全一致。
实战示例:单塔生成器与城市生成器的调用方式
FaceFrame 本身不直接对外暴露,实际开发者通过 SkyscraperGenerator 来使用整条管线。最直接的用法见示例页面 examples/webgpu_generator_building.html,其 generate() 函数(L161–187)构造生成器并生成单栋建筑:
generator = new SkyscraperGenerator( {
seed: parameters.seed,
totalHeight: parameters.height,
footprint: { width: parameters.width, depth: parameters.depth },
floorHeight: parameters.floorHeight,
bayWidth: parameters.bayWidth,
chamferWidth: parameters.chamfer,
setbackDepth: parameters.setback
}, material );
building = generator.build();
building.castShadow = building.receiveShadow = true;
scene.add( building );
示例页面通过 Inspector 面板暴露了与 FaceFrame 相关的实时参数(L60–70 与 L147–155):
seed(0–100):决定塔楼整体风格的种子,内部通过 mulberry32 PRNG 派生 footprint、分段比例等参数;height(60–200 m)、width(18–60 m)、depth(16–50 m):决定 footprint 尺寸,进而决定每条立面的length;floorHeight(3–6 m):楼层高,会被吸附到砖模数;楼层数由totalHeight / floorHeight推导,build()会把请求的总高四舍五入成整层数(L287–297);bayWidth(1.8–4.5 m):.bays()的分隔宽度,直接决定每面墙的开间数与窗柱密度;chamfer(0–10 m):45° 切角宽度,只有产生了斜边,buildFaces才会输出第五个朝向的 FaceFrame;setback(0–4):冠部退台深度(以bayWidth为单位),决定冠部轮廓与第二组面框架。
批量生成整座城市时,CityGenerator 会为每个地块各自构造一个 SkyscraperGenerator(examples/jsm/generators/CityGenerator.js#L86-L102),每个塔独立传入 seed / totalHeight / footprint / floorHeight / bayWidth / chamferWidth 等参数,并只有在街区角落地块才传 chamferWidth > 0 使切角朝向路口。SkyscraperGenerator.defaults 中给出了所有参数的默认基线(L447–458):seed: 35、totalHeight: 140、floorHeight: 4、bayWidth: 2.6、stringCourseEvery: 6、chamferWidth: 4、setbackDepth: 1.5、acChance: 0.12 等,其余参数(footprint 比例、分段比例、壁柱宽深等)由种子派生的 randomStyle() 填充,且遵循“固定默认值 < 种子派生风格 < 调用方显式参数”的覆盖优先级(L274–276)。
使用要点小结
- 坐标语义:FaceFrame 的参数
(u, v, w)与构件本地坐标(x 横向, y 向上, z 朝外)一一对应,w < 0表示退入立面(如玻璃、窗洞内壁),w > 0表示凸出(如挑檐、窗式空调)。单位与世界单位一致(示例中均为米)。 - 对称的端部边距:
.bays()刻意把取整余量平分到两端,使窗列对称美观;当需要贴边满排时,可把margin用作首个元素的起始偏移,代码中所有消费方都遵守这一约定。 - 转角去重:
bays()的索引覆盖“含末端分界线”,因此壁柱、宝塔饰等“位于分界线上”的构件循环要跳过最后一个分界索引,避免与相邻立面的同类构件在转角重叠(见 L773–775)。 - 框架是构造约定,不是导出 API:FaceFrame 属于
SkyscraperGenerator.js模块内部实现(未在导出列表中),但它决定了所有立面参数的语义;如需在自己的程序化建筑里复用同一套思路,可直接按本文件相同的类定义与buildFaces()模式在项目内复制扩展。
延伸阅读(仓库内源码)
- 官方文档页:docs/pages/FaceFrame.html
- FaceFrame 完整类实现:examples/jsm/generators/city/SkyscraperGenerator.js#L553-L595
- 面框架生成管线
buildFaces/buildFootprint:examples/jsm/generators/city/SkyscraperGenerator.js#L468-L551 - 单塔生成器主体与参数默认值:examples/jsm/generators/city/SkyscraperGenerator.js#L251-L458
- 整座城市的塔楼布局:examples/jsm/generators/CityGenerator.js
- 可直接运行的单塔交互示例:examples/webgpu_generator_building.html
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00