国风乡村 3D 地图生成器 · 技术分享文档
单文件 HTML + Three.js r128 实现的程序化国风古村 3D 地图生成器
无构建工具、无本地依赖,双击即可运行

1. 项目概览
| 项 | 说明 |
|---|---|
| 技术栈 | 原生 JavaScript (ES5) + Three.js r128 |
| 部署形态 | 单个 HTML 文件(约 1300 行),CDN 加载引擎 |
| 核心玩法 | 一键程序化生成随机古村地图,支持种子复现 |
| 交互 | 左键旋转 / 右键平移 / 滚轮缩放 / 空格重新生成 / 移动端单指旋转 + 双指捏合 |
| 渲染特性 | PCF 软阴影、ACES 色调映射、sRGB 输出、雾效、实例化渲染 |
1.1 三层容错的引擎加载
依次尝试 jsDelivr → unpkg → cdnjs 三个 CDN,全部失败则显示友好错误页;WebGL 创建失败也有独立提示:
html
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script>if (typeof THREE === 'undefined') { document.write('<script src="https://unpkg.com/..."><\/script>'); }</script>
2. 架构设计
整体流程是经典的 "数据 → 逻辑网格 → 3D 场景" 三段式:
点击生成 / 空格
│
▼
generate(seed) ← 纯逻辑层
├── mulberry32(seed) 确定性随机数
├── 42×42 Int8Array 网格 逐步铺设 河流→道路→房屋→广场→农田→宝塔→装饰
└── 收集摆放数据 (houses, farms, trees, ...)
│
▼
buildScene(data) ← 渲染层
├── InstancedMesh 批量绘制 地块 / 水面 / 庄稼 / 花朵 / 远景森林
├── 工厂函数生成组合体 makeHouse / makePagoda / makeBridge ...
└── addPop() 注册入场动画
│
▼
animate() ← 每帧循环
├── 云朵漂移 / 水面色相波动 / 小船起伏
└── easeOutBack 弹出动画队列
2.1 逻辑网格(Tile Map)
地图核心是一个 42×42 的 Int8Array,每格 2 世界单位:
js
var N = 42, CELL = 2, HALF = N * CELL / 2;
var T = { EMPTY:0, WATER:1, ROAD:2, BRIDGE:3, HOUSE:4, FARM:5, TREE:6, DECOR:7, PLAZA:8 };
配套一组极简的网格 API:idx / inB / get / set / wp(wp 负责网格坐标 → 世界坐标转换)。所有生成规则都只操作这个一维数组,与渲染完全解耦------先想清楚"哪格是什么",再决定"画成什么样"。
2.2 确定性随机:mulberry32
用 32 位种子的 mulberry32 PRNG 替换 Math.random,同一种子 → 同一村庄,方便复现与分享:
js
function mulberry32(a) {
return function () {
a |= 0; a = a + 0x6D2B79F5 | 0;
var t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
在其上封装 rr(a,b) 浮点区间、ri(a,b) 整数区间、pick(arr) 随机取样、shuffle 洗牌,构成整个生成器的随机工具箱。
3. 程序化生成算法
生成顺序经过精心设计,优先级高的元素先落地,后续元素避让:
3.1 河流:随机游走 (Random Walk)
随机选择横向或纵向贯穿,宽 2~3 格。每前进一格有 45% 概率上下偏移 1 格,形成自然蜿蜒:
js
if (rand() < 0.45) rpos += pick([-1, 0, 1]);
rpos = clamp(rpos, 4, N - 5 - rw);
rcell/dcell 两个坐标交换函数让"横河+纵路"与"纵河+横路"共用同一套逻辑。
3.2 主路与桥:正交穿越 + 冲突升级
主路与河流方向垂直,同样随机游走;关键技巧是靠近水域时锁定方向 (向前探测 4 格),保证道路垂直过河。路遇水格自动升级为 BRIDGE,桥格集合最终会拟合成一座完整石拱桥。
3.3 支路、牌坊、房屋
- 支路:从主路上随机点垂直伸出 2~3 条,长 8~16 格,遇水即停。
- 牌坊:主路两端(a=2 与 a=N-3)85% 概率各立一座,占位
DECOR防止被侵占。 - 房屋(沿路吸附算法 ):打乱所有路格,对每个路格尝试 4 个方向放置 2×2~3×2 的房屋,要求:
- 目标格全部
EMPTY; - 周围一圈无其他
HOUSE(避免贴脸);
朝向取Math.atan2(-dir[0], -dir[1]),大门永远面向道路。
- 目标格全部
3.4 广场 + 水井、农田、宝塔
- 广场:在前 80 个路格附近找 3×3 空地,中央放水井,并打通与路的连接格。
- 农田:随机撒 3~5 块 3~5 × 3~4 的矩形,要求周边 3 格内有路/水/房(贴近生活区),55% 概率为水田。
- 宝塔:随机试 60 次,要求 3×3 空地且曼哈顿距离最近房屋 ≥ 16------远离村落的山野孤塔更有意境。
3.5 植被:概率场叠加
树木的生成概率是多个因子的叠加,简单却有效:
js
var p = edgeD < 4 ? 0.24 : 0.045; // 地图边缘密林
if (靠近树丛中心) p += 0.3; // 3 个随机 grove 聚簇
if (nearW) p = 0.2; // 水边概率固定
树种也与环境关联:水边多柳树/桃树,内陆多松树/阔叶树。水边空格还有 25% 概率长芦苇,1.2% 概率放石头。
4. 渲染优化
4.1 材质 / 几何体缓存池
M() 与 G() 两个工厂函数按 key 缓存,全场景同色材质、同参几何体只创建一次:
js
function M(hex, opts) {
var key = hex + '|' + JSON.stringify(opts || '');
if (!MATS[key]) MATS[key] = new THREE.MeshLambertMaterial(...);
return MATS[key];
}
重新生成地图时,disposeMap() 通过 GEOS_set 区分共享几何体与一次性几何体,只 dispose 后者,避免误杀缓存。
4.2 InstancedMesh 大批量绘制
数量最大的五类物体全部使用实例化渲染,Draw Call 从数千降到个位数:
| 对象 | 实例数量级 | 逐实例颜色 |
|---|---|---|
| 地块 (land) | ~1600 | setHSL 按类型着色,带随机扰动 |
| 水面 (water) | ~100 | 蓝绿色随机明度 |
| 庄稼 (crops) | 数百~上千 | 旱田/水田不同色相 |
| 花朵 (flowers) | 数百 | 5 色随机 |
| 远景森林 (fol+trk) | 220×2 | 绿色系 HSL 抖动 |
逐格颜色扰动(col.setHSL(h, s, l + rand()*0.05))是低成本打破单调感的关键。
4.3 顶点着色的程序化远山
makeMountain 在 ConeGeometry 上做两层正弦扰动(低频轮廓 + 高频细节),再按高度 t 混合底色→山腰→岩石→雪线四段颜色写入 vertex color,一个材质渲染所有山体:
js
var n = Math.sin(ang * f1 + ph1) * 0.22 + Math.sin(ang * f2 + ph2) * 0.12;
pos.setX(i, x * (1 + n * (1 - t * 0.55))); // 越往山顶扰动越小
远山分两圈布置(半径 1.5~1.95 HALF 的中景、2~3 HALF 的远景),加上 16 个绿色半球丘陵与 220 棵实例化松树,营造纵深。
4.4 光影与氛围
- 半球光(天空蓝 / 草地绿)+ 暖色平行光,2048 阴影贴图 + PCFSoft + normalBias 防阴影痤疮
THREE.Fog(0xcfe3bd, 130, 400)让远山融入天色;太阳与云朵材质设fog:false排除雾化- 太阳 = 实体球 + 半透明光晕球 + Canvas 径向渐变生成的 Sprite 辉光,零贴图资源
- CSS 径向渐变 vignette 覆盖层压暗四角,Lambert 材质 + ACES 色调映射得到柔和的低多边形水彩感
5. 动画系统
全部动画集中在一个 requestAnimationFrame 循环,dt 钳制在 0.05s 防切页跳变:
- 入场弹出 :每个物体注册进
popList,延迟 = 距地图中心距离(popDelay),用easeOutBack缓动从 0 弹到目标缩放------生成地图时呈现从中心向外扩散的波浪式生长效果。 - 云朵:9 组椭球云以各自速度向 +X 漂移,超界回绕。
- 水面 :整体材质明度按
sin(t*1.4)波动,模拟粼光。 - 小船 :
sin驱动 y 浮沉 + z 轴微倾,摇橹感。
6. 交互与相机
自实现轨道相机(球坐标 theta/phi/radius + target):
- 左键拖拽改 theta/phi(phi 钳制 0.18~1.35,不许翻到地下)
- 右键平移沿相机水平面的 forward/right 移动 target,范围限 ±45
- 滚轮/双指捏合缩放 radius(28~190)
- 移动端 touch 事件独立处理,
touch-action:none防浏览器手势冲突
7. 最后效果



想要获取源码,绿泡泡搜🔍 "码来的小朋友" 。