【three.js】用一个 HTML 文件写出写实风 3D 跑酷游戏 —— Chrome 恐龙 3D 复刻实战分享

📅 2026/7/18 14:46:56
【three.js】用一个 HTML 文件写出写实风 3D 跑酷游戏 —— Chrome 恐龙 3D 复刻实战分享
用一个 HTML 文件写出写实风 3D 跑酷游戏 —— Chrome 恐龙 3D 复刻实战分享关键词Three.js · 单文件交付 · 程序化生成 · 视差滚动 · 游戏手感成品dino3d.html双击即玩全部代码约 1000 行唯一外部依赖是一个 CDN 的script标签。优化后的效果为一、为什么要作茧自缚地只用一个文件这个项目的需求很朴素把 Chrome 断网页面那只像素小恐龙升级成写实风 3D 版加上更多障碍物树木、岩石、泥潭、飞鸟、野猪、蝎子拷给任何一台电脑都能直接跑不装 Node、不跑构建、不解压资源包。单文件 零安装听起来只是个打包问题实际上它像多米诺骨牌一样推倒了一连串技术选型不能用 ES Module。双击 HTML 是file://协议浏览器会用 CORS 拦掉import。所以只能用老式全局 script 标签引入 Three.js r128window.THREE。不能有图片。没有纹理贴图文件所有材质纹理必须运行时用 Canvas 画出来。不能有模型。没有 glTF/FBX恐龙、野猪、蝎子全部用球体、圆柱、圆锥现场捏。不能有音频文件。音效用 WebAudio 的振荡器现场合成。约束反而成了这个项目最有意思的部分——下面挑几个值得分享的点展开。二、没有美术资源写实感从哪来写实风 3D 的常规做法是 PBR 贴图 高模。一张贴图都没有的情况下我把写实感拆成了五个可以纯代码实现的要素2.1 一个 20 行的万能纹理机functionmakeNoiseTexture(base,spot,size,density,spotSize){constcdocument.createElement(canvas);c.widthc.heightsize;constctxc.getContext(2d);ctx.fillStylebase;ctx.fillRect(0,0,size,size);for(leti0;idensity;i){ctx.globalAlpha0.05Math.random()*0.22;// 低透明度随机噪点ctx.fillStylespot;ctx.beginPath();ctx.arc(Math.random()*size,Math.random()*size,Math.random()*spotSize0.5,0,Math.PI*2);ctx.fill();}consttnewTHREE.CanvasTexture(c);t.wrapSt.wrapTTHREE.RepeatWrapping;returnt;}原理简单到不好意思底色 N 个低透明度噪点圆。但换一组参数就是一种材质——配方效果绿底 深绿点恐龙鳞片棕底 深棕点树皮 / 野猪毛灰底 深灰点花岗岩深棕底 黑点 roughness 0.35湿泥潭低粗糙度出反光湿的感觉全靠它一个隐藏要点质感一半靠贴图一半靠roughness。同一张泥巴纹理roughness: 1是干土0.35就是烂泥塘。2.2 冷暖对冲的光照newTHREE.HemisphereLight(0xbfd8ff,0x8a6f4d,0.75);// 天光冷蓝 地面反弹暖棕newTHREE.DirectionalLight(0xfff2d8,1.35);// 黄昏暖阳 PCF 软阴影半球光是性价比之王一行代码就有了天空是蓝的、地面反光是暖的这种环境光叙事比单纯 AmbientLight 立体得多。2.3 雾是最便宜的氛围组scene.fognewTHREE.Fog(0xd8c8a8,60,220);这行代码同时干了三件事远山自动褪色产生大气透视障碍物在雾里生成、慢慢显形顺便掩护了凭空刷怪的穿帮生成点 x55 刚好在雾的 near60 之外雾色与天空渐变底部同色系地平线无缝衔接。2.4 顶点扰动的低成本地形圆锥体 最规则的假山。给它下半部分顶点加随机偏移再开flatShading立刻变成有棱有角的岩石山constgnewTHREE.ConeGeometry(w,h,7,3);constposg.attributes.position;for(leti0;ipos.count;i){if(pos.getY(i)h*0.45){pos.setX(i,pos.getX(i)(Math.random()-0.5)*w*0.25);pos.setZ(i,pos.getZ(i)(Math.random()-0.5)*w*0.25);}}g.computeVertexNormals();2.5 配色要解耦初版所有地面都是沙黄色系——荒原、跑道、雾、远山糊成一片。后来按图层重新分配色相和明度荒原 → 暗橄榄色干草绿斑 深色碎土跑道 → 浅暖沙色 两侧深色压边远山 → 冷灰蓝同屏元素想拉开层次色相、明度至少要错开一个。这条平面设计的常识在 3D 场景里同样成立。三、“玩家不动世界在动”跑酷游戏第一课恐龙的 X 坐标从头到尾没变过PLAYER_X -14跑动感全靠世界向后滚。好处有两个一是相机、光照、阴影相机的覆盖范围全部可以写死二是避免坐标无限增长——跑上半小时浮点精度崩了模型开始抖动经典的 floating origin 问题直接不存在。3.1 三层视差背景按深度拆成三层各自用不同比例消费同一个滚动量worldDx云层 5% ── 几乎不动 远景雪山 12% ── 慢慢挪 近景丘陵 35% ── 明显滑动 前景跑道 100% ── 全速飞驰速率差就是纵深感本身。人眼对近快远慢的运动视差极其敏感三层不同速一叠加平面立刻变深邃。3.2 踩坑无限循环的正确姿势背景层要无限循环。我的第一版是整个 Group 滚出一个周期就把position.x归零——结果每隔十几秒所有雪山集体闪现一次非常出戏。正确姿势是父组只管滚子对象各自回卷nearLayer.position.x-worldDx*0.35;// 父组永远向左for(constmofnearLayer.children){constwxm.position.xnearLayer.position.x;// 换算世界坐标if(wx-170)m.position.x320;// 谁出界谁自己绕回队尾}每帧最多只有一座山悄悄传送且发生在视野外雾中肉眼完全无感。地面同理用 3 块瓦片排队循环x -75就180。四、用球和圆柱捏一只能跑能跳的恐龙没有模型文件恐龙由约 25 个图元拼成。拼模型不难难的是让它能动。两个关键技巧4.1 枢轴前置想让腿绕髋关节摆动就把腿建成一个 GroupGroup 的原点放在髋关节处大腿小腿脚掌都挂在 Group 下面。之后legGroup.rotation.z θ就是标准的摆腿不需要任何骨骼系统。头部同理用geometry.translate()把几何体相对枢轴偏移旋转 Group 就是低头。4.2 踩坑转错轴腿会劈叉第一版跑步动画写的是leg.rotation.x上线一看恐龙双腿朝屏幕内外两侧劈叉像在跳鬼畜女团舞。原因很基础但极易犯前进方向是 X 轴前后摆腿应该在 XY 平面内旋转也就是绕 Z 轴。绕 X 轴转的是 YZ 平面——侧向劈叉。// ❌ 侧向劈叉legs[0].rotation.xMath.sin(t*spd)*0.85;// ✅ 一前一后奔跑legs[0].rotation.zMath.sin(t*spd)*0.85;legs[1].rotation.zMath.sin(t*spdMath.PI)*0.85;// 反相心得给图元角色做程序动画之前先在纸上画一下自己的坐标系。4.3 所有过渡动画只用一个公式下蹲压扁、低头、死亡倒地……全部状态过渡共用一行帧率无关的指数插值value(target-value)*Math.min(1,dt*k);// k 越大跟随越快不需要 Tween 库。60 帧和 144 帧下手感一致dt已参与计算。五、游戏手感三个值得抄的设计5.1 可变高度跳跃长按跳高初版跳跃是固定初速度玩家反馈跳太矮、不可控。加高初速度那小障碍也得大跳节奏全毁。正确解法是平台跳跃游戏的祖传配方Variable Jump Height// 上升期且按住跳跃键 → 用低重力(-26)否则全重力(-68)constg(jumpHeldvy0)?HOLD_GRAVITY:GRAVITY;vyg*dt;// 提前松键 → 截断上升速度 → 小跳functionreleaseJump(){jumpHeldfalse;if(!onGroundvy7)vy7;}轻点小跳过蝎子按住大跳跨泥潭高度随按键时长连续变化。实现只有两个常量 一个布尔效果是手感质变。注意keydown要用e.repeat过滤系统连发keyup和touchend都要触发松键截断。5.2 碰撞判定故意降维故意放水场景是 3D 的但玩法是单车道横版所以碰撞直接降维成X 轴区间 Y 轴规则的 2D 判定没用Box3.setFromObject自动包围盒会把树冠、尾巴这些视觉突出物算进判定产生大量我明明没碰到的冤案手工 hitbox 让判定与视觉解耦可以按手感单独调参。三类障碍三套规则泥潭是我最喜欢的设计——只在贴地时判死if(hb.groundOnly)returndinoY0.35;// 泥潭腾空掠过安全elseif(hb.air)return垂直区间重叠(留0.25余量);// 飞鸟蹲下让头低于鸟腹elsereturndinoYhb.h-0.3;// 树/石跳得不够高所有阈值统一向玩家让利 0.25~0.35。原则判定宁可偏松绝不冤枉。玩家觉得擦过去了就必须让他过去一次视觉与判定不符的死亡比十次真实失误更劝退。5.3 难度曲线解锁制 联动收紧constpool[tree,rock];// 新手只见静物if(score150)pool.push(scorpion,mud);// 学会看地面if(score350)pool.push(bird,mud);// 学会下蹲if(score600)pool.push(boar,bird);// 迎面高速单位三个小心思重复 push 等于加权重生成间隔与速度联动max(0.55, 1.5 - speed·0.018)但保底 0.55 秒反应窗口野猪、飞鸟带extraSpeed迎面冲刺打破匀速滚动的节奏惯性。六、零文件音效WebAudio 三行合成constoaudioCtx.createOscillator(),gaudioCtx.createGain();o.frequency.value880;g.gain.exponentialRampToValueAtTime(0.001,now0.1);// 指数衰减包络跳跃 方波 520760Hz 双音过百分 正弦 880→1175Hz 上行致敬原版里程碑音死亡 锯齿波 220140Hz 下坠。合计不到 30 行。两个工程细节AudioContext必须在用户手势后resume()自动播放策略整体try/catch包裹音频不可用时静默降级游戏照跑。七、性能让核显笔记本也满 60 帧手段一句话粒子对象池22 个尘土球预创建循环复用运行期零 GC及时回收障碍出左边界立即scene.remove场景物体数恒定setPixelRatio(min(dpr, 2))4K 屏别傻乎乎按 3x 渲染低面数球 8~18 段、锥 5~7 棱全场 3 万三角形dt min(delta, 0.05)切后台回来不会一帧飞出十米穿模阴影相机收紧2048 阴影贴图只覆盖 ±60 游戏区一寸不浪费八、踩坑清单血泪浓缩版file://下 ES Module 会死单文件交付请用全局构建版类库。背景整层归零回卷 集体闪现父组只滚动子对象各自出界回卷。程序动画转错轴 鬼畜劈叉前进方向是 X前后摆腿绕 Z 转。hitbox 高度重复计算飞鸟的position.y已含飞行高度yOff再存一份就是双倍高度判定飘到天上去。同一个量只允许存在一个权威来源。同色系糊成一锅粥场景分层不仅是 Z 坐标分层配色也要分层。固定跳跃高度手感差可变跳跃高度是跑酷/平台游戏的标配成本极低别省。九、结语这个项目最大的收获是重新体会到约束催生设计不能加载资源逼出了程序化纹理和图元建模不能用构建工具逼出了对模块边界的清醒认识一个 1000 行的文件反而比很多规范工程更容易讲清楚每一帧发生了什么。下次断网的时候希望你想起的不是那只像素小恐龙而是——自己动手一个 HTML 文件就能造出一个世界。想要下载源码和游戏绿泡泡 码来的小朋友