今日开源[第48期]Operation Ironhold 游戏项目解读
Operation Ironhold 项目深度解读
一个被 AI 代理用 5 条提示词「写」出来的完整浏览器 FPS 游戏。整个游戏是 单个 290 KB 的
index.html,零构建、零素材、零 npm 依赖,唯一的外部资源是从 CDN 拉取的 three.js r128。
目录
1. 项目概览
1.1 名称
Operation Ironhold(钢铁守望行动) —— 一个发生在「7 区集装箱货场」的单人第一人称射击游戏。玩家需要在 3 分钟内清除盘踞货场的 10 名敌对士兵。
1.2 作者及其介绍
| 项 | 内容 |
|---|---|
| GitHub 作者 | StarKnightt(user id 92244026) |
| 共同提交者 | cursoragent(即 Cursor 的 AI 编码代理,标注为构建者) |
| 许可证 | MIT |
| 仓库规模 | 5 次提交,结构极简(单游戏文件 + 提示词 + 2 个开发脚本 + 截图) |
| 代码规模 | 约 6,500 行 JavaScript + CSS,全部内联进一个 299,091 字节(≈290 KB)的 index.html |
| 发布 | 提供 GitHub Pages 在线试玩 |
关键事实:这个项目本身就是「AI 代理编程」的产物。仓库里有一份 PROMPTS.md,逐字记录了驱动整个游戏生成的 5 条提示词(连错别字都保留)。也就是说,游戏不是人手写出来的,而是作者用 5 轮对话「描述需求」让 Cursor agent 实现的。这让它成为 AI 辅助游戏开发的一个紧凑、可复盘的样本。
1.3 作用
- 一文件即游戏:打开 HTML 即玩,没有任何构建步骤、包管理器或图片/音频/模型文件。
- 全运行时生成:混凝土、波纹钢板、木箱、锈迹、砖墙、铁丝网贴图在加载时用
<canvas>画出来;武器、手、士兵由 box/cylinder/sphere 拼接;天空是逐片元着色器;所有音效用 Web Audio API 合成。 - 完整 FPS 体验:4 把武器(步枪/霰弹/手枪/狙击)、ADS 开镜、跑酷攀爬、10 个有 AI 的敌人、HUD(准星/血甲/弹药/小地图/击杀提示/伤害方向)、后处理(泛光/色差/暗角/调色/受伤红染/低血脉冲)、性能优化(动态分辨率、实例化、合批、对象池)。
1.4 背景
作者把「让 AI 从自然语言规格直接产出可玩游戏」作为目标。整个仓库刻意保持最小:游戏逻辑全在 index.html,而 PROMPTS.md 把「需求 → 成品」的映射原样留档。5 条提示词分别对应:
- 完整规格书(一次性给出玩法、武器、敌人、地图、HUD、音效、后处理、性能要求的万字级需求);
- ADS + 狙击 + 跑酷(加入第四把武器、瞄准镜、攀爬/二段跳);
- Bug 修复清单(最长的修正轮次,碰撞/敌人 AI/武器可靠性被整体重建);
- 准星重做(改为 Valorant 风格竞技准星);
- 敌人朝向 / 天空像素化 / M4 倍率 三处收尾修复(附两张截图)。
2. 安装使用教程与运行条件
2.1 安装 —— 几乎为零
整个安装过程 = 在桌面浏览器里打开
index.html。
方式 A(最简单):双击 index.html 用任意桌面浏览器打开。
方式 B(本地起服务,推荐以避免某些浏览器的 file:// 限制):
cd operation-ironhold
python -m http.server 8123
# 然后访问 http://127.0.0.1:8123/index.html
方式 C(在线):直接访问 GitHub Pages 试玩链接 https://starknightt.github.io/operation-ironhold/。
2.2 启动与交互
- 点击页面 即开始。点击这一动作同时用来请求 Pointer Lock(指针锁定)——浏览器规定鼠标捕获必须由用户手势触发。
- 按
Esc释放指针并暂停,再点覆盖层恢复。 - 没有触屏输入,因此手机上无法游玩。
2.3 操作键位
| 输入 | 动作 |
|---|---|
W A S D |
移动 |
| 鼠标 | 视角 |
| 左键 | 开火 |
| 右键 | 切换 ADS(开镜) |
R |
换弹 |
Shift |
冲刺 / 开镜时屏息 |
Ctrl |
蹲伏 |
Space |
跳跃;空中再点二段跳;贴近边缘按住可攀爬 |
1 2 3 4 |
M4 卡宾枪 / KS-12 泵动霰弹 / P-9 手枪 / SR-7 长弓狙击 |
V |
步枪在连发/半自动间切换 |
| 鼠标滚轮 | 切枪 |
Esc |
暂停 |
2.4 运行依赖与条件
| 类别 | 要求 |
|---|---|
| 硬件 | 任意能跑 WebGL 的桌面设备;无 GPU 加速要求;中端硬件目标 60fps |
| 软件 | 桌面浏览器(Chrome / Firefox / Edge 等)支持 WebGL + Pointer Lock |
| 网络 | 首次加载需联网拉取 CDN 上的 three.js r128(已内联游戏逻辑,仅库本身在线) |
| 构建 | 无 —— 没有 npm、没有打包器、没有编译 |
| 二进制素材 | 完全没有 —— 全部程序化生成 |
2.5 回合规则
- 3:00 内消灭全部 10 名敌人即胜;阵亡或超时即败。
- 初始 100 血 + 50 甲,甲在耗尽前吸收一半伤害。
- 敌人前 3 秒不开火(让你进入状态);战斗导演限制同时朝你开火的敌人最多 2 个(其余继续机动),这是保持交火「可读」而非被交叉火力淹没的关键约束。
- 结算屏显示击杀数、爆头数、命中率、存活时间。
3. 工作流程与执行步骤
3.1 启动期(一次性)
1. 错误陷阱 + three.js 加载检查(THREE 未定义则报错停摆)
2. 定义辅助函数:clamp / lerp / rand / mergeGeoms(无插件几何体合并)/ linearizeMats / damp
3. SFX 模块定义(仅定义,需用户手势后 init)
4. 渲染器 / 场景 / 相机 / 独立视图模型场景(vmScene + vmCamera, FOV 41.9) 建好;allocTargets() 分配离屏 RT
5. 后处理链:postMat 创建 brightMat / blurMat / compMat;adaptive 动态分辨率闭包初始化
6. 程序化纹理:makeConcrete / makeContainerTex / makeBarrelTex / makeCrateTex ... 填充 TEX 字典
7. 光照 + 天空穹顶着色器 skyMat + 远景天际线 rank()
8. 世界生成:地面 / 围栏 / 仓库(含坡道与狙击台) / 集装箱 / 板条箱 / 油桶,填充 colliders / worldSolid / mapRects
9. 武器视图模型 build(buildRifle/buildShotgun/buildPistol/buildSniper)
10. spawnEnemies() 生成 10 名敌人;WEAPONS[0] 视图模型可见;updateVitalsUI / updateAmmoUI / layoutCrosshair
11. UI.boot 隐藏;requestAnimationFrame(frame) 进入主循环
3.2 主循环 frame(now)(每帧)
requestAnimationFrame(frame)
├─ dt = clamp((now-last)/1000, 0, 0.05) // 防止卡顿后穿模
├─ adaptive(dt) // 动态分辨率
├─ if G.running && !G.over:
│ G.elapsed/time/grace 计时
│ updatePlayer(dt) // 移动/跳跃/蹲/冲/瞄准/开火输入
│ updateCombatDirector() // 仅最近 2 个有视线的敌人可开火
│ for each enemy: updateEnemy(e, dt) // 感知→状态机→战术→开火
│ if G.time<=0: endGame(false)
├─ else: 菜单慢速运镜(集装箱从近旁掠过产生视差)
├─ 常驻视觉系统:天空/尘埃/太阳光束/视图模型/弹壳/曳光/粒子/准星/伤害弧 更新
├─ HUD 节流刷新(0.1s)+ 小地图(0.055s)
├─ 受伤/低血脉冲 → compMat.uniforms
└─ 渲染管线(见下)
渲染管线(离屏合成,手写后处理):
renderer.setRenderTarget(sceneRT); renderer.render(scene, camera)
if started: clearDepth(); renderer.render(vmScene, vmCamera) // 视图模型叠在场景上
brightMat: sceneRT → bloomA // 提亮部
blurMat ×4(横/纵/横×2.6/纵×2.6): bloomA ↔ bloomB // 分离模糊
compMat(sceneRT + bloomA) → 屏幕 // 合成:泛光+色差+暗角+调色+受伤+低血+噪点
3.3 一次「开火」的执行步骤(fireWeapon)
1. 冷却 / 换弹 / 切枪 / 泵动 / 栓动 任一进行中 → 返回 false
2. 弹匣空 → 空击声 + 自动起换弹;否则 mag--,置 fireCooldown = 60/rpm
3. 刷新敌人命中盒矩阵 → 用相机朝向从准星射出 shootRay(far = 武器射程)
4. 对每个弹丸(pellets):在 spread 锥内随机偏移方向,raycast 命中盒+worldSolid
- 命中敌人:距离衰减(falloff) × 部位倍率(头2x/腿0.78) → damageEnemy
- 命中墙:取面法线 → fxImpactWall
5. 命中则 SFX.hitBeep + showHitmark
6. 枪声 + alertToGunfire(噪声半径,让附近巡逻兵听到并转入交战)
7. 后坐力 = 固定模式(首踢+稳定爬升+反向横移),非纯随机 → 可学习压枪
8. 枪口闪光(flashT/flashPower) + 抛壳 + 枪口烟 + 曳光弹
9. updateAmmoUI
3.4 敌人 AI 的一次更新(updateEnemy)
死亡态 → ragdoll 倒下 + 枪械脱落物理
感知 → 每 0.13~0.19s 用 hasLOS 检测;首次发现进入 ALERT 并设 0.30~0.80s 人类反应延迟
状态机:
PATROL → 沿 waypoint 巡逻,到点上随机 idle
ALERT → 反应延迟倒计时,到点转 COMBAT 并无线电呼叫
COMBAT → 瞄准玩家、周期性重投战术(hold/flank/push)、掩体选取、横移、burst 开火、
弹匣打空自动换弹、丢失视线则压制射击 lastKnown 并 hunting;
枪口被挡(muzzleClear)则放弃该点而非隔箱盲射
障碍触须 → 近+远两次探针决定绕行方向,带迟滞防止在箱边乒乓抖动
3.5 开发期回归测试工作流(tools/,不进游戏)
tools/test-harness.js:手动经 DevTools 协议注入,暴露window.H测试接口(碰撞审计audit/tunnel/inside/charge、武器精度accuracy/wtest、命中保真fidelity、边界escape/ram、敌人看门watch)。tools/autoplay-bot.js:定义window.__human(err, react)自动机器人,用与玩家相同的colliders做网格 BFS 寻路,自主打完一整局,用于无值守回归。
4. 文件逐个解读(按模块)
4.1 根目录文件
| 文件 | 作用 |
|---|---|
index.html |
整个游戏(6,500 行)。唯一被浏览器加载的文件。结构见 4.2。 |
PROMPTS.md |
5 条构建提示词逐字记录,含错别字。是「需求→成品」的可复盘映射,也是本项目最独特的资产。 |
LICENSE |
MIT 许可证全文。 |
.gitignore |
忽略 node_modules 等(基本为模板)。 |
og.jpg |
Open Graph 社交分享卡片图(让分享链接显示为游戏而非裸 URL)。 |
screenshots/ |
gameplay.jpg / menu.jpg / scope.jpg —— 仅 README 引用,非游戏运行所需。 |
4.2 index.html 内部模块(按顺序)
文件内以「
/* N. 模块名 */」注释分节。以下为各节作用与关键代码。
| 行段 | 模块 | 作用 / 关键函数 |
|---|---|---|
| 头部 | CSS + HTML body | #hud/#scope/.screen 样式;<canvas id="c">、HUD 容器、开始/结束/暂停层、错误层 |
| 错误陷阱 | 启动守卫 | window.onerror 监听;检测 THREE 是否从 CDN 加载 |
| — | 辅助函数 | clamp lerp rand mergeGeoms(无插件合并 BufferGeometry)linearizeMats(sRGB→线性)damp(帧率无关平滑) |
| — | SFX 音频 | IIFE 返回对象:SFX.init() gunshot(kind,pan,dist) radio() footstep() impactWall/impactFlesh hitBeep(head) killChime() alarm(win) suspend/resume;基于 Web Audio 合成 + 程序化脉冲响应卷积混响 |
| — | 渲染/后处理 | renderer camera vmScene/vmCamera(FOV 41.9) allocTargets() adaptive(按帧时间中值调分辨率)postMat() blit() brightMat/blurMat/compMat FS_VERT |
| — | 程序化纹理 | makeConcrete/makeContainerTex/makeBarrelTex/makeCrateTex/makeFenceTex/makeWallTex/makeCamoTex/... 各自画 <canvas> 生成 CanvasTexture,汇入 TEX |
| — | 光照/天空 | 半球光 + 方向太阳光(带阴影);skyMat 逐片元噪声云穹顶;skyDome(renderOrder 1000);rank() 生成远景天际线剪影 |
| — | 世界/地图 | colliders/worldSolid/groundMesh/ceilMesh/mapRects 全局数组;MAT 材质字典;addCollider() box() instancedByColor();60×60 货场:地面标线、围栏+隐形墙、双层仓库(坡道+狙击台) |
| 3112 | 碰撞辅助 | blocked(x,z,y0,y1,r) depenetrate() moveSlide() nearestFree() nearestReachable() groundAt() ceilingAt();walkable 惰性洪泛填充(确保巡逻点可达,防止敌人卡在箱子里) |
| 3270 | FX 对象池 | 曳光/贴花/粒子(加性火花+alpha烟血)/弹壳/精灵 —— 全部池化复用 |
| 4014 | 武器定义 | WEAPONS 数组(M4/KS-12/P-9/SR-7 四把,含 magSize/reserve/damage/headMult/rpm/spread/recoil/adsFov/range/falloff/... 完整数值);buildRifle/... 构建视图模型 |
| 4323 | 敌人 | ENEMY_NAMES(VIPER…SHADOW)、makeCamoTex()(多尺度斑驳迷彩)、spawnEnemies()、enemyHitMeshes 重建、敌人对象结构(state/route/wp/hp/canSee/reactT/strafe/tactic/...) |
| 4825 | 武器函数 | setADS() toggleADS() switchWeapon() startReload() finishReload() fireWeapon() |
| 5073 | 伤害 | damagePlayer(amount, fromPos)(含护甲减半、受伤红染、伤害方向弧、死亡判定)damageEnemy() enemyShoot() |
| 5290 | 战斗导演 | MAX_SHOOTERS=2;updateCombatDirector() 仅放行距玩家最近的 2 个有视线敌人为 mayFire;残局(≤3 人)触发全图 sweep 搜捕,避免 hide-and-seek |
| 5338 | 敌人 AI | updateEnemy(e,dt):死亡 ragdoll、感知 LOS、ALERT 反应延迟、COMBAT 三战术(hold/flank/push)+掩体+横移+burst 开火+换弹、丢失视线压制、PATROL 巡逻、hasLOS() muzzleClear() pickCover()、障碍触须迟滞绕行 |
| 6386 | 主循环 | frame(now)(见 3.2)、渲染管线(见 3.2)、菜单运镜 |
4.3 tools/ 开发脚本
tools/test-harness.js(20.7 KB,开发专用,不被 index.html 引用)
- 顶部注释:
dev-only test harness, loaded manually over CDP; never referenced by index.html。 - 覆写
window.onerror收集__errs;暴露window.H接口:place()(摆位并清残留后坐)、frame()(跑两帧让状态沉降)、lane(d)(找一段开阔射击通道)、fidelity(n,d,ads)("无幽灵Miss"测试:比较准星射线落点与射击结果)、accuracy()、wtest()(逐武器检查开火/抛壳侧/闪光/ADS/换弹)、audit()/tunnel()/inside()/charge()(碰撞审计)、escape()/ram()(边界探测)、watch(secs)(敌人看门,标记卡进几何体或原地发呆者)。 - 末尾
H.rig()注入无敌钩子(window.__god)与时钟快进,输出'dev ready'。
tools/autoplay-bot.js(5.6 KB,开发专用)
NAV:基于blocked()的 0.5m 网格 BFS 寻路(与玩家遵守同一组碰撞体),返回朝目标的下一 waypoint —— 所以机器人绕掩体而非撞墙。window.__human(err, react):可配置 瞄准误差(弧度)/反应延迟 的自动玩家;循环内选最近有视线的敌人、转向、近距开镜、卡弹换弹、交火时横移、无目标时路由至最近活敌;落地贴地、自动换弹。输出'human bot running',用于无值守回归。
4.4 关键代码片段(真实节选)
① 武器数值定义(节选)
const WEAPONS = [
{ id:'rifle', name:'M4 CARBINE', magSize:30, reserve:210, damage:31, headMult:2.0,
rpm:760, auto:true, semiToggle:true, pellets:1,
spreadBase:0.0055, spreadMax:0.052, spreadShot:0.0058, spreadRecover:0.075,
recoilKick:0.035, recoilRot:0.055, fovKick:0.55, reloadTime:2.15, drawTime:0.55,
range:140, falloffStart:45, falloffRange:60, falloffMin:0.55,
adsFov:38, adsSpread:0.42, adsTime:0.20, noise:36, build:buildRifle },
{ id:'sniper', name:'SR-7 LONGBOW', magSize:5, reserve:25, damage:125, headMult:2.0,
rpm:999, auto:false, pellets:1, boltTime:1.5, range:220,
adsFov:15, adsSpread:0.012, adsTime:0.20, scope:true, noise:60, build:buildSniper }
/* shotgun / pistol 同理 */
];
② 碰撞与滑动移动
function blocked(x,z,y0,y1,radius){
for (let i=0;i<colliders.length;i++){
const c = colliders[i];
if (y1 <= c.minY + 0.02 || y0 >= c.maxY - 0.02) continue;
const cx = clamp(x,c.minX,c.maxX), cz = clamp(z,c.minZ,c.maxZ);
const dx = x-cx, dz = z-cz;
if (dx*dx + dz*dz < radius*radius) return true;
}
return false;
}
function moveSlide(pos, dx, dz, radius, height){
const y0 = pos.y + 0.30, y1 = pos.y + height - 0.05;
if (blocked(pos.x, pos.z, y0, y1, radius)){ // 已嵌进几何体 → 先挤出再动
pos.x += dx; pos.z += dz; depenetrate(pos, radius, y0, y1); return;
}
if (dx !== 0 && !blocked(pos.x+dx, pos.z, y0, y1, radius)) pos.x += dx;
if (dz !== 0 && !blocked(pos.x, pos.z+dz, y0, y1, radius)) pos.z += dz;
}
③ 战斗导演(最多 2 个射手)
const MAX_SHOOTERS = 2;
function updateCombatDirector(){
_active.length = 0;
for (const e of enemies){
e.mayFire = false;
if (!e.dead && e.state===ST.COMBAT && e.canSee) _active.push(e);
}
/* 残局搜捕:≤3 人时持续把幸存者重新指派去玩家大致方位 */
if (G.grace > 0) return;
_active.sort((a,b)=> a.obj.position.distanceToSquared(camera.position)
- b.obj.position.distanceToSquared(camera.position));
for (let i=0;i<Math.min(MAX_SHOOTERS,_active.length);i++) _active[i].mayFire = true;
}
④ 敌人感知与状态机(节选)
e.losTimer -= dt;
if (e.losTimer<=0){
e.losTimer = 0.13 + Math.random()*0.06;
const see = hasLOS(e);
if (see && !e.canSee && e.state===ST.PATROL){
e.state = ST.ALERT;
e.reactT = rand(0.30,0.80); // 人类反应延迟 300~800ms
e.tag.draw(e.hp,true);
}
e.canSee = see; if (see) e.lastSeen = 0;
}
⑤ 复合后处理着色器(compMat 片元,手写泛光/色差/暗角/调色/受伤/低血)
uniform sampler2D tScene, tBloom;
uniform vec2 res; uniform float time, bloom, vig, dmg, low, ab, flash, scope;
varying vec2 vUv;
vec3 aces(vec3 x){ const float a=2.51,b=0.03,c=2.43,d=0.59,e=0.14;
return clamp((x*(a*x+b))/(x*(c*x+d)+e),0.0,1.0); }
void main(){
vec2 uv = vUv; vec2 d = uv - 0.5; float r2 = dot(d,d);
float chroma = (ab + dmg*0.9)*0.02;
vec3 col;
col.r = texture2D(tScene, uv - d*chroma*r2).r; // 色差
col.g = texture2D(tScene, uv).g;
col.b = texture2D(tScene, uv + d*chroma*r2).b;
col += texture2D(tBloom, uv).rgb * bloom; // 泛光
float lum = dot(col, vec3(0.2126,0.7152,0.0722));
col = mix(vec3(lum), col, 0.78); // 去饱和
col = aces(col * 1.14);
col = (col - 0.5)*1.07 + 0.5;
col = mix(col, vec3(0.52,0.02,0.02), dmg*0.42); // 受伤红染
float edge = smoothstep(0.05, 0.30, r2);
col = mix(col, mix(col, vec3(0.42,0.0,0.0), 0.8), low*edge); // 低血脉冲
float vg = 1.0 - smoothstep(0.09, 0.52, r2*1.5);
col *= mix(1.0, 0.40 + 0.60*vg, vig*(1.0 - scope)); // 暗角
gl_FragColor = vec4(pow(max(col,0.0), vec3(0.4545)), 1.0);
}
⑥ 自动机器人寻路(节选)
const NAV = (() => {
const STEP = 0.5, W = Math.round(60/STEP)+1;
function build(){
free = new Uint8Array(W*W);
for (let j=0;j<W;j++) for (let i=0;i<W;i++)
free[j*W+i] = blocked(world(i), world(j), 0.35, 1.6, 0.42) ? 0 : 1;
}
return function nav(sx,sz,tx,tz){ /* BFS, 返回朝目标的下一 waypoint 或 null */ };
})();
5. 优势、不足、创新点与亮点
5.1 优势
- 极致可移植:单 HTML 文件,无构建、无依赖、无素材,双击即玩。分发成本趋近于零。
- 全程序化生成:贴图、模型、天空、音效全部运行时合成 —— 仓库里零二进制资产,体积仅 290 KB 却塞进完整 FPS。
- 工程严谨性超出"AI 玩具"预期:
- 碰撞用
blocked/moveSlide/depenetrate三层防护,连"已嵌进箱子的身体如何挤出"都处理了; - 敌人 AI 与玩家共享同一组碰撞体做网格 BFS 寻路,不会卡墙;
- 战斗导演把"同时向你开火"限制在 2 人,刻意把不可赢的 30 DPS 交叉火力压下去;
- 后坐力是固定可学习模式而非纯随机,奖励压枪技巧。
- 碰撞用
- 性能意识:动态分辨率(
adaptive看帧时间调渲染比例)、InstancedMesh按色分桶、静态景物合批、曳光/贴花/弹壳/粒子池化、阴影投射按包围盒裁剪(砍掉约 2/3 阴影 pass)。 - 可测试性:两个
tools/脚本提供碰撞审计、武器保真、命中保真、边界、敌人看门与自动机器人,把"AI 写出来的游戏是否真能玩"变成可回归的断言。 - 透明度:
PROMPTS.md把 5 轮提示词原样公开,是 AI 游戏开发难得的"需求→代码"对照档案。
5.2 不足
- 单文件结构难维护:6,500 行挤在一个 HTML 里,没有模块边界,二次开发需通读全文,IDE 支持弱。
- 质量天花板由 AI 代理与提示词决定:作为 agent 产物,深层 bug(敌人朝向、天空像素化、M4 倍率)需第 3、5 轮专门修正,说明一次性产出仍有明显缺陷。
- 内容体量小:仅 1 张地图、4 把武器、10 个敌人、单回合,属 demo/展示级而非完整游戏。
- 依赖 CDN three.js:离线无法启动(库本身未内联);r128 是较老版本,
EffectComposer缺失迫使后处理全手写。 - 无移动端 / 无触屏:明确不支持手机。
- 强绑定英文/西方军工美术语境:命名、无线电callout、HUD 均为英文,未做本地化。
5.3 创新点 / 亮点
- 「提示词即规格书」的可复盘范式:
PROMPTS.md把游戏从"一句想法"到"成品"的 5 步映射完整留档,本身就是 AI 辅助开发的方法论样本。 - 把"提出好问题"外置为工程约束:3 万字的初始提示词 + 专门的 Bug 修复轮次,展示了如何用语义化需求驱动 agent 产出可玩系统。
- 手写后处理链路取代 EffectComposer:r128 无
EffectComposer,作者用一对 render target +bright→blur×4→comp自己拼出泛光/色差/暗角/调色/受伤/低血,且阴影染色用乘性(加性会在线性空间淹没近黑像素导致全屏泛蓝)——这是踩过坑才有的判断。 - 逐片元天空着色器替代 equirect 贴图:README 清晰论证了"4096² 等距贴图在 15° 狙击视野下只有 11 像素/纹理元"的失败,改用
dir.xz/dir.y把云投影到头顶平面、并用fwidth做 band-limited 抗锯齿——把"为什么不用贴图"讲成了图形学课。 - 视模型长焦配对(VM_FOV 41.9 vs 世界 75):用"更窄镜头 + 把枪推更远"的配对压平深度梯度,避免枪托在机匣上膨胀;
adsPos用数据描述瞄具对齐,移动瞄具必须同步改数——把"手感对齐"变成可审计的常量。 - 轻量但真实的 AI 约束集:
MAX_SHOOTERS=2、残局 sweep、人类反应延迟、枪口被挡不开火、丢失视线压制射击——一组简单规则就制造出"可读的交火"与"有阶段的接战"。 - 色彩空间手动管理:渲染器走线性输出、复合着色器自己做 gamma,
linearizeMats()在启动把所有材质推过convertSRGBToLinear(),新材质不处理就会过饱和——把 three.js 的色彩陷阱显式工程化。

浙公网安备 33010602011771号