今日开源[第48期]Operation Ironhold 游戏项目解读

Operation Ironhold 项目深度解读

一个被 AI 代理用 5 条提示词「写」出来的完整浏览器 FPS 游戏。整个游戏是 单个 290 KB 的 index.html,零构建、零素材、零 npm 依赖,唯一的外部资源是从 CDN 拉取的 three.js r128。


目录

  1. 项目概览:名称 / 作者 / 作用 / 背景
  2. 安装、使用教程与运行条件
  3. 工作流程与执行步骤
  4. 文件逐个解读(按模块)
  5. 优势、不足、创新点与亮点

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 条提示词分别对应:

  1. 完整规格书(一次性给出玩法、武器、敌人、地图、HUD、音效、后处理、性能要求的万字级需求);
  2. ADS + 狙击 + 跑酷(加入第四把武器、瞄准镜、攀爬/二段跳);
  3. Bug 修复清单(最长的修正轮次,碰撞/敌人 AI/武器可靠性被整体重建);
  4. 准星重做(改为 Valorant 风格竞技准星);
  5. 敌人朝向 / 天空像素化 / 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=2updateCombatDirector() 仅放行距玩家最近的 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 优势

  1. 极致可移植:单 HTML 文件,无构建、无依赖、无素材,双击即玩。分发成本趋近于零。
  2. 全程序化生成:贴图、模型、天空、音效全部运行时合成 —— 仓库里零二进制资产,体积仅 290 KB 却塞进完整 FPS。
  3. 工程严谨性超出"AI 玩具"预期
    • 碰撞用 blocked/moveSlide/depenetrate 三层防护,连"已嵌进箱子的身体如何挤出"都处理了;
    • 敌人 AI 与玩家共享同一组碰撞体做网格 BFS 寻路,不会卡墙;
    • 战斗导演把"同时向你开火"限制在 2 人,刻意把不可赢的 30 DPS 交叉火力压下去;
    • 后坐力是固定可学习模式而非纯随机,奖励压枪技巧。
  4. 性能意识:动态分辨率(adaptive 看帧时间调渲染比例)、InstancedMesh 按色分桶、静态景物合批、曳光/贴花/弹壳/粒子池化、阴影投射按包围盒裁剪(砍掉约 2/3 阴影 pass)。
  5. 可测试性:两个 tools/ 脚本提供碰撞审计、武器保真、命中保真、边界、敌人看门与自动机器人,把"AI 写出来的游戏是否真能玩"变成可回归的断言。
  6. 透明度PROMPTS.md 把 5 轮提示词原样公开,是 AI 游戏开发难得的"需求→代码"对照档案。

5.2 不足

  1. 单文件结构难维护:6,500 行挤在一个 HTML 里,没有模块边界,二次开发需通读全文,IDE 支持弱。
  2. 质量天花板由 AI 代理与提示词决定:作为 agent 产物,深层 bug(敌人朝向、天空像素化、M4 倍率)需第 3、5 轮专门修正,说明一次性产出仍有明显缺陷。
  3. 内容体量小:仅 1 张地图、4 把武器、10 个敌人、单回合,属 demo/展示级而非完整游戏。
  4. 依赖 CDN three.js:离线无法启动(库本身未内联);r128 是较老版本,EffectComposer 缺失迫使后处理全手写。
  5. 无移动端 / 无触屏:明确不支持手机。
  6. 强绑定英文/西方军工美术语境:命名、无线电callout、HUD 均为英文,未做本地化。

5.3 创新点 / 亮点

  1. 「提示词即规格书」的可复盘范式PROMPTS.md 把游戏从"一句想法"到"成品"的 5 步映射完整留档,本身就是 AI 辅助开发的方法论样本。
  2. 把"提出好问题"外置为工程约束:3 万字的初始提示词 + 专门的 Bug 修复轮次,展示了如何用语义化需求驱动 agent 产出可玩系统。
  3. 手写后处理链路取代 EffectComposer:r128 无 EffectComposer,作者用一对 render target + bright→blur×4→comp 自己拼出泛光/色差/暗角/调色/受伤/低血,且阴影染色用乘性(加性会在线性空间淹没近黑像素导致全屏泛蓝)——这是踩过坑才有的判断。
  4. 逐片元天空着色器替代 equirect 贴图:README 清晰论证了"4096² 等距贴图在 15° 狙击视野下只有 11 像素/纹理元"的失败,改用 dir.xz/dir.y 把云投影到头顶平面、并用 fwidth 做 band-limited 抗锯齿——把"为什么不用贴图"讲成了图形学课。
  5. 视模型长焦配对(VM_FOV 41.9 vs 世界 75):用"更窄镜头 + 把枪推更远"的配对压平深度梯度,避免枪托在机匣上膨胀;adsPos 用数据描述瞄具对齐,移动瞄具必须同步改数——把"手感对齐"变成可审计的常量。
  6. 轻量但真实的 AI 约束集MAX_SHOOTERS=2、残局 sweep、人类反应延迟、枪口被挡不开火、丢失视线压制射击——一组简单规则就制造出"可读的交火"与"有阶段的接战"。
  7. 色彩空间手动管理:渲染器走线性输出、复合着色器自己做 gamma,linearizeMats() 在启动把所有材质推过 convertSRGBToLinear(),新材质不处理就会过饱和——把 three.js 的色彩陷阱显式工程化。
posted @ 2026-08-04 17:51  zhang-yd  阅读(65)  评论(0)    收藏  举报