threejs-landscape 实战:用 Three.js 构建极坐标网格地形、实例化草地与昼夜过渡的程序化风景

发布时间:2026/10/10 1:47:04
threejs-landscape 实战:用 Three.js 构建极坐标网格地形、实例化草地与昼夜过渡的程序化风景 【免费下载链接】SkillsAgent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents项目地址https://gitcode.com/gh_mirrors/skills48/Skills点击查看免费下载导读本文围绕开源仓库 Skills 中的threejs-landscapeskillSKILL.md展开完整讲解如何用 Three.js 构建一块能站住主体的程序化风景背景极坐标网格地形让分辨率跟随镜头、域扭曲噪声生成侵蚀感山体、顶点着色器塑形的实例化草地在零 CPU 成本下随风摆动以及用完整状态插值实现的晨/午/黄昏/夜四时段无缝过渡。读完本文你可以将这套方案直接用于 hero 背景、产品展示台、滚动叙事世界等任何需要让建筑、物体或角色坐在一个真实场景里而不是漂浮在一张渐变图上的页面并配套掌握可运行示例demo/index.html中的全部实现细节与验收清单。场景示例参考实现 demo/index.html 的运行截图展示近景 15 万根实例化草叶、极坐标网格地形与六站天空共同构成的程序化风景图片来自 demo/preview.jpg。背景的使命让主体身在某处然后退场skill 的开篇立意非常明确背景只有一份工作——让主体看起来真的待在某处somewhere一眼读懂场景后马上让出注意力见 SKILL.md 引言。因此整个方案的设计取舍都围绕前景主体展开而不是让背景自身成为焦点。在仓库的目录组织上threejs-landscape与两个兄弟 skill 构成组合拳threejs-weather在风景之上叠加雨、风暴或雪threejs-towers当站在风景中的主体是建筑时使用。对应的 Agent 配置agents/openai.yaml给出的默认提示词即Use $threejs-landscape to put this subject in a real place: noise terrain, instanced grass, a gradient sky and a time-of-day system直接点明了该 skill 交付的四件套噪声地形、实例化草、渐变天空、昼夜系统。配套的演示提示词文档demo/PROMPT.md进一步定义了体验契约一块延伸到地平线柔和地形的草地平原拖拽环绕、滚轮/双指缩放四个时段共用一个控件、以交叉淡化而非硬切切换、夜晚出现星星还有一个 GRID 开关可以打开网格线框让极坐标地形结构一目了然。用极坐标网格环摄相机分辨率跟随镜头为什么不能用正方形高度场文档给出的第一个硬性结论是不要构建正方形高度场SKILL.md 第 2 节。长焦镜头只能看到狭窄的楔形视野正方形网格的大部分三角形都浪费在相机背后而真正被拍到的地平线方向却又很快耗尽分辨率。正确的做法是以相机脚下为中心采样极坐标网格径向环随距离越来越疏const AN 900, RN 52, R0 2.0, R1 700; // angular, radial, near, far for (let r 0; r RN; r) { const t r / RN; const rad R0 (R1 - R0) * Math.pow(t, 2.4); // dense near, sparse far for (let a 0; a AN; a) { const th a / AN * Math.PI * 2; push(Math.cos(th) * rad, landH(x, z), Math.sin(th) * rad); } }pow(t, 2.4)是整个技巧的核心近处环密、远处环疏使得每一环大致覆盖相同数量的屏幕像素几何体预算被花在真正可见的地方。文档给出的经验对比是主体附近两千个三角形胜过均匀铺开的两万个——从源码结构看参考 demodemo/index.html的具体参数略有收紧AN620, RN48, R01.6, R1760也就是说约 620×48 个采样点并按同样的 2.4 次幂曲线分布。小心绕线方向winding极坐标网格上最容易犯的错误是把所有四边形都绕反最终从地面看到天空。文档给出的索引顺序是(a0, b1, b0, a0, a1, b1)并在 demo 中落地为见 demo/index.html 索引构建段const a1 (a 1) % AN; const a0 r * AN a, b0 r * AN a1, a2 (r 1) * AN a, b2 (r 1) * AN a1; idx.push(a0, b2, b0, a0, a2, b2);注意 demo 中环形角度方向索引做了% AN环绕闭合这是极坐标网格独有的细节。验收标准是在搭建任何其他内容之前先故意把相机绕到地平线以下看一眼如果透过地面能看到天空说明绕线反了。在叠加八度之前先扭曲采样域域扭曲domain warp是侵蚀感的最大来源文档给出的判断很直白朴素的 fBm 读起来像揉皱的纸而在求值地形之前先用另一个噪声场扭曲采样位置是通往看起来被侵蚀过的地形的最大一步function landH(x, z) { const wx x fbm(x * 0.012, z * 0.012, 3) * 26; // domain warp const wz z fbm(x * 0.012 41, z * 0.012 - 17, 3) * 26; let h fbm(wx * 0.0075, wz * 0.0075, 5) * 34; // broad landforms h ridged(wx * 0.021, wz * 0.021, 3) * 9; // ridge lines return h; }要点有三先把采样位置扭曲两个不同种子偏移的 fBm 各贡献 26 个单位的位移再在扭曲后的位置叠加大尺度地形的 fBm 与山脊的 ridged 噪声保持解析函数独立于网格。草、石头、雾以及任何散落物都必须采样同一个landH否则它们会悬浮或下沉。demo 中这一原则贯彻得很彻底地形网格顶点、石头放置、法线计算landN用有限差分landH(xe,z)-landH(x-e,z)求法线全部走同一个函数见 demo/index.html 的landH/landN山脊层必须随距离缩放在 40 个单位处读起来刚好的山脊振幅在 600 个单位处就不可见了远处的环需要把振幅乘上去否则地平线会变平。demo 的landH还额外演示了一个实用技巧将近场压平。相机在 80 个单位高度环绕绝对不能被包进山体里所以代码把d110的整片近场用地形高度乘以t*t*(3-2*t)t(d-110)/170的平滑过渡压成平地让地形成形都发生在远处见 demo/index.html 的landH实现。这解释了 demo 中相机脚下永远是平地、草场永远平坦的观感——草 patch 平移也因此只需要做平移而无需重新采样地形。按地形在做什么着色而不是贴图斜率与湿度派生的顶点色平铺地面贴图总会暴露自己。文档给出的方案是用地形自身的属性计算顶点色SKILL.md 第 4 节const slope 1 - normal.y; // steep rock const moist smoothstep(-4, 6, -height); // low wet, green const c rock.clone() .lerp(grass, (1 - slope * 3.2) * (0.35 moist * 0.65)) .lerp(sand, Math.max(0, 0.5 - moist) * 0.6);这带来三个免费收益悬崖变石色、洼地变绿色、山脊变浅色——不需要 UV也没有接缝。为避免颜色出现条带还要在顶点色上叠加一层极低频噪声。demo 的实现与之对应demo/index.html 地形着色段const slope 1 - n.y; const moist Math.max(0, Math.min(1, (3.0 - y) / 12)); const tint NZ.fbm(x * 0.02, z * 0.02, 2) * 0.5 0.5; c.copy(ROCK) .lerp(GRASS, Math.max(0, Math.min(1, 1 - slope * 3.4)) * (0.34 moist * 0.66)) .lerp(SAND, Math.max(0, 0.42 - moist) * 0.55) .lerp(DARK, tint * 0.22);两个细节值得注意demo 中slope由有限差分法线landN的n.y计算而不是网格自身的顶点法线——这样石头、草丛与地形对坡度的判断完全一致demo 额外把远环向雾色渐变c.lerp(new THREE.Color(0xd8c9a4), Math.pow(t, 2.6) * 0.55)让地平线没有硬边与雾色自然衔接。配色上 demo 使用MeshLambertMaterial({vertexColors:true})色板为 ROCK/GRASS/SAND/DARK 四色线框模式的 wire 网格MeshBasicMaterial({wireframe:true, opacity:0.16})则用于 GRID 开关直观展示极坐标网格结构demo/index.html。草一条 ribbon全部在顶点着色器里塑形十万根草 一个 InstancedMesh文档的核心主张是10 万根草只是单条五段 ribbon 的InstancedMesh每一次弯曲、倾斜、收窄和阵风都发生在顶点着色器里因此风的成本在 CPU 上为零SKILL.md 第 5 节const geo new THREE.BufferGeometry(); // 11 verts: a strip a tip const mesh new THREE.InstancedMesh(geo, mat, 104000); mat.onBeforeCompile sh { Object.assign(sh.uniforms, grassUni); sh.vertexShader uniform float uTime, uWindAmp; uniform vec2 uWind; attribute vec4 aParams; // height, phase, tint, lean varying float vT; varying float vTint; sh.vertexShader.replace(#include begin_vertex, float gT position.y; // 0 at root, 1 at tip float gBend uRestBend sin(uTime * 1.7 aParams.y) * uWindAmp; vec2 gRib ... ; // sweep the blade along an arc vec3 transformed vec3(gRib.x, gT * aParams.x, gRib.y); ); }; mat.customProgramCacheKey () grass; // or every instance recompiles两个工程要点customProgramCacheKey必须有。onBeforeCompile会改变着色器源码若不提供自定义缓存键每个实例都会触发一次重编译草场 patch 要跟着相机走。保持一片固定网格的草围绕观察者把网格按格子尺寸做整数对齐平移而不是向外扩张草场。玩家永远到不了边缘你也永远不为身后的草付费。demo 把这一整套落实得很完整demo/index.html 草场段几何体5 段BSEG5条带每段 2 顶点 1 个尖端顶点共 11 个顶点实例数GRASS_N150000skill 示例为 104000参考 demo 为 15 万每实例aParams四分量高度0.30 Math.random()*0.36、相位0~2π、色调随机、静息倾角0.10~0.36patch 半径GRASS_R64按面积均匀采样radMath.sqrt(Math.random())*GRASS_R草场沿镜头前方GRASS_AHEAD74个单位、按CELL2的格子取整平移placeCamera中的Math.round(.../CELL)*CELL同时把uOrigin传给着色器顶点着色器中风相位基于世界坐标而非局部坐标gWorldgRoot.xzuOrigin后取 sin否则阵风纹理会随着 patch 一起漂移patch 边缘淡出gEdge 1.0 - smoothstep(0.74, 1.0, length(gRoot.xz)/uRadius)避免 patch 边缘在草地上留下一道标尺线整体im.position.y -0.5下移与近场被压平的地形吻合。一个能坑掉你一小时的颜色陷阱文档专门用一段强调了不要在片元着色器里硬编码草色然后指望材质颜色能改它。diffuseColor.rgb * mix(base, tip, t)是乘运算——白色材质乘上绿色常量仍然是绿色每一个 debug 输出都会告诉你材质是白的但画面就是不变。凡是需要给草重新上色的场景积雪、季节、夜间调色板那个 mix 必须使用自己的 uniform。demo 用uSnow演示了正确姿势demo/index.html 草片元着色器段vec3 gBase vec3(0.150, 0.180, 0.092) * (0.74 vTint * 0.54); vec3 gTip vec3(0.560, 0.630, 0.330) * (0.76 vTint * 0.56); gBase mix(gBase, vec3(0.60, 0.65, 0.72), uSnow * 0.86); gTip mix(gTip , vec3(0.90, 0.94, 1.00), uSnow); diffuseColor.rgb * mix(gBase, gTip, pow(vT, 0.78)) * 1.30;草色深浅在着色器内完成材质MeshLambertMaterial({color:0xffffff})保持白色任何外部 recolor 都走uSnow之类的 uniform。石头来自同一个高度函数石头与草不同它们要给眼睛提供度量尺。文档给出的做法是按坡度做拒绝采样rejection sampling散点然后setMatrixAt写入InstancedMesh把 icosphere 压扁并焊接让石头看起来是嵌入地面而非搁在地上SKILL.md 第 6 节geo.scale(1, 0.62, 1); geo.translate(0, 0.3, 0); // sunk, not resting三四种尺寸的几千块石头就足够了——它们最要紧的是出现在主体附近给眼睛一个度量尺度的参照。demo 的实现细节demo/index.html 石头段IcosahedronGeometry(0.5, 1)压扁scale(1, 0.62, 1)并上移translate(0, 0.3, 0)实现下沉1600 块径向2.0 Math.pow(Math.random(), 0.55) * 95分布坡度拒绝if (landN(x, z).y 0.80) continue;——只有法线足够竖直坡度够缓的位置才放石头石头不会出现在悬崖上放置高度取自同一landH(x,z)再减去sc*0.12让石头微陷入土每块石头在 x/y/z 三个轴向独立随机缩放约 0.10~0.52打破重复感。天空8×512 画布上的六个渐变压点把垂直渐变画进一张小 canvas再映射到背面球体MeshBasicMaterial({side: THREE.BackSide})重绘成本低到可以在过渡的每一帧都重画SKILL.md 第 7 节const grd ctx.createLinearGradient(0, 0, 0, 512); [0, 0.30, 0.52, 0.68, 0.84, 1].forEach((s, i) grd.addColorStop(s, cols[i])); ctx.fillStyle grd; ctx.fillRect(0, 0, 8, 512); skyMat.map.needsUpdate true;六个压点是定数三个压点会变成CSS 渐变十个压点又无法调校。而且地平线压点要略微放在几何地平线之下让雾色与天空在没有可见线条的情况下相接。demo 的实现demo/index.html 天空段SKY_STOPS[0, 0.30, 0.52, 0.68, 0.84, 1]天空球SphereGeometry(900, 32, 24)renderOrder-20保证最先绘制、永不遮挡场景fog:false, depthWrite:false让天空不受雾影响也不参与深度写入paintSky在每帧时间过渡中通过skyMat.map.needsUpdatetrue重画贴图实现天空颜色的连续插值。星星只放在相机够得到的纬度带里最常见的错误是把星星撒满整个球面。文档的估算很直观约 10° 镜头的画面只看到球面的约 0.5%把 1200 颗星撒满球面屏幕上大概只有 6 颗看起来像 bugSKILL.md 第 8 节。正确做法是把星星限制在相机仰角夹子实际允许到达的纬度带内用三个尺寸等级、不同点尺寸让亮度有变化用devicePixelRatio缩放PointsMaterial.size否则视网膜屏上星星会消失开加色混合AdditiveBlending、sizeAttenuation:false、fog:false、depthWrite:false。文档给出的量级是三个等级约 3.9 万个点每层一个 draw call。demo 的实现demo/index.html 星星段略作收敛[22000, 1.7*DPR], [6000, 2.7*DPR], [1400, 4.6*DPR]共 29400 点纬度带u限制在[-0.06, 0.85]与相机仰角夹子EL_MIN-0.004, EL_MAX0.20对应并且星色做了三种温度采样w0.88暖色[1, 0.87, 0.70]、w0.13冷蓝[0.74, 0.84, 1]、其余近白[0.97, 0.97, 0.93]再乘0.46 Math.pow(rnd(), 1.7) * 0.54的随机亮度点纹理来自 64×64 canvas 的径向渐变软点星星初始opacity:0由昼夜系统的stars值统一驱动显隐m.opacity st; m.visible st 0.005夜晚才出现。时间系统插值而不是切换每个时段是一整套完整状态文档的关键设计是把每个时段存成完整状态——太阳位置与颜色、半球光、环境光、补光、轮廓光、雾色与远近、地面色、草色、六个天空压点、阴影强度、星星不透明度——然后在两个状态之间 lerpSKILL.md 第 9 节function applyState(A, B, t) { key.position.setFromSpherical(lerpAngle(A.sun, B.sun, t)); key.color.copy(A.sunC).clone().lerp(B.sunC, t); scene.fog.color.copy(A.fog).lerp(B.fog, t); for (let i 0; i 6; i) CUR.sky[i].copy(A.sky[i]).lerp(B.sky[i], t); paintSky(CUR.sky); }demo 完整实现了这一架构demo/index.html 时间系统段THEMES数组定义 MORNING / NOON / SUNSET / NIGHT 四套状态每套包含css页面主题色用于同步 UI 的浅/深色、sky六色、fog及远近、ground/grass、sun方位角、高度角、颜色、强度、hemiC/ambC/fillC三组灯光、stars不透明度。applyState逐项插值太阳按方位角/高度角换算成DirectionalLight位置并让阴影相机随太阳高度角动态收放ext 22 34 * Math.cos(el)半球光、环境光、补光、雾、地面色、草色、六个天空压点、星星不透明度全部同步插值过渡时长 1.25 秒使用 easeInOut 曲线themeMix0.5 ? 2*t*t : 1 - Math.pow(-2*t2,2)/2逐帧调用applyState并重绘天空 canvas。中途切换冻结当前插值态而不是回跳文档点名的第二个坑是用户在过渡中途切换时要把当前已插值出的状态冻结为新的起点 A而不是回到上一个预设否则每次不耐烦的点击都会跳变SKILL.md 第 9 节。demo 的applyTheme正是这样实现的切换时把themeFrom里每个字段按当前进度t重新插值一遍A.sunC A.sunC.clone().lerp(B.sunC, t)等再以themeMix 0重新开始——连续点按 TIME 按钮只会连续平滑渐变绝不跳变。天气是乘数不是更多预设最后一个架构原则天气作为叠加在上面的乘数永远不要做成新的预设。四个时段 × 四种天气应该是四个状态 四个修饰符而不是十六个状态SKILL.md 第 9 节。demo 中uSnow就是这种乘数式天气的雏形——它不改任何时段定义只作为 uniform 叠加在草色 mix 上更完整的雨/雪/风暴方案可进一步参考 threejs-weather。收尾前的验收清单文档在第 10 节给出了五条做完之前必须检查的项目这也是参考 demo 的可验证验收标准SKILL.md 第 10 节把相机绕到地平线以下一次。如果透过地面能看到天空绕线反了看最远的一环。如果远处地形是平的说明你的山脊振幅没有随距离缩放连续两次在过渡中途切换时段。画面任何时候都不该跳变在视网膜屏上检查。星星和细几何体是 DPR bug 最先暴露的地方盯 draw call 数量而不是三角形数量。文档给出的对比主张是18 个 draw call 内的 130 万三角形可以跑 120fps而 900 个 draw call 的 20 万三角形跑不到——批量提交比顶点总量更关键。demo 为此还内置了无头验证钩子demo/index.html 末尾window.__theme可瞬间切换时段并渲染window.__stats返回{fps, tri, calls, grass, stones, theme}的运行时统计数据右上角常驻显示实时的 fps / 三角形数 / draw call 数——你可以用这些数字直接核对上述第 5 条而不必肉眼估算。把这个 skill 用起来Agent 提示词三板斧如果你通过 Codex、Claude、Cursor 等 AI 编码 Agent 使用该 skilldemo/PROMPT.md 提供了三档提示词模板最小提示Use $threejs-landscape to put this scene in a real place: a noise heightfield, instanced grass, a gradient sky, and morning/noon/sunset/night that cross-fade.——一句话锁定全部核心交付复刻演示要求以 demo/index.html 为视觉、动效与性能参考实现A Place To Stand Something In——一份独立 HTML包含可拖拽环绕/缩放、四时段交叉淡化、GRID 线框开关并满足实现契约中的全部条目极坐标网格、域扭曲、无贴图顶点色、顶点着色器塑形草、纬度带星星、插值时段混搭改造Use $threejs-landscape but make it a coastal version: wet sand, tide line, and marram grass in clumps rather than an even field.或加上跟随地形的慢速镜头推拉。在具体集成时请保持解析地形函数与网格分离这条总纲所有散落物草、石头、星星、雾采样同一个landH所有时段只做状态插值所有天气只做乘数——这样你就得到了一个分辨率跟随镜头、成本可控、可被任意主体站在上面的程序化风景。小结从 SKILL.md 的方法论到 demo/index.html 的落地实现这套方案的技术主线可以归纳为四条几何预算跟着镜头走——极坐标网格 pow(t, 2.4)让每一环覆盖等量像素一切程序化、拒绝贴图——域扭曲噪声出地形、斜率/湿度出顶点色、六压点 canvas 出天空CPU 零成本的风——单条 ribbon 的InstancedMesh把全部草形变放进顶点着色器patch 平移而非扩张时间是插值不是开关——完整状态 中途冻结昼夜无缝天气只做乘数。配合 demo/PROMPT.md 的提示词与 agents/openai.yaml 的默认配置即可在任意 AI 编码工作流中复现这一套安静退场的风景。赞分享【免费下载链接】SkillsAgent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents项目地址https://gitcode.com/gh_mirrors/skills48/Skills点击查看免费下载相关推荐Understand-Anything3步生成代码知识图谱Understand Anything3步生成代码知识图谱 刚接手一个二十万行的陌生项目不知道从哪下手Understand Anything 用一条命令扫Ant引擎昼夜系统时间变化与光照过渡Ant引擎昼夜系统时间变化与光照过渡 引言 在游戏开发和实时渲染领域实现逼真的昼夜循环是创造沉浸式体验的关键技术。Ant引擎通过其先进的昼夜系统为开发者提游戏开发图形学3D渲染告别卡顿Godot地形草渲染优化实例化与LOD实战指南告别卡顿Godot地形草渲染优化实例化与LOD实战指南 你还在为游戏中大量草丛导致帧率暴跌发愁本文将带你用Godot Engine的实例化Instanc游戏开发图形学跨平台上一篇Boot2Docker测试策略单元测试与集成测试容器化下一篇WinUtil终极指南一键批量安装软件与Windows系统优化神器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询