1. 从“鹈鹕骑车”说起一个3D测试基准的诞生大模型评测这件事做久了容易陷入一种疲惫。跑分榜单翻来覆去就是那几套题MMLU、GSM8K、HumanEval刷到后面连模型自己都能背出答案。真正让我感兴趣的是那种能一眼看出“这个模型到底懂不懂空间关系”的测试。前段时间“鹈鹕骑车”这个梗又火了一轮各种版本满天飞但绝大多数都是2D的SVG或者静态图片。我就琢磨能不能把它做成一个真正的3D场景用Three.js在浏览器里跑起来然后拿它去测一批大模型看看谁能在单文件HTML里把这个场景还原得最像样。这个想法落地之后就有了今天要聊的这个项目一个基于Three.js的3D版“鹈鹕骑车”测试基准以及用它实测12款大模型的结果。核心关键词就几个GPT-6 Astra、Three.js、HTML、3D、大模型。说白了就是让每个模型生成一份完整的、可独立运行的HTML文件里面用Three.js构建一个3D场景——一只鹈鹕骑着一辆自行车有基本的几何体、材质、光照、动画甚至还要有地面和天空。然后我按照一套固定的评分标准去打分最后看谁排第一。适合谁来读这篇东西如果你是大模型应用开发者想找一个比跑分更直观的评测维度这篇有参考价值。如果你是前端或者Three.js玩家想知道怎么用代码生成的方式做3D场景也能拿走不少实操细节。哪怕你只是好奇“大模型到底能不能写3D代码”看完也能有个具体的判断。我会把整个设计思路、评分标准、每个模型的实测表现、踩过的坑全部摊开来讲。2. 为什么选Three.js和单文件HTML作为测试载体2.1 测试载体的选择逻辑评测大模型的代码能力载体很关键。用Python跑个脚本太容易了模型见过无数遍。用React或者Vue搭个项目依赖太多环境一配就是半小时而且不同模型对构建工具的理解差异太大测出来的结果掺杂了太多非核心因素。我最后锁定的是单文件HTML Three.js原因有这么几条。第一零依赖、零构建。一个HTML文件里面用CDN引入Three.js浏览器打开就能跑。不需要npm install不需要webpack不需要任何本地环境。模型生成什么我就直接看什么中间没有任何转译或打包的干扰。这对评测的公平性至关重要。第二Three.js的API足够复杂能拉开差距。Three.js不是简单的DOM操作它涉及场景图、相机投影、几何体构建、材质系统、光照模型、动画循环。一个模型如果只是“见过”Three.js写出来的代码大概率是几何体位置错乱、相机朝向不对、光照全黑。只有真正理解3D空间关系的模型才能把这些元素正确组合在一起。第三视觉结果一目了然。跑分还要看数字3D场景直接截图就能判断好坏。鹈鹕的嘴是不是朝前、自行车轮子是不是圆的、鹈鹕有没有坐在车座上、地面是不是在轮子下面——这些都是一眼能看出来的。评测的直观性是这套方案最大的优势。第四单文件HTML的约束刚好合适。如果允许模型生成多个文件它可能会把逻辑拆得很散增加评估难度。单文件强制它把所有代码组织在一个结构里既能看出代码组织能力又能保证可复现性。而且HTML本身有!doctype html、html langzh-cn、head、meta charsetutf-8这些标准结构模型对这些基础标签的处理也能反映它的基本功。2.2 Three.js版本与CDN策略在实际操作中我固定使用Three.js的r128版本通过CDN引入。为什么不用最新版因为不同模型对Three.js版本的记忆不一样有些模型训练数据里只有旧版本的API比如Geometry和BufferGeometry的差异、THREE.Geometry在后续版本被移除等。如果我用最新版很多模型会因为API变更而失败但这不代表它3D能力差只是版本没对上。固定一个广泛使用的稳定版本能最大程度减少版本差异带来的噪声。CDN地址我统一用https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js。这个地址稳定、全球可达而且r128的API足够经典模型生成代码时不容易踩到版本坑。实测下来12款模型里有9款能正确引入这个CDN剩下3款要么写错了路径要么用了import语法但没配typemodule这些都会在评分里扣分。提示如果你自己要做类似的评测CDN地址一定要在提示词里写死并且明确告诉模型“使用script标签引入不要用ES module”。否则不同模型的引入方式五花八门评估成本会急剧上升。2.3 评分维度的设计评分不能只看“能不能跑”那样太粗。我设计了一套百分制评分表分成五个维度每个维度20分。这样既能拉开差距又能定位问题。评分维度分值核心考察点代码可运行性20HTML结构完整、CDN引入正确、无语法错误、浏览器无报错场景完整性20鹈鹕、自行车、地面、天空、光照是否齐全空间关系正确性20鹈鹕是否在自行车上、轮子是否着地、相机是否对准场景视觉细节丰富度20材质颜色、几何体细分、阴影、动画、装饰元素代码组织与可读性20变量命名、函数拆分、注释、结构清晰度这套评分表是我反复调整后定下来的。最早的时候我把“动画”单独列了一个维度后来发现很多模型连静态场景都搭不对动画根本无从谈起就把它并入了视觉细节。另外“空间关系正确性”这一项权重给得很高因为这是3D能力的核心——一个模型可以写出很漂亮的代码但如果鹈鹕飘在空中、自行车轮子嵌进地面那它的3D理解就是有问题的。3. 12款大模型实测过程与核心发现3.1 参与测试的模型清单与测试方法这次实测覆盖了12款主流大模型包括GPT-6 Astra、Claude系列、Gemini系列、以及几款国产模型和开源模型。测试方法很直接给每个模型相同的提示词要求它生成一个完整的单文件HTML用Three.js构建3D版“鹈鹕骑车”场景。提示词里明确了技术栈、CDN地址、场景元素、以及“代码必须可直接在浏览器运行”的硬性要求。提示词的核心部分是这样的请生成一个完整的单文件HTML使用Three.jsr128版本通过CDN引入构建一个3D场景。 场景内容一只鹈鹕骑着一辆自行车。 要求 1. 鹈鹕要有身体、脖子、头、喙、翅膀、腿。 2. 自行车要有两个轮子、车架、车把、车座、脚踏板。 3. 鹈鹕坐在车座上翅膀扶着车把脚踩在脚踏板上。 4. 有地面和天空有方向光和环境光。 5. 相机对准场景能看到完整的鹈鹕和自行车。 6. 代码必须可直接运行不要用ES module用script标签引入Three.js。每个模型生成后我在Chrome浏览器里打开截图记录然后按照评分表逐项打分。为了减少偶然性每个模型我跑了三次取平均分。三次之间差异很大的我会单独标注说明这个模型输出不稳定。3.2 GPT-6 Astra的表现为什么它能拿第一GPT-6 Astra是这次测试里综合得分最高的总分87分。它的优势不在于某个单项特别突出而在于没有明显短板。代码可运行性拿了满分20分一次跑通浏览器控制台没有任何报错。场景完整性也是满分鹈鹕的喙、翅膀、腿自行车的轮子、车架、车把、车座、脚踏板一个不少。空间关系拿了18分鹈鹕确实坐在车座上翅膀搭在车把上脚踩在脚踏板附近轮子着地相机角度也合适。让我印象最深的是它的视觉细节。GPT-6 Astra给鹈鹕的喙做了一个渐变的橙色材质给自行车轮子加了辐条地面用了PlaneGeometry并设置了receiveShadow方向光投射出柔和的阴影。它还加了一个简单的动画自行车轮子缓慢旋转鹈鹕的翅膀有轻微的上下摆动。这些细节让整个场景看起来是“活”的而不是一堆静态几何体的堆砌。代码组织方面它把场景初始化、几何体创建、动画循环拆成了三个函数变量命名清晰关键步骤有注释。虽然注释不多但每一处都点在要害上。比如创建鹈鹕身体时它注释了“椭球体模拟身体长轴沿Z轴”这种注释说明模型是真的理解自己在做什么而不是盲目堆代码。实操心得GPT-6 Astra在生成3D代码时倾向于先构建一个“骨架”函数把场景、相机、渲染器初始化好然后再逐个添加物体。这种自顶向下的写法比那些一上来就堆几何体的模型要稳健得多。如果你用GPT-6 Astra写Three.js代码可以放心让它一次生成通常不需要太多修改。3.3 第二梯队的表现与差距分析排在GPT-6 Astra后面的是Claude系列和Gemini系列得分在75到82之间。它们的共同特点是代码能跑场景基本完整但在空间关系或视觉细节上有明显扣分。Claude系列的一个典型问题是鹈鹕的腿和脚踏板对不上。它把鹈鹕的腿画出来了但腿的末端悬在脚踏板上方几厘米的位置没有真正“踩”上去。这说明模型对“接触”这个概念的空间理解还不够精确。另一个问题是自行车的车架用了CylinderGeometry但旋转角度没算对导致车架看起来是歪的。这些细节在2D评测里根本看不出来但在3D场景里一眼就能发现。Gemini系列的优势在于材质和颜色搭配它给鹈鹕的羽毛做了深浅不一的灰色给自行车车架做了金属质感的MeshStandardMaterial。但它的弱项是相机设置有两款Gemini模型生成的场景里相机位置太远鹈鹕和自行车只占画面很小一块需要手动调整camera.position才能看清。这反映出一个问题模型知道要放相机但对“构图”没有概念。国产模型里有几款表现不错得分在70到78之间。它们的代码可运行性普遍很好HTML结构规范!doctype html、html langzh-cn、meta charsetutf-8这些标签一个不落。但在3D空间理解上和头部模型还有差距。比如有一款模型把鹈鹕的喙画成了朝上的圆柱体而不是朝前的锥体导致鹈鹕看起来像在仰头看天。3.4 开源模型与本地部署模型的表现开源模型这次也测了几款包括一些可以在本地部署的版本。它们的得分普遍在55到70之间主要问题集中在代码可运行性和场景完整性上。有一款开源模型生成的代码里Three.js的CDN地址写错了导致THREE对象未定义整个场景全黑。另一款模型用了THREE.Geometry但r128版本里这个类已经废弃控制台报错场景无法渲染。还有一款模型把script标签放在了head里但Three.js的CDN加载是异步的导致脚本执行时THREE还没加载完直接报错。这些问题在头部模型里很少出现说明开源模型在“工程细节”上还需要加强。不过也有亮点有一款开源模型虽然场景简单但代码结构非常清晰每个几何体都封装成了函数注释也很详细。如果只看代码组织它能拿高分但3D效果确实差了一些。注意如果你打算用本地部署的模型做3D代码生成一定要在提示词里明确Three.js的版本和CDN地址并且要求它“不要使用已废弃的API”。否则你会在调试上花掉大量时间。4. 从零复现3D鹈鹕骑车的完整实操流程4.1 场景搭建的核心步骤如果你想自己复现这个测试或者想用Three.js做一个类似的3D场景我把核心步骤拆解一下。整个流程可以分成五步初始化、建地面和天空、建自行车、建鹈鹕、加光照和动画。第一步初始化场景、相机和渲染器。这是所有Three.js项目的起点。场景用THREE.Scene()相机用THREE.PerspectiveCamera()渲染器用THREE.WebGLRenderer()。相机的aspect要设为window.innerWidth / window.innerHeightposition设在(5, 3, 8)左右lookAt(0, 1, 0)这样能俯视整个场景。渲染器的setSize要设为窗口宽高并且把domElement挂到document.body上。const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(5, 3, 8); camera.lookAt(0, 1, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled true; document.body.appendChild(renderer.domElement);第二步建地面和天空。地面用一个PlaneGeometry旋转-Math.PI / 2让它水平材质用MeshStandardMaterial颜色选浅灰或草绿。天空可以用scene.background设一个颜色或者用SphereGeometry做一个天空球。我一般直接用scene.background new THREE.Color(0x87CEEB)简单有效。第三步建自行车。自行车可以拆成几个部分两个轮子、车架、车把、车座、脚踏板。轮子用TorusGeometry车架用CylinderGeometry车座用BoxGeometry。关键是位置和旋转要对。两个轮子分别在x -1和x 1的位置半径0.5管径0.05。车架连接两个轮子的中心可以用几根圆柱体拼成三角形结构。第四步建鹈鹕。鹈鹕的身体用SphereGeometry并缩放成椭球脖子用CylinderGeometry头用SphereGeometry喙用ConeGeometry翅膀用BoxGeometry或SphereGeometry压扁腿用细CylinderGeometry。鹈鹕要放在车座上方翅膀伸向车把脚踩在脚踏板上。这一步最考验空间想象力建议先在纸上画个草图确定各个部位的坐标。第五步加光照和动画。光照用DirectionalLight和AmbientLight组合。方向光从斜上方照下来开启castShadow让地面接收阴影。环境光调低一点避免场景太亮失去层次。动画可以用requestAnimationFrame在循环里旋转轮子、摆动翅膀然后调用renderer.render(scene, camera)。4.2 关键参数的计算与选择3D场景里参数不是随便填的每一个都有依据。我拿几个关键参数举例。相机位置的计算。场景的中心大概在(0, 1, 0)鹈鹕和自行车的总高度约2米宽度约3米。要让整个场景进入视野相机距离中心大约8到10米。用PerspectiveCamera的fov 45垂直视野角度45度在距离8米时垂直可见范围是2 * 8 * tan(22.5°) ≈ 6.6米足够覆盖2米高的场景。水平范围按宽高比换算16:9的屏幕下约11.7米也够用。所以camera.position.set(5, 3, 8)是一个合理的起点。轮子半径与车架高度的关系。轮子半径0.5米直径1米。车架要连接两个轮子的中心中心高度就是0.5米。车座的高度一般在轮子中心上方0.5到0.7米所以车座在y 1.0到1.2之间。鹈鹕坐在车座上身体中心大约在y 1.5。这些数字不是拍脑袋来的而是按照真实自行车的比例缩放的。光照强度的选择。DirectionalLight的强度设为1.0到1.5AmbientLight的强度设为0.3到0.5。方向光太强阴影会太硬环境光太强场景会失去立体感。我实测下来方向光1.2、环境光0.4是一个比较平衡的组合。方向光的位置设在(5, 10, 7)从右上方照下来阴影投在左下方视觉上比较自然。提示Three.js的DirectionalLight默认不投射阴影需要设置castShadow true并且设置shadow.camera的范围。如果阴影范围太小阴影会被裁切范围太大阴影会模糊。一般设shadow.camera.left -10、right 10、top 10、bottom -10覆盖整个场景。4.3 鹈鹕几何体的精细调整鹈鹕是场景的主角它的几何体需要精细调整。我用的是组合法身体一个椭球脖子一个圆柱头一个球喙一个锥体翅膀两个压扁的球腿两根细圆柱。身体用SphereGeometry(0.5, 32, 32)然后scale.set(1.2, 0.8, 0.9)变成一个横向的椭球。位置在(0, 1.5, 0)。脖子用CylinderGeometry(0.12, 0.15, 0.6, 16)稍微倾斜连接身体和头。头用SphereGeometry(0.2, 32, 32)位置在(0.3, 2.0, 0)。喙用ConeGeometry(0.08, 0.4, 16)旋转让它朝前位置在(0.5, 1.95, 0)。翅膀用SphereGeometry(0.3, 32, 32)scale.set(0.3, 0.8, 0.5)位置在身体两侧稍微向前伸模拟扶车把的姿势。腿用CylinderGeometry(0.04, 0.04, 0.5, 8)从身体下方伸到脚踏板。这些参数是我反复调整后定下来的。最早的时候喙的角度不对鹈鹕看起来像在仰头。后来把ConeGeometry的旋转角度从Math.PI / 2改成Math.PI / 2 0.2喙就朝前下方了看起来自然很多。翅膀的位置也很关键太靠后像收着翅膀太靠前像要飞起来最后定在身体前方0.2米的位置刚好搭在车把上。4.4 动画循环与性能优化动画部分我加了两个简单的效果轮子旋转和翅膀摆动。轮子旋转很简单在动画循环里让轮子的rotation.x或rotation.z随时间增加。翅膀摆动用Math.sin函数让翅膀的rotation.z在正负0.1弧度之间来回摆动。function animate() { requestAnimationFrame(animate); const time Date.now() * 0.001; wheelFront.rotation.z time * 2; wheelBack.rotation.z time * 2; wingLeft.rotation.z Math.sin(time * 3) * 0.1; wingRight.rotation.z -Math.sin(time * 3) * 0.1; renderer.render(scene, camera); } animate();性能方面Three.js在浏览器里跑主要瓶颈是几何体数量和阴影计算。我这个场景的几何体数量在50个左右对现代浏览器来说毫无压力。但如果模型生成的代码里用了高细分的SphereGeometry(1, 128, 128)或者开了多个阴影光源帧率就会下降。实测中有一款模型给每个几何体都开了castShadow和receiveShadow导致阴影计算量翻倍帧率掉到30以下。后来我手动关掉了一些不必要的阴影帧率就恢复正常了。实操心得在评测大模型生成的Three.js代码时帧率也是一个隐性指标。有些模型代码能跑但帧率很低说明它对性能没有概念。你可以用Chrome的Performance面板录一段看看帧率曲线。如果稳定在60帧说明代码质量不错如果掉到30帧以下就要检查是不是几何体细分太高或者阴影开太多了。5. 常见问题与排查技巧实录5.1 模型生成代码的典型问题速查表在实测12款模型的过程中我整理了一份常见问题速查表。这些问题覆盖了从HTML结构到Three.js API的各个层面基本上你拿任何一个模型生成的3D代码都能在这张表里找到对应的排查方向。问题现象可能原因排查方法解决思路页面全黑控制台报THREE is not definedCDN引入失败或路径错误检查script标签的src是否正确换用稳定的CDN地址确保网络可达页面全黑控制台报THREE.Geometry is not a constructor使用了已废弃的API搜索代码里的THREE.Geometry替换为THREE.BufferGeometry或具体几何体类场景能渲染但物体位置错乱坐标计算错误或旋转角度不对逐个检查物体的position和rotation用AxesHelper辅助定位逐步调整鹈鹕飘在空中没坐在车座上鹈鹕和自行车的Y坐标不匹配对比鹈鹕身体和车座的position.y调整鹈鹕的Y坐标使其与车座接触轮子嵌进地面轮子中心Y坐标小于轮子半径检查轮子的position.y和半径将轮子中心Y设为半径值使其刚好着地相机看不到场景相机位置太远或朝向错误检查camera.position和lookAt把相机拉近确保lookAt指向场景中心阴影不显示未开启castShadow或receiveShadow检查光源和物体的阴影属性开启方向光的castShadow地面设receiveShadow帧率过低几何体细分太高或阴影计算量大用Performance面板录制帧率降低几何体细分减少阴影光源数量动画不生效动画循环未调用或属性未更新检查requestAnimationFrame和属性赋值确保动画循环在渲染前更新物体属性HTML结构不完整缺少!doctype html或meta charset检查HTML头部补全标准HTML5结构这张表是我踩了无数坑之后总结出来的。最典型的是“页面全黑”这一类问题十有八九是CDN引入失败或者API废弃。有一次我拿到一份代码THREE对象死活加载不出来查了半天发现模型把CDN地址写成了https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.js少了个.min但那个路径下确实有文件只是加载慢导致脚本执行时THREE还没定义。后来我统一要求模型用three.min.js问题就少了。5.2 空间关系错误的排查方法空间关系错误是3D场景里最难排查的一类问题。2D代码里位置错了顶多是元素重叠或偏移但在3D里一个坐标错了整个场景的透视关系就全乱了。我常用的排查方法是加辅助线。在场景里加一个THREE.AxesHelper(5)会在原点画出红绿蓝三根轴分别代表X、Y、Z方向。再给关键物体加THREE.BoxHelper把它的包围盒画出来。这样你就能直观地看到每个物体在空间中的位置和大小。比如鹈鹕的包围盒如果悬在自行车包围盒的上方说明Y坐标太高了如果两个包围盒交叉说明位置有重叠。另一个方法是打印坐标。在控制台里输出关键物体的position和rotation和预期值对比。比如车座的position.y是1.0鹈鹕身体的position.y是1.5但鹈鹕身体的半径是0.5那么鹈鹕身体的底部在1.0刚好和车座接触。如果鹈鹕身体的position.y是2.0底部就在1.5悬空了0.5米一眼就能看出来。注意排查空间关系时先把动画停掉让场景静止。动画运行的时候物体的位置一直在变很难判断静态位置是否正确。等静态位置调好了再开动画。5.3 模型输出不稳定的应对策略同一个模型同一个提示词跑三次可能得到三个不同的结果。这种不稳定性在3D代码生成里特别明显。有一次我测一款模型第一次生成的代码能跑第二次生成的代码里自行车轮子变成了方形第三次生成的代码直接报错。这种波动性说明模型对3D空间的理解还不够稳定。应对策略有几个。第一多次采样取平均。每个模型跑三次取平均分这样能减少偶然性。如果三次之间差异很大就在报告里单独标注说明这个模型输出不稳定。第二固定随机种子。如果模型支持设置temperature或seed尽量固定下来减少随机性。第三细化提示词。提示词越具体模型输出的方差越小。比如把“鹈鹕坐在车座上”改成“鹈鹕的身体底部与车座顶部接触鹈鹕身体中心Y坐标比车座Y坐标高0.5”模型就更容易生成一致的结果。实测下来GPT-6 Astra的输出稳定性最好三次生成的代码结构基本一致只有细节上的微小差异。开源模型的波动性最大三次里可能有一次完全跑不通。这也从侧面反映了模型能力的差距——稳定的模型不仅知道怎么做而且每次都能做对。6. 评测之外的思考3D代码生成还能怎么用这套3D鹈鹕骑车的测试最初只是出于好玩但做下来发现它的价值不止于评测。它其实是一个很好的3D代码生成能力试金石。传统的代码评测看的是逻辑正确性但3D场景评测看的是空间理解、几何直觉、视觉审美这些是更接近“智能”的东西。我在实际操作中的体会是一个模型如果能在单文件HTML里正确构建出鹈鹕骑车那它在其他3D任务上的表现通常也不会差。比如让它生成一个3D房间布局、一个简单的产品展示页、或者一个数据可视化场景它都能迁移过去。反过来如果模型连鹈鹕和自行车的位置关系都搞不定那它在更复杂的3D任务上大概率也会翻车。这个测试还可以继续扩展。比如加一个“多物体交互”的维度让鹈鹕骑车经过一棵树或者让自行车在坡道上行驶。再比如加一个“物理模拟”的维度让轮子根据速度旋转让鹈鹕的身体随着颠簸上下起伏。这些扩展会让评测更全面但也更复杂。我目前的做法是先保持简单把基础场景的评测做扎实等模型整体水平上来了再增加难度。最后分享一个小技巧如果你也想用大模型生成Three.js代码提示词里一定要写清楚“使用r128版本”和“用script标签引入”。这两个约束能帮你避开80%的兼容性问题。另外生成之后先在浏览器里跑一遍打开控制台看有没有报错再用AxesHelper检查空间关系。这套流程走下来基本上能快速判断一份3D代码的质量。
