roofHip 函数实现详解

本文档详细解析 cgaprocessor.jsroofHip 函数的实现。该函数用于生成四坡屋顶(Hip Roof / 庑殿顶)——四面都有坡度的屋顶形式,顶部有一条屋脊线,四角有斜脊(hip line)。

一、函数定位与特点

roofHip 是屋顶函数中最复杂的一个。它采用矩形近似算法:将 footprint 投影到2D平面后,用其包围盒的长宽比来近似生成四坡屋顶。当 footprint 接近正方形时会自动退化为金字塔顶。

四坡屋顶(Hip Roof)的特征:四面均有坡度,长边方向有两条斜脊(hip)从屋脊端点延伸至角点,短边方向有两片梯形坡面(gable)。

二、源码实现(第 1185~1420 行)

2.1 参数与预处理

function func_roofHip(processor, angle) {
  angle = angle || 30;
  var top = processor.top;
  if (!top.faces.length) return;

  top.mergeVertices();
  top.computeFaceNormals();

  var normal = top.faces[0].normal.clone().normalize();
  if (!normal.length()) throw 'roofHip needs a non-degenerate face normal';

  var center = new THREE.Vector3();
  top.vertices.forEach(v => center.add(v));
  center.divideScalar(top.vertices.length);

  // Project vertices to 2D plane
  var projected = top.vertices.map(v => v.clone().sub(center));
  var xaxis = projected[1] ? projected[1].clone().sub(projected[0]) : new THREE.Vector3(1,0,0);
  xaxis.sub(normal.clone().multiplyScalar(xaxis.dot(normal)));
  if (xaxis.length() < 0.0001) xaxis = new THREE.Vector3(1,0,0).cross(normal);
  if (xaxis.length() < 0.0001) xaxis = new THREE.Vector3(0,1,0).cross(normal);
  xaxis.normalize();

  var yaxis = new THREE.Vector3().crossVectors(normal, xaxis).normalize();

  var pts2d = top.vertices.map(v => {
    var d = v.clone().sub(center);
    return { x: d.dot(xaxis), y: d.dot(yaxis), vertex: v };
  });

  // Sort CCW
  var c2 = pts2d.reduce((a, p) => { a.x += p.x; a.y += p.y; return a; }, {x:0,y:0});
  c2.x /= pts2d.length; c2.y /= pts2d.length;
  pts2d.sort((a,b) => Math.atan2(a.y-c2.y, a.x-c2.x) - Math.atan2(b.y-c2.y, b.x-c2.x));

  // Compute bounding box to determine orientation
  var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
  pts2d.forEach(p => {
    minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
    minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
  });
  var W = maxX - minX, D = maxY - minY;

2.2 退化判断

  // Near-square → pyramid (correct degeneracy)
  if (Math.min(W,D) / Math.max(W,D) > 0.85) {
    func_roofPyramid(processor, angle);
    return;
  }

  // ...
  var h = Math.tan(THREE.Math.degToRad(angle)) * shortSize / 2;
  var hipRun = h / Math.tan(THREE.Math.degToRad(angle));
  if (hipRun > shortSize / 2 + 0.0001) {
    func_roofPyramid(processor, angle);
    return;
  }

2.3 顶点生成与面片构建

  var ridgeHalfShort = shortSize / 2 - hipRun;
  var ridgeHalfLong = longSize / 2;

  // ... corners and roofBase definitions ...

  var ridgeLeft  = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar(0)).add(normal.clone().multiplyScalar(h));
  var ridgeRight = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar(0)).add(normal.clone().multiplyScalar(h));

  // Footprint corners (ground level)
  var fl = center.clone().add(longAxis.clone().multiplyScalar(-halfW))
    .add(shortAxis.clone().multiplyScalar(-halfD));
  var fr = center.clone().add(longAxis.clone().multiplyScalar( halfW))
    .add(shortAxis.clone().multiplyScalar(-halfD));
  var br = center.clone().add(longAxis.clone().multiplyScalar( halfW))
    .add(shortAxis.clone().multiplyScalar( halfD));
  var bl = center.clone().add(longAxis.clone().multiplyScalar(-halfW))
    .add(shortAxis.clone().multiplyScalar( halfD));

  // Roof base corners (at height h)
  var flh = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar(-halfD)).add(normal.clone().multiplyScalar(h));
  var frh = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar(-halfD)).add(normal.clone().multiplyScalar(h));
  var brh = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar( halfD)).add(normal.clone().multiplyScalar(h));
  var blh = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW))
    .add(shortAxis.clone().multiplyScalar( halfD)).add(normal.clone().multiplyScalar(h));

  function addFace(a, b, c) {
    var l = geometry.vertices.length;
    geometry.vertices.push(a, b, c);
    geometry.faces.push(new THREE.Face3(l, l+1, l+2));
  }

  // Hip faces (triangles from ridge end to corner)
  addFace(ridgeLeft, flh, fl);
  addFace(ridgeRight, fr, frh);
  addFace(ridgeRight, brh, br);
  addFace(ridgeLeft, bl, blh);

  // Gable faces (trapezoids from ridge to eave)
  addFace(ridgeLeft, ridgeRight, frh);
  addFace(ridgeLeft, frh, flh);
  addFace(ridgeRight, ridgeLeft, blh);
  addFace(ridgeRight, blh, brh);

  geometry.mergeVertices();
  geometry.computeFaceNormals();
  geometry.computeVertexNormals();
  processor.update(geometry);
}

三、参数说明

参数类型默认值说明
angleNumber30屋顶坡度角(度)

