首页
/ three.js FaceFrame 全解:摩天楼立面生成器中“沿边、朝上、朝外”局部坐标架的构建与复用

three.js FaceFrame 全解:摩天楼立面生成器中“沿边、朝上、朝外”局部坐标架的构建与复用

2026-09-06 18:51:05作者:邓越浪Henry

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 };

}

其数学逻辑可以概括为三行公式:

  1. 开间数 count = max( 1, floor( length / bayWidth ) ):向下取整保证所有 bay 宽度一致,Math.max(1, …) 保证极端情况下(边比 bay 还短)至少排一个;
  2. 端部边距 margin = ( length − count × bayWidth ) / 2:剩余长度在首尾两端对称平分,使构件列在视觉上居中、两端对称;
  3. 返回对象 { count, margin, width: bayWidth },其中 width 恒等于传入的 bayWidth,便于调用方统一取用。

以默认参数为例:当塔楼某条立面(footprint 宽度 34 减去切角后的完整边)长约 34 m、bayWidth = 2.6 m 时,count = floor( 34 / 2.6 ) = 13margin = ( 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 ) );

}

它由两步组成:

  1. Matrix4.makeBasis( u, v, n ):用框架的三个轴向单位向量构造一个纯旋转矩阵(因 buildFaces 保证三轴构成右手正交系,见下节),它把“x 横向 → u、y 向上 → v、z 朝外 → n”的规范局部坐标映射到世界朝向;
  2. 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() 中,这类矩阵被批量收集进 windowsglasspiers 等数组,最终由 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() 先把 floorHeightwindowHeightbayWidthpierWidth 全部对齐到 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–70L147–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 会为每个地块各自构造一个 SkyscraperGeneratorexamples/jsm/generators/CityGenerator.js#L86-L102),每个塔独立传入 seed / totalHeight / footprint / floorHeight / bayWidth / chamferWidth 等参数,并只有在街区角落地块才传 chamferWidth > 0 使切角朝向路口。SkyscraperGenerator.defaults 中给出了所有参数的默认基线(L447–458):seed: 35totalHeight: 140floorHeight: 4bayWidth: 2.6stringCourseEvery: 6chamferWidth: 4setbackDepth: 1.5acChance: 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() 模式在项目内复制扩展。

延伸阅读(仓库内源码)

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388