四、核心算法流程

输入:平面 footprint + angle Step 1: 投影到2D,计算包围盒 W×D Step 2: 近正方形?→ 调用 roofPyramid Step 3: 确定长短轴,计算 h 和 hipRun Step 4: 生成 ridge、roofBase、footprint 顶点 Step 5: 构建 4 个 hip 面 + 2 个 gable 面 输出:四坡屋顶三角网格 长短比 > 0.85 hipRun > short/2

五、详细步骤解析

Step 1: 2D 投影与包围盒(第 1196~1226 行)

roofGable 相同,先建立局部坐标系,将 footprint 顶点投影到2D平面,按极角排序。然后计算投影点的包围盒:

Step 2: 退化判断(第 1229~1248 行)

2.1 近正方形退化

if (Math.min(W,D) / Math.max(W,D) > 0.85) {
  func_roofPyramid(processor, angle);
  return;
}

当长短边比例超过 0.85 时(接近正方形),四坡屋顶退化为金字塔顶。

2.2 hipRun 超限退化

var h = Math.tan(THREE.Math.degToRad(angle)) * shortSize / 2;
var hipRun = h / Math.tan(THREE.Math.degToRad(angle));
if (hipRun > shortSize / 2 + 0.0001) {
  func_roofPyramid(processor, angle);
  return;
}

计算关键参数:

有趣的是,hipRun = shortSize / 2 恒成立(因为 h 就是这么定义的)。所以 hipRun > shortSize / 2 的判断实际上永远不会触发。这行代码可能是作者预留的安全检查,或复制自其他实现。

Step 3: 坐标系对齐(第 1237~1241 行)

var isWider = W >= D;
var longAxis = isWider ? xaxis : yaxis;
var shortAxis = isWider ? yaxis : xaxis;
var longSize = isWider ? W : D;
var shortSize = isWider ? D : W;

将坐标轴与 footprint 的长短边对齐,确保屋脊始终沿长边方向

Step 4: 3D 顶点生成(第 1371~1389 行)

代码生成了三类顶点:

顶点类型变量名位置高度
屋脊左端点ridgeLeft长轴 -ridgeHalfW,短轴 0h
屋脊右端点ridgeRight长轴 +ridgeHalfW,短轴 0h
前左地面角点fl长轴 -halfW,短轴 -halfD0
前右地面角点fr长轴 +halfW,短轴 -halfD0
后右地面角点br长轴 +halfW,短轴 +halfD0
后左地面角点bl长轴 -halfW,短轴 +halfD0
前左 roofBaseflh长轴 -ridgeHalfW,短轴 -halfDh
前右 roofBasefrh长轴 +ridgeHalfW,短轴 -halfDh
后右 roofBasebrh长轴 +ridgeHalfW,短轴 +halfDh
后左 roofBaseblh长轴 -ridgeHalfW,短轴 +halfDh

其中 ridgeHalfW = halfW - hipRun = halfW - shortSize/2 = (W - D)/2(当 W ≥ D 时)。

footprint(地面) roofBase(高度 h) ridge fl fr br bl flh frh brh blh ridgeL ridgeR W (longSize) D (shortSize)

图:俯视示意图。中间 roofBase 矩形和 ridge 都在高度 h,外圈 footprint 在高度 0。

Step 5: 面片构建(第 1392~1414 行)

5.1 Hip 面(四个角点三角形)

面片顶点覆盖区域
Front-left hipridgeLeft → flh → fl前左角,从屋脊到地面
Front-right hipridgeRight → fr → frh前右角,从屋脊到地面
Back-right hipridgeRight → brh → br后右角,从屋脊到地面
Back-left hipridgeLeft → bl → blh后左角,从屋脊到地面

5.2 Gable 面(前后水平面)

面片顶点说明
Front gable (1)ridgeLeft → ridgeRight → frh前侧上方水平三角形
Front gable (2)ridgeLeft → frh → flh前侧上方水平三角形
Back gable (1)ridgeRight → ridgeLeft → blh后侧上方水平三角形
Back gable (2)ridgeRight → blh → brh后侧上方水平三角形
实现说明:代码中的 "gable faces" 实际上是水平面(四个顶点均在高度 h),而非倾斜的坡面。这是一种简化近似:屋顶中间区域为平顶,四周通过 hip 面(三角形斜面)过渡到地面角点。对于矩形 footprint,这会产生一种"截顶金字塔"式的视觉效果。在 procedural building 中,这种简化通常足以满足远处观察的视觉需求。

六、关键数学关系

参数公式说明
屋顶高度 hh = tan(θ) × shortSize / 2由坡角和短边决定
hipRunhipRun = h / tan(θ) = shortSize / 2hip 线水平投影长度
ridgeHalfW(W - D) / 2屋脊半长(当 W ≥ D)
屋脊长度W - D等于 longSize - shortSize

七、使用示例

// 默认 30 度四坡屋顶
Roof --> roofHip()

// 45 度陡坡四坡屋顶
Roof --> roofHip(45)

// 在方形 footprint 上会自动退化为金字塔顶
Square --> primitiveQuad(4, 4) roofHip(30)

八、与其他 roof 函数的对比

函数坡面数屋脊适用 footprint
roofGable2有(沿长边)任意多边形(最长边为屋脊)
roofHip4(近似)有(沿长边)矩形近似(长短比 < 0.85)
roofPyramidN无(汇聚为一点)近正方形
roofShed1任意(沿 Z 轴坡面)
文档生成信息:基于 cgaprocessor.js 第 1185~1420 行源码分析。生成时间:2026-05-28。