GPT-5.2:从“会聊”升级到“能交付”
GPT-5.2 有没有吹,就看大家后面的实测了。反正 Sam 毫不掩饰夸:它是目前世界上最智能的通用模型,尤其擅长处理现实世界的知识工作任务。
OpenAI 这次发布 GPT-5.2[1],核心诉求很直接:不追求“更会聊天”,而是更像能交付成果的同事,面向专业知识工作与长时运行的代理(agentic)任务。它重点强化的是产出型能力:表格、演示、代码、视觉理解、长上下文与工具链流程——也就是那些最容易卡在“最后一公里”的活。
官方叙事可以压缩成一句:GPT-5.2 更擅长把复杂任务从头做到尾,减少中途断链。为了证明这一点,它把重点放在 GDPval[2] 上:一个更贴近真实职场交付物的评测,覆盖 44 个职业,要求模型交付可直接使用的产物(artifact),比如销售演示、会计表格、排班表、流程图甚至短视频,而不是只给“看起来像正确答案”的文字。
GDPval(Gross Domestic Product value benchmark)是 Tejal Patwardhan 等人提出的评测基准,用来衡量模型在真实、具经济价值任务上的交付能力。它覆盖美国劳工统计局(BLS)的工作活动,横跨 44 个职业与对美国 GDP 贡献最大的 9 个行业部门,任务来自平均从业 14 年的行业专家代表性工作,核心评价维度是“可交付成果质量”。论文还指出,提高推理强度(reasoning effort)、提供更多任务上下文(task context)与更强的脚手架(scaffolding)能提升表现,并开源 220 个任务的黄金子集(gdpval datasets[3])及公开自动评分服务。
在 GDPval 上,GPT-5.2 的说法很“硬”:GPT-5.2 Thinking 被描述为首次达到人类专家水位,在与行业专业人士的对比里有 70.9% 的条目“赢或打平”,Pro 更高为 74.1%。它还给出一笔“经济账”:在历史口径下,完成同类任务的速度可达专家 11 倍以上、成本不到 1%,并强调这更适合“人工监督+模型出活”的协作方式(实际速度在 ChatGPT 中可能波动)。
额,blog 中还有大量基准与分项指标,这里就不截图了(我可能对测评有点去魅了,具体还是看实测表现吧)。在价格方面,也是略有提升。
特点
GPT-5.2 知识截止日期为 2025 年 8 月 31 日。这一点意义重大——GPT 5.1 和 5 的截止日期都是 2024 年 9 月 30 日,而 GPT-5 mini 的截止日期是 2024 年 5 月 31 日。
GPT-5.2 Thinking 评估也是出奇的高,再次领先!
抛开分数,核心功能性特征可以朴素地理解成五件事:更会产出成品、更能处理超长材料、更能看懂图和界面、更擅长把工具链跑通、更适合用不同档位来平衡速度和质量。
- “成品交付物(artifact)”能力更强。它不是只写一段看起来对的文字,而是更像能给你一份可用的表格、一个能演示的 PPT、一段能跑的前端页面。你能明显感受到它更在乎结构、排版、格式、引用和一致性——这些都是现实工作里最耗人的部分。代价也很现实:生成复杂成品会更慢,有时要等几分钟,适合拿来做“第一版交付物”,再人工修。
- 长材料处理更稳。这里的提升不是“能塞更长”,而是“能在很长的上下文里不乱串、不丢关键点”。对你这种经常要分析文档、代码仓库、会议转录、多文件项目的人来说,实际收益是:你可以把“完整材料”交给它做梳理、对照、抽取、归纳,最后输出一个结构化结论,而不是必须自己先手工切块、做摘要、拼来拼去还担心遗漏。
- 视觉理解更可用,尤其是“图表 + 软件界面”。它在 blog 里说得很工程:看图不是玄学,但通常需要配合工具(比如 Python)才能跑到理想效果。落到实践,你可以更放心地让它读仪表盘截图、产品 UI 截图、技术图、报告里的图表,然后让它做解释、找异常、给操作路径、甚至按元素位置来描述界面组件。对做自动化/Agent 的产品尤其关键:它更像能“看懂你屏幕上发生了什么”。
- 工具调用(tool calling)更像“流程控制器”。以前模型常见问题是:能想、能说,但一进多步骤就容易丢状态、漏步骤、卡在中间。这里强调的是“长链不掉线”:能分解任务、按顺序调用工具、读回结果、再继续下一步,最后给你一个能交差的闭环输出。你如果在做 AI 类浏览器/代理系统,这点意味着:更适合用它当“总控大脑”,把检索、抓取、解析、计算、生成、校验串起来。
- 它更像有一个“速度/质量旋钮”。blog 反复提推理强度(reasoning effort)以及在低推理档位下也更能打,这对产品落地很重要:你可以把任务按成本分层——日常查询、路由、轻量总结用快档;要做规划、复杂推导、长链工具、产出成品时再切高档。这样用户体验才不会变成“每件事都等很久”,同时关键任务又能顶住。
测评
GPT-5.2 除了能力全面提升外,似乎在 3D 场景构建方面也尤为出色。
我自己编写了一个 prompt 同时测试 GPT-5.2 Thinking、Gemini Thinking 3 Pro、DeepSeek 深度推理,让它们输出单个 HTML。先说结论(以下案例运行在 Mac M1 Pro (16G)):三者在一次性交付结果方面都出现或多或少的问题,需要经过几次修复才能功能。我个人感觉呈现效果 GPT-5.2 最佳,细节处理丰富。
以下是 prompt,也欢迎大家自测:
做一个可直接打开运行的单文件 HTML(three.js CDN),呈现“桌面上一个透明六边形玻璃托盘里,多颗自发光的彩色弹珠在里面永不停歇地滚动互撞”。
视觉氛围:夜晚或暗室感。弹珠本身是柔和的自发光(像荧光玻璃/能量球),颜色五颜六色但不刺眼,带柔光晕染(bloom),并且会把周围玻璃托盘与桌面轻微“染色”反射出来。托盘仍是厚玻璃,能看到折射、高光、边缘亮线;桌面有少量环境反射与微弱噪点纹理,让画面像真实拍摄而不是纯 CG。
运动体验:默认 20~40 颗弹珠,彼此会咔哒碰撞、靠墙反弹、互相挤压后散开,长期运行不减速、不穿墙、不抖动卡边、不突然爆飞;整体手感像“装满会发光弹珠的玻璃托盘”。碰撞要有轻微“软”感:不是硬邦邦弹球,更像带一点柔性的能量球(视觉上可通过光晕轻微压缩/拉伸来暗示,但几何不要真变形过头)。
交互以体验为主(不要暴露一堆参数):页面角落放一个极简控制条
- “摇一摇”:像轻晃托盘一样给系统注入随机扰动,让弹珠重新洗牌
- “慢动作”:0.25x / 1x / 2x
- “相机”:桌面视角 / 俯视 / 轻微环绕
- “灯光感觉”:柔光强度(bloom)、色彩饱和度、拖影/余辉(可选)
隐藏开发者开关(按 D):显示/隐藏调试叠加(六边形边界线、速度箭头、碰撞点),用来证明这是“真实碰撞模拟”,默认关闭以保持观感。
目标:用户第一眼觉得“这是一盘会发光的彩色弹珠在玻璃托盘里不停碰撞”,画面干净、柔和、真实、有质感。
GPT-5.2
首次给出的代码在静态资源引用方面出现错误(此问题几乎必现,我几次测试都出现了),在修复该问题后,又出现了一次代码逻辑错误。第三次给出完整代码运行通过。
以下是效果展示,从体验上来说,页面未出现明显卡顿,操作基本流畅,3D 效果也最接近设想(一定要看视频,效果惊艳):
源代码:
<!doctype html>
<htmllang="zh">
<head>
<metacharset="utf-8" />
<metaname="viewport"content="width=device-width,initial-scale=1" />
<title>Glow Marbles in Glass Hex Tray</title>
<style>
html, body { margin:0; padding:0; height:100%; background:#05060a; overflow:hidden; }
canvas { display:block; }
.hud {
position: fixed;
left: 14px;
bottom: 14px;
display: flex;
gap: 10px;
align-items: center;
padding: 10px12px;
border-radius: 14px;
background: rgba(10,12,18,0.55);
border: 1px solid rgba(255,255,255,0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: rgba(255,255,255,0.85);
font: 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC","Hiragino Sans GB","Noto Sans CJK SC", sans-serif;
user-select: none;
}
.hudbutton, .hud select {
appearance: none;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.88);
padding: 7px10px;
border-radius: 10px;
cursor: pointer;
outline: none;
}
.hudbutton:hover, .hud select:hover { background: rgba(255,255,255,0.10); }
.hudbutton:active { transform: translateY(1px); }
.hud.group { display:flex; align-items:center; gap:8px; }
.hud.label { opacity: 0.75; }
.hudinput[type="range"] { width: 120px; accent-color: rgba(255,255,255,0.75); }
.hint {
position: fixed;
right: 14px;
bottom: 14px;
padding: 8px10px;
border-radius: 12px;
background: rgba(10,12,18,0.38);
border: 1px solid rgba(255,255,255,0.06);
color: rgba(255,255,255,0.55);
font: 11px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
user-select:none;
}
.hintkbd{
font: 11px/1.1 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
padding: 2px6px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.70);
}
</style>
<!-- ✅ 关键:解决 examples/jsm 内部 `from "three"` 的裸模块导入 -->
<scriptasyncsrc="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
<scripttype="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
</head>
<body>
<divclass="hud"id="hud">
<buttonid="shakeBtn"title="像轻晃托盘一样扰动弹珠">摇一摇</button>
<divclass="group">
<spanclass="label">慢动作</span>
<selectid="timeScale">
<optionvalue="0.25">0.25x</option>
<optionvalue="1"selected>1x</option>
<optionvalue="2">2x</option>
</select>
</div>
<divclass="group">
<spanclass="label">相机</span>
<selectid="camMode">
<optionvalue="table"selected>桌面视角</option>
<optionvalue="top">俯视</option>
<optionvalue="orbit">轻微环绕</option>
</select>
</div>
<divclass="group">
<spanclass="label">灯光感觉</span>
<spanclass="label"title="Bloom 强度">柔光</span>
<inputid="bloom"type="range"min="0"max="2.2"step="0.01"value="1.15"/>
<spanclass="label"title="色彩饱和度">饱和</span>
<inputid="sat"type="range"min="0.6"max="1.6"step="0.01"value="1.15"/>
<spanclass="label"title="拖影/余辉">余辉</span>
<inputid="trail"type="range"min="0"max="1"step="0.01"value="0.22"/>
</div>
</div>
<divclass="hint">按 <kbd>D</kbd> 显示/隐藏调试叠加</div>
<scripttype="module">
import * asTHREEfrom"three";
import { RoomEnvironment } from"three/addons/environments/RoomEnvironment.js";
import { EffectComposer } from"three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from"three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from"three/addons/postprocessing/UnrealBloomPass.js";
import { AfterimagePass } from"three/addons/postprocessing/AfterimagePass.js";
import { ShaderPass } from"three/addons/postprocessing/ShaderPass.js";
/* ----------------------------- Scene setup ----------------------------- */
const renderer = newTHREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.physicallyCorrectLights = true;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = newTHREE.Scene();
scene.background = newTHREE.Color(0x05060a);
// Soft room environment for believable reflections
const pmrem = newTHREE.PMREMGenerator(renderer);
const envTex = pmrem.fromScene(newRoomEnvironment(renderer), 0.04).texture;
scene.environment = envTex;
// Camera
const camera = newTHREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.02, 20);
const camTarget = newTHREE.Vector3(0, 0.08, 0);
let camMode = "table";
functionapplyCameraMode(mode, immediate=false) {
camMode = mode;
if (mode === "table") {
const p = newTHREE.Vector3(1.05, 0.68, 1.15);
if (immediate) camera.position.copy(p);
camera.userData._goalPos = p;
camera.userData._goalFov = 42;
} elseif (mode === "top") {
const p = newTHREE.Vector3(0.001, 1.50, 0.001);
if (immediate) camera.position.copy(p);
camera.userData._goalPos = p;
camera.userData._goalFov = 50;
} else { // orbit
const p = newTHREE.Vector3(1.10, 0.72, 1.05);
if (immediate) camera.position.copy(p);
camera.userData._goalPos = p;
camera.userData._goalFov = 42;
}
}
applyCameraMode("table", true);
/* ------------------------------- Lighting ------------------------------ */
const hemi = newTHREE.HemisphereLight(0x0b1022, 0x030407, 0.25);
scene.add(hemi);
const key = newTHREE.SpotLight(0xbfd3ff, 2.2, 6, Math.PI * 0.22, 0.8, 1.0);
key.position.set(1.5, 1.8, 1.0);
key.target.position.set(0, 0.1, 0);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024);
key.shadow.bias = -0.0002;
scene.add(key, key.target);
const rim = newTHREE.DirectionalLight(0x8b7bff, 0.18);
rim.position.set(-1.2, 1.0, -1.0);
scene.add(rim);
// “Color bleeding” proxy lights (few point lights updated from marbles)
const bleedLights = [];
constBLEED_COUNT = 6;
for (let i = 0; i < BLEED_COUNT; i++) {
const L = newTHREE.PointLight(0xffffff, 0.35, 1.6, 2);
bleedLights.push(L);
scene.add(L);
}
/* ------------------------------- Table -------------------------------- */
functionmakeTableTexture(size=512) {
const c = document.createElement("canvas");
c.width = c.height = size;
const ctx = c.getContext("2d");
ctx.fillStyle = "#0a0c12";
ctx.fillRect(0, 0, size, size);
const img = ctx.getImageData(0, 0, size, size);
for (let i = 0; i < img.data.length; i += 4) {
const n = (Math.random() * 2 - 1);
const v = 18 + n * 10;
img.data[i] = v;
img.data[i+1] = v;
img.data[i+2] = v + 2;
img.data[i+3] = 255;
}
ctx.putImageData(img, 0, 0);
ctx.globalAlpha = 0.10;
ctx.strokeStyle = "rgba(255,255,255,0.22)";
for (let k = 0; k < 250; k++) {
const x = Math.random() * size;
const y = Math.random() * size;
const len = 20 + Math.random() * 80;
const ang = (Math.random() * Math.PI) * 0.8 + 0.2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(ang) * len, y + Math.sin(ang) * len);
ctx.stroke();
}
ctx.globalAlpha = 1;
const tex = newTHREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(2.2, 2.2);
tex.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
return tex;
}
const tableTex = makeTableTexture(512);
const table = newTHREE.Mesh(
newTHREE.PlaneGeometry(8, 8),
newTHREE.MeshStandardMaterial({
map: tableTex,
roughness: 0.92,
metalness: 0.08,
envMapIntensity: 0.55
})
);
table.rotation.x = -Math.PI / 2;
table.receiveShadow = true;
scene.add(table);
/* ----------------------------- Hex glass tray -------------------------- */
// Inner region for physics (marbles roll inside this hex)
constINNER_APOTHEM = 0.58; // distance center->side (physics boundary)
constWALL_THICKNESS = 0.07; // glass wall thickness (visual)
constTRAY_FLOOR_T = 0.045;
constTRAY_WALL_H = 0.14;
functionhexVertices(apothem) {
const R = apothem / Math.cos(Math.PI / 6); // vertex radius
const verts = [];
for (let i = 0; i < 6; i++) {
const a = (Math.PI / 3) * i + Math.PI / 6;
verts.push(newTHREE.Vector2(Math.cos(a) * R, Math.sin(a) * R)); // (x,z) as (x,y)
}
return verts;
}
functionshapeFromVerts(verts) {
const s = newTHREE.Shape();
s.moveTo(verts[0].x, verts[0].y);
for (let i = 1; i < verts.length; i++) s.lineTo(verts[i].x, verts[i].y);
s.closePath();
return s;
}
const outerVerts = hexVertices(INNER_APOTHEM + WALL_THICKNESS);
const innerVerts = hexVertices(INNER_APOTHEM);
const outerShape = shapeFromVerts(outerVerts);
const holePath = newTHREE.Path();
holePath.moveTo(innerVerts[0].x, innerVerts[0].y);
for (let i = 1; i < innerVerts.length; i++) holePath.lineTo(innerVerts[i].x, innerVerts[i].y);
holePath.closePath();
outerShape.holes.push(holePath);
const glassMat = newTHREE.MeshPhysicalMaterial({
color: newTHREE.Color(0x0c1222),
transmission: 1.0,
thickness: 0.22,
ior: 1.52,
roughness: 0.06,
metalness: 0.0,
specularIntensity: 1.0,
specularColor: newTHREE.Color(0xffffff),
envMapIntensity: 1.25,
transparent: true,
opacity: 1.0,
clearcoat: 0.2,
clearcoatRoughness: 0.15
});
const wallGeo = newTHREE.ExtrudeGeometry(outerShape, {
depth: TRAY_WALL_H,
bevelEnabled: true,
bevelSegments: 2,
steps: 1,
bevelSize: 0.014,
bevelThickness: 0.012
});
wallGeo.rotateX(-Math.PI / 2);
wallGeo.translate(0, TRAY_FLOOR_T, 0);
const trayWalls = newTHREE.Mesh(wallGeo, glassMat);
trayWalls.castShadow = true;
trayWalls.receiveShadow = true;
scene.add(trayWalls);
const innerShape = shapeFromVerts(innerVerts);
const floorGeo = newTHREE.ExtrudeGeometry(innerShape, {
depth: TRAY_FLOOR_T,
bevelEnabled: true,
bevelSegments: 1,
steps: 1,
bevelSize: 0.010,
bevelThickness: 0.010
});
floorGeo.rotateX(-Math.PI / 2);
const trayFloor = newTHREE.Mesh(floorGeo, glassMat);
trayFloor.castShadow = true;
trayFloor.receiveShadow = true;
scene.add(trayFloor);
const edgeLine = newTHREE.LineLoop(
newTHREE.BufferGeometry().setFromPoints(outerVerts.map(v =>newTHREE.Vector3(v.x, TRAY_FLOOR_T + TRAY_WALL_H + 0.001, v.y))),
newTHREE.LineBasicMaterial({ color: 0x9fb6ff, transparent:true, opacity: 0.10 })
);
scene.add(edgeLine);
/* --------------------------- Hex boundary math -------------------------- */
// ✅ 必须在 randInHex / balls 初始化之前创建 planes
const hex2 = hexVertices(INNER_APOTHEM); // Vector2(x,z)
const planes = [];
for (let i = 0; i < 6; i++) {
const a = hex2[i];
const b = hex2[(i + 1) % 6];
const ex = b.x - a.x;
const ez = b.y - a.y;
// outward normal for CCW edge: (ez, -ex)
let nx = ez, nz = -ex;
const invLen = 1 / Math.hypot(nx, nz);
nx *= invLen; nz *= invLen;
planes.push({ ax: a.x, az: a.y, nx, nz });
}
functionpointInsideHex(x, z, margin=0) {
for (const pl of planes) {
const d = pl.nx * (x - pl.ax) + pl.nz * (z - pl.az);
if (d > -margin) returnfalse;
}
returntrue;
}
/* ------------------------------ Post FX -------------------------------- */
const composer = newEffectComposer(renderer);
composer.addPass(newRenderPass(scene, camera));
const bloomPass = newUnrealBloomPass(
newTHREE.Vector2(window.innerWidth, window.innerHeight),
1.15, // strength
0.7, // radius
0.25// threshold
);
composer.addPass(bloomPass);
const afterPass = newAfterimagePass();
afterPass.uniforms["damp"].value = 0.88;
composer.addPass(afterPass);
constSaturationShader = {
uniforms: { tDiffuse: { value: null }, saturation: { value: 1.15 } },
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float saturation;
varying vec2 vUv;
void main() {
vec4 c = texture2D(tDiffuse, vUv);
float l = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));
vec3 gray = vec3(l);
c.rgb = mix(gray, c.rgb, saturation);
gl_FragColor = c;
}
`
};
const satPass = newShaderPass(SaturationShader);
composer.addPass(satPass);
/* ------------------------------ Marbles -------------------------------- */
constBALL_COUNT = 32; // 20~40
const R = 0.038; // radius
constY0 = TRAY_FLOOR_T + R + 0.002; // sit on floor
functionmakeHaloTexture(size=256) {
const c = document.createElement("canvas");
c.width = c.height = size;
const ctx = c.getContext("2d");
const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
g.addColorStop(0.00, "rgba(255,255,255,0.95)");
g.addColorStop(0.18, "rgba(255,255,255,0.55)");
g.addColorStop(0.45, "rgba(255,255,255,0.18)");
g.addColorStop(1.00, "rgba(255,255,255,0.00)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
const tex = newTHREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
return tex;
}
const haloTex = makeHaloTexture(256);
functionrandInHex() {
constRv = INNER_APOTHEM / Math.cos(Math.PI/6);
for (let tries = 0; tries < 2000; tries++) {
const x = (Math.random() * 2 - 1) * Rv * 0.95;
const z = (Math.random() * 2 - 1) * Rv * 0.95;
if (pointInsideHex(x, z, R)) return { x, z };
}
return { x: 0, z: 0 };
}
functionniceGlowColor() {
const h = Math.random();
const s = 0.70 + Math.random()*0.15;
const l = 0.52 + Math.random()*0.10;
returnnewTHREE.Color().setHSL(h, s, l);
}
const ballGeo = newTHREE.SphereGeometry(R, 32, 20);
const balls = [];
for (let i = 0; i < BALL_COUNT; i++) {
const c = niceGlowColor();
const mat = newTHREE.MeshPhysicalMaterial({
color: newTHREE.Color(0x101225),
transmission: 0.22,
thickness: 0.12,
ior: 1.45,
roughness: 0.08,
metalness: 0.0,
envMapIntensity: 0.85,
emissive: c.clone(),
emissiveIntensity: 2.3
});
const m = newTHREE.Mesh(ballGeo, mat);
m.castShadow = true;
const haloMat = newTHREE.SpriteMaterial({
map: haloTex,
color: c.clone(),
transparent: true,
opacity: 0.22,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const halo = newTHREE.Sprite(haloMat);
halo.scale.setScalar(R * 7.0);
const p = randInHex();
m.position.set(0, Y0, 0);
halo.position.set(0, Y0, 0);
const ang = Math.random() * Math.PI * 2;
const sp = 0.55 + Math.random() * 0.35;
const v = newTHREE.Vector2(Math.cos(ang) * sp, Math.sin(ang) * sp);
const g = newTHREE.Group();
g.position.set(p.x, 0, p.z);
g.add(m);
g.add(halo);
scene.add(g);
balls.push({ group: g, mesh: m, halo, col: c, p: newTHREE.Vector2(p.x, p.z), v, pulse: 0 });
}
/* --------------------------- Physics: walls & balls --------------------- */
functionresolveWall(ball, debugHit=false) {
let bestPen = 0;
let best = null;
for (const pl of planes) {
const d = pl.nx * (ball.p.x - pl.ax) + pl.nz * (ball.p.y - pl.az);
const pen = d + R; // circle radius constraint
if (pen > bestPen) { bestPen = pen; best = pl; }
}
if (best && bestPen > 0) {
ball.p.x -= best.nx * bestPen;
ball.p.y -= best.nz * bestPen;
const vn = ball.v.x * best.nx + ball.v.y * best.nz;
if (vn > 0) {
ball.v.x -= 2 * vn * best.nx;
ball.v.y -= 2 * vn * best.nz;
if (debugHit) addCollisionPoint(ball.p.x, Y0, ball.p.y);
ball.pulse = Math.min(1, ball.pulse + Math.min(0.35, vn * 0.18 + bestPen * 1.8));
}
}
}
functionresolveBalls(debugHit=false) {
const e = 0.98; // slightly soft
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const A = balls[i], B = balls[j];
const dx = B.p.x - A.p.x;
const dz = B.p.y - A.p.y;
const d2 = dx*dx + dz*dz;
const minD = 2*R;
if (d2 < (minD*minD) && d2 > 1e-10) {
const d = Math.sqrt(d2);
const nx = dx / d;
const nz = dz / d;
const pen = (minD - d);
const corr = pen * 0.52;
A.p.x -= nx * corr; A.p.y -= nz * corr;
B.p.x += nx * corr; B.p.y += nz * corr;
const rvx = B.v.x - A.v.x;
const rvz = B.v.y - A.v.y;
const velN = rvx * nx + rvz * nz;
if (velN < 0) {
const jimp = -(1 + e) * velN / 2;
const ix = jimp * nx;
const iz = jimp * nz;
A.v.x -= ix; A.v.y -= iz;
B.v.x += ix; B.v.y += iz;
const impulseMag = Math.abs(jimp);
A.pulse = Math.min(1, A.pulse + Math.min(0.35, impulseMag * 0.12 + pen * 2.5));
B.pulse = Math.min(1, B.pulse + Math.min(0.35, impulseMag * 0.12 + pen * 2.5));
if (debugHit) addCollisionPoint((A.p.x+B.p.x)*0.5, Y0, (A.p.y+B.p.y)*0.5);
}
}
}
}
}
/* ------------------------------ Debug ---------------------------------- */
let debug = false;
const debugGroup = newTHREE.Group();
scene.add(debugGroup);
const dbgHexLine = newTHREE.LineLoop(
newTHREE.BufferGeometry().setFromPoints(hex2.map(v =>newTHREE.Vector3(v.x, Y0 + 0.001, v.y))),
newTHREE.LineBasicMaterial({ color: 0x66aaff, transparent:true, opacity: 0.65 })
);
debugGroup.add(dbgHexLine);
const arrows = balls.map(() => {
const a = newTHREE.ArrowHelper(newTHREE.Vector3(1,0,0), newTHREE.Vector3(0, Y0, 0), 0.15, 0xffcc66, 0.05, 0.03);
a.cone.material.transparent = true;
a.line.material.transparent = true;
a.cone.material.opacity = 0.8;
a.line.material.opacity = 0.55;
debugGroup.add(a);
return a;
});
constHIT_MAX = 36;
const hitGeo = newTHREE.SphereGeometry(0.008, 10, 8);
const hitMat = newTHREE.MeshBasicMaterial({ color: 0xffffff, transparent:true, opacity: 0.85 });
const hitInst = newTHREE.InstancedMesh(hitGeo, hitMat, HIT_MAX);
hitInst.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
debugGroup.add(hitInst);
let hitIdx = 0;
const _m4 = newTHREE.Matrix4();
functionaddCollisionPoint(x,y,z) {
_m4.makeTranslation(x, y + 0.004, z);
hitInst.setMatrixAt(hitIdx % HIT_MAX, _m4);
hitInst.instanceMatrix.needsUpdate = true;
hitIdx++;
}
functionsetDebug(on) {
debug = on;
debugGroup.visible = !!on;
}
setDebug(false);
window.addEventListener("keydown", (e) => {
if (e.key === "d" || e.key === "D") setDebug(!debug);
});
/* ------------------------------- UI ------------------------------------ */
const shakeBtn = document.getElementById("shakeBtn");
const timeScaleSel = document.getElementById("timeScale");
const camModeSel = document.getElementById("camMode");
const bloomSlider = document.getElementById("bloom");
const satSlider = document.getElementById("sat");
const trailSlider = document.getElementById("trail");
let timeScale = 1.0;
timeScaleSel.addEventListener("change", () => { timeScale = parseFloat(timeScaleSel.value || "1"); });
camModeSel.addEventListener("change", () =>applyCameraMode(camModeSel.value, false));
bloomSlider.addEventListener("input", () => { bloomPass.strength = parseFloat(bloomSlider.value); });
satSlider.addEventListener("input", () => { satPass.uniforms.saturation.value = parseFloat(satSlider.value); });
trailSlider.addEventListener("input", () => {
const t = parseFloat(trailSlider.value);
afterPass.uniforms["damp"].value = 0.65 + t * 0.28;
afterPass.enabled = (t > 0.001);
});
bloomPass.strength = parseFloat(bloomSlider.value);
satPass.uniforms.saturation.value = parseFloat(satSlider.value);
trailSlider.dispatchEvent(newEvent("input"));
/* -------------------------- “Shake” dynamics --------------------------- */
let shakeT = 0;
const shakeForce = newTHREE.Vector2(0,0);
shakeBtn.addEventListener("click", () => {
shakeT = 0.7;
const a = Math.random() * Math.PI * 2;
const mag = 2.2 + Math.random() * 1.0;
shakeForce.set(Math.cos(a) * mag, Math.sin(a) * mag);
for (const b of balls) {
b.v.x += (Math.random()*2-1) * 0.35;
b.v.y += (Math.random()*2-1) * 0.35;
b.pulse = Math.min(1, b.pulse + 0.2);
}
});
/* ------------------------- Energy stabilization ------------------------- */
functioncomputeK() {
let k = 0;
for (const b of balls) k += (b.v.x*b.v.x + b.v.y*b.v.y);
return k / balls.length;
}
let targetK = computeK();
functiongentleEnergyCorrect() {
const k = computeK();
if (k < 1e-6) return;
const ratio = Math.sqrt(targetK / k);
const blend = 0.025;
const s = 1 + (ratio - 1) * blend;
for (const b of balls) b.v.multiplyScalar(s);
}
/* ------------------------ Bleed lights update -------------------------- */
for (let i = 0; i < BLEED_COUNT; i++) {
const v = outerVerts[i];
bleedLights[i].position.set(v.x * 0.85, 0.22, v.y * 0.85);
}
functionupdateBleedLights() {
for (let i = 0; i < BLEED_COUNT; i++) {
const L = bleedLights[i];
const lp = L.position;
let r=0,g=0,b=0,wSum=0;
for (const ball of balls) {
const dx = ball.p.x - lp.x;
const dz = ball.p.y - lp.z;
const d2 = dx*dx + dz*dz + 0.05;
const w = 1.0 / d2;
r += ball.col.r * w;
g += ball.col.g * w;
b += ball.col.b * w;
wSum += w;
}
if (wSum > 0) {
r/=wSum; g/=wSum; b/=wSum;
L.color.setRGB(r, g, b);
L.intensity = 0.26;
}
}
}
/* ------------------------------- Loop ---------------------------------- */
const clock = newTHREE.Clock();
let accumulator = 0;
constFIXED_DT = 1/120;
constSOLVER_ITERS = 2;
let orbitPhase = 0;
functiontick() {
requestAnimationFrame(tick);
const dt = Math.min(0.05, clock.getDelta());
accumulator += dt * timeScale;
if (camera.userData._goalPos) {
camera.position.lerp(camera.userData._goalPos, 0.06);
camera.fov += (camera.userData._goalFov - camera.fov) * 0.06;
camera.updateProjectionMatrix();
}
if (camMode === "orbit") {
orbitPhase += dt * 0.18;
const r = 1.25;
const y = 0.70;
const x = Math.cos(orbitPhase) * r;
const z = Math.sin(orbitPhase) * r;
camera.position.lerp(newTHREE.Vector3(x, y, z), 0.03);
}
camera.lookAt(camTarget);
while (accumulator >= FIXED_DT) {
accumulator -= FIXED_DT;
if (shakeT > 0) {
shakeT -= FIXED_DT;
const decay = Math.exp(-FIXED_DT * 3.2);
shakeForce.multiplyScalar(decay);
} else {
shakeForce.set(0,0);
}
for (const b of balls) {
b.v.x += shakeForce.x * FIXED_DT;
b.v.y += shakeForce.y * FIXED_DT;
b.v.x += (Math.random()*2-1) * 0.0007;
b.v.y += (Math.random()*2-1) * 0.0007;
b.p.x += b.v.x * FIXED_DT;
b.p.y += b.v.y * FIXED_DT;
}
for (let it = 0; it < SOLVER_ITERS; it++) {
for (const b of balls) resolveWall(b, debug);
resolveBalls(debug);
for (const b of balls) resolveWall(b, false);
}
gentleEnergyCorrect();
}
for (let i = 0; i < balls.length; i++) {
const b = balls[i];
b.pulse = Math.max(0, b.pulse - dt * 1.7);
b.group.position.set(b.p.x, 0, b.p.y);
const glow = 0.22 + b.pulse * 0.38;
b.halo.material.opacity = glow;
const haloScale = (R * 7.0) * (1 + b.pulse * 0.55);
b.halo.scale.setScalar(haloScale);
const meshScale = 1 + b.pulse * 0.05;
b.mesh.scale.setScalar(meshScale);
b.mesh.material.emissiveIntensity = 2.1 + b.pulse * 1.2;
if (debug) {
const a = arrows[i];
const speed = Math.hypot(b.v.x, b.v.y);
const dir = newTHREE.Vector3(b.v.x, 0, b.v.y).normalize();
a.position.set(b.p.x, Y0 + 0.01, b.p.y);
a.setDirection(dir);
a.setLength(THREE.MathUtils.clamp(speed * 0.22, 0.05, 0.22), 0.045, 0.028);
}
}
if ((performance.now() | 0) % 3 === 0) updateBleedLights();
composer.render();
}
/* ------------------------------ Resize --------------------------------- */
functiononResize() {
const w = window.innerWidth, h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
composer.setSize(w, h);
bloomPass.setSize(w, h);
}
window.addEventListener("resize", onResize);
tick();
</script>
</body>
</html>
Gemini 3 Pro
输出的代码进行了一次修正,第二次运行成功。
以下视频是运行效果,整体上来说,基本符合预期,但被 GPT-5.2 惊艳到了,Gemini 显得稍微平庸了一点。页面操作也很流畅,无明显卡顿。
源代码:
<!DOCTYPE html>
<htmllang="zh">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>夜光弹珠 - 永动碰撞</title>
<style>
body { margin: 0; overflow: hidden; background-color: #050505; font-family: 'Segoe UI', sans-serif; user-select: none; }
#canvas-container { width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; }
/* UI Control Bar */
#ui-bar {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
z-index: 10; display: flex; gap: 15px;
background: rgba(20, 20, 30, 0.6);
backdrop-filter: blur(10px);
padding: 10px20px; border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 010px30pxrgba(0,0,0,0.5);
color: rgba(255, 255, 255, 0.9);
font-size: 14px; align-items: center;
}
.control-group { display: flex; align-items: center; gap: 8px; border-right: 1px solid rgba(255,255,255,0.1); padding-right: 15px; }
.control-group:last-child { border-right: none; padding-right: 0; }
button {
background: rgba(255, 255, 255, 0.1); border: none; color: white;
padding: 6px12px; border-radius: 6px; cursor: pointer; transition: 0.2s;
font-size: 12px; letter-spacing: 0.5px;
}
button:hover { background: rgba(255, 255, 255, 0.25); transform: translateY(-1px); }
button:active { transform: translateY(1px); }
button.active { background: #4dabf7; color: #000; font-weight: bold; box-shadow: 0010pxrgba(77, 171, 247, 0.5); }
label { font-size: 12px; color: #aaa; margin-right: 5px; }
input[type="range"] { width: 80px; accent-color: #4dabf7; cursor: pointer; }
#debug-info {
position: absolute; top: 20px; left: 20px; z-index: 10;
color: #0f0; font-family: monospace; font-size: 12px;
background: rgba(0,0,0,0.8); padding: 10px; border-radius: 4px;
display: none; pointer-events: none;
}
.loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: monospace; z-index: 20; }
</style>
<scripttype="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
</head>
<body>
<divclass="loading"id="loader">初始化光学实验室...</div>
<divid="debug-info">DEBUG MODE ON<br>FPS: <spanid="fps">0</span><br>Objects: <spanid="obj-count">0</span></div>
<divid="ui-bar">
<divclass="control-group">
<buttonid="btn-shake">👋 摇一摇</button>
</div>
<divclass="control-group">
<label>速度</label>
<buttonclass="speed-btn"data-speed="0.25">0.25x</button>
<buttonclass="speed-btn active"data-speed="1">1x</button>
<buttonclass="speed-btn"data-speed="2">2x</button>
</div>
<divclass="control-group">
<label>视角</label>
<buttonid="cam-table"class="active">桌面</button>
<buttonid="cam-top">俯视</button>
<buttonid="cam-rotate">环绕</button>
</div>
<divclass="control-group">
<label>柔光</label>
<inputtype="range"id="bloom-slider"min="0"max="2"step="0.1"value="0.8">
</div>
</div>
<divid="canvas-container"></div>
<scripttype="module">
import * asTHREEfrom'three';
import { EffectComposer } from'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from'three/addons/postprocessing/UnrealBloomPass.js';
// --- Configuration ---
constCONFIG = {
ballCount: 30,
ballRadius: 0.6,
trayRadius: 12,
wallHeight: 2.5,
wallThickness: 0.8,
friction: 0.0, // Frictionless for perpetual motion
restitution: 0.95, // Bounciness (slightly less than 1 to allow logic to inject energy)
minSpeed: 8.0, // Minimum speed to maintain perpetual feel
maxSpeed: 15.0,
colors: [0xff0055, 0x00ffaa, 0x00ccff, 0xffaa00, 0xaa00ff, 0xffffff]
};
// --- Globals ---
let scene, camera, renderer, composer, clock;
let marbles = []; // Array to store marble data { mesh, velocity, mass, etc. }
let walls = []; // Mathematical planes for collision
let timeScale = 1.0;
let isDevMode = false;
let debugLines = newTHREE.Group();
let cameraMode = 'table'; // table, top, rotate
let shakeImpulse = 0;
// --- Initialization ---
functioninit() {
const container = document.getElementById('canvas-container');
document.getElementById('loader').style.display = 'none';
// 1. Scene & Camera
scene = newTHREE.Scene();
scene.fog = newTHREE.FogExp2(0x050505, 0.02);
camera = newTHREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
// 2. Renderer
renderer = newTHREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" }); // AA handled by composer
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ReinhardToneMapping;
container.appendChild(renderer.domElement);
// 3. Post Processing (Bloom)
const renderScene = newRenderPass(scene, camera);
const bloomPass = newUnrealBloomPass(newTHREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.threshold = 0.1;
bloomPass.strength = 0.8; // Controlled by UI
bloomPass.radius = 0.5;
composer = newEffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// 4. Lighting (Subtle, mostly from emission)
const ambientLight = newTHREE.AmbientLight(0x222222);
scene.add(ambientLight);
// Simulating a desk lamp off-screen
const dirLight = newTHREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(-10, 20, -10);
scene.add(dirLight);
// Rim light for glass edges
const rimLight = newTHREE.PointLight(0x4444ff, 1, 50);
rimLight.position.set(0, 10, 0);
scene.add(rimLight);
// 5. Build Environment
createTable();
createTray();
createMarbles();
// Debug setup
scene.add(debugLines);
// updateDebugOverlay(); // REMOVED: This function was undefined and unnecessary
// 6. Events
window.addEventListener('resize', onWindowResize);
document.addEventListener('keydown', (e) => {
if(e.key.toLowerCase() === 'd') toggleDebug();
});
// Start Loop
clock = newTHREE.Clock();
animate();
setupUI(bloomPass);
}
// --- Scene Objects ---
functioncreateTable() {
// Procedural Noise Texture for Table
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#080808';
ctx.fillRect(0,0,512,512);
for(let i=0; i<50000; i++) {
ctx.fillStyle = `rgba(255,255,255, ${Math.random() * 0.05})`;
ctx.fillRect(Math.random()*512, Math.random()*512, 2, 2);
}
const texture = newTHREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
const planeGeo = newTHREE.PlaneGeometry(100, 100);
const planeMat = newTHREE.MeshStandardMaterial({
map: texture,
roughness: 0.6,
metalness: 0.2,
color: 0x111111
});
const table = newTHREE.Mesh(planeGeo, planeMat);
table.rotation.x = -Math.PI / 2;
table.position.y = -0.05; // Slightly below tray bottom
table.receiveShadow = true;
scene.add(table);
}
functioncreateTray() {
// Hexagon Shape
const shape = newTHREE.Shape();
const r = CONFIG.trayRadius;
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2;
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r;
if (i === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
// Define math planes for collision (Normal points INWARD)
const normal = newTHREE.Vector3(-Math.cos(angle), 0, -Math.sin(angle));
// Distance from origin to wall is slightly less than r (apothem)
// Apothem of hexagon = r * cos(30deg) = r * 0.866
const dist = r * Math.cos(Math.PI/6);
walls.push({ normal: normal, constant: dist, p1: newTHREE.Vector2(x,y) }); // simplified
}
shape.closePath();
// Glass Material
const glassMat = newTHREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0.1,
roughness: 0.05,
transmission: 1.0, // Glass
thickness: 1.5,
ior: 1.5,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.3
});
// 1. Bottom
const bottomGeo = newTHREE.ShapeGeometry(shape);
const bottom = newTHREE.Mesh(bottomGeo, glassMat);
bottom.rotation.x = -Math.PI / 2;
scene.add(bottom);
// 2. Walls (Extruded Frame)
// Create a hole shape for extrusion
const holePath = newTHREE.Path();
const t = CONFIG.wallThickness;
const r2 = r + t;
const outerShape = newTHREE.Shape();
// Outer Hexagon
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2;
const x = Math.cos(angle) * r2;
const y = Math.sin(angle) * r2;
if (i === 0) outerShape.moveTo(x, y);
else outerShape.lineTo(x, y);
}
outerShape.closePath();
outerShape.holes.push(shape); // Add inner hexagon as hole
const wallGeo = newTHREE.ExtrudeGeometry(outerShape, {
depth: CONFIG.wallHeight,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.1,
bevelSegments: 2
});
const wallMesh = newTHREE.Mesh(wallGeo, glassMat);
wallMesh.rotation.x = -Math.PI / 2;
scene.add(wallMesh);
// Add faint edge lines for style
const edges = newTHREE.EdgesGeometry(wallGeo);
const line = newTHREE.LineSegments(edges, newTHREE.LineBasicMaterial( { color: 0x88ccff, transparent: true, opacity: 0.2 } ) );
line.rotation.x = -Math.PI / 2;
scene.add(line);
}
functioncreateMarbles() {
const geometry = newTHREE.SphereGeometry(CONFIG.ballRadius, 32, 32);
for (let i = 0; i < CONFIG.ballCount; i++) {
const color = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
const material = newTHREE.MeshStandardMaterial({
color: 0x000000, // Base color dark
emissive: color,
emissiveIntensity: 2.0, // High for bloom
roughness: 0.2,
metalness: 0.8
});
const mesh = newTHREE.Mesh(geometry, material);
// Random position inside hexagon
const r = (CONFIG.trayRadius - 2) * Math.sqrt(Math.random());
const theta = Math.random() * 2 * Math.PI;
mesh.position.set(r * Math.cos(theta), CONFIG.ballRadius, r * Math.sin(theta));
// Random velocity
const vAngle = Math.random() * Math.PI * 2;
const speed = CONFIG.minSpeed + Math.random() * 5;
const velocity = newTHREE.Vector3(Math.cos(vAngle) * speed, 0, Math.sin(vAngle) * speed);
scene.add(mesh);
marbles.push({
mesh: mesh,
velocity: velocity,
radius: CONFIG.ballRadius,
mass: 1,
id: i,
originalColor: newTHREE.Color(color)
});
}
document.getElementById('obj-count').innerText = marbles.length;
}
// --- Physics Engine (Custom) ---
functionupdatePhysics(dt) {
// Sub-stepping for stability
const steps = 8;
const subDt = dt / steps;
for (let step = 0; step < steps; step++) {
// 1. Move & Wall Collision
marbles.forEach(m => {
// Apply shake impulse if active
if (shakeImpulse > 0) {
m.velocity.x += (Math.random() - 0.5) * shakeImpulse;
m.velocity.z += (Math.random() - 0.5) * shakeImpulse;
}
// Move
m.mesh.position.addScaledVector(m.velocity, subDt);
// Check Hexagon Walls (Simplified Plane check)
// Hexagon walls are at distance 'apothem' from center.
// Apothem ~ trayRadius * 0.866.
const distFromCenter = Math.sqrt(m.mesh.position.x**2 + m.mesh.position.z**2);
const wallDistLimit = (CONFIG.trayRadius * Math.cos(Math.PI/6)) - m.radius;
if (distFromCenter > wallDistLimit * 0.8) { // Optimization: only check detailed math near edge
// Accurate check: Dot product with all 6 normals
// Normal of a side i: (cos(a), sin(a)) where a = 30, 90, 150...
for(let i=0; i<6; i++) {
const angle = (i * 60 + 30) * (Math.PI / 180);
const nx = Math.cos(angle);
const nz = Math.sin(angle);
// Distance along normal
const d = m.mesh.position.x * nx + m.mesh.position.z * nz;
// Wall position along normal is apothem
const limit = CONFIG.trayRadius * Math.cos(Math.PI/6);
// If penetration
if (d > limit - m.radius) {
// Position Correction
const overlap = d - (limit - m.radius);
m.mesh.position.x -= nx * overlap;
m.mesh.position.z -= nz * overlap;
// Velocity Reflection: V = V - 2(V.N)N
const vDotN = m.velocity.x * nx + m.velocity.z * nz;
if (vDotN > 0) { // Only reflect if moving towards wall
m.velocity.x -= 2 * vDotN * nx;
m.velocity.z -= 2 * vDotN * nz;
// Wall Bounciness
m.velocity.multiplyScalar(CONFIG.restitution);
}
}
}
}
});
// 2. Marble-Marble Collision
for (let i = 0; i < marbles.length; i++) {
for (let j = i + 1; j < marbles.length; j++) {
const m1 = marbles[i];
const m2 = marbles[j];
const dx = m2.mesh.position.x - m1.mesh.position.x;
const dz = m2.mesh.position.z - m1.mesh.position.z;
const distSq = dx*dx + dz*dz;
const radSum = m1.radius + m2.radius;
if (distSq < radSum * radSum) {
const dist = Math.sqrt(distSq);
// Normal vector
const nx = dx / dist;
const nz = dz / dist;
// Position Correction (avoid sticking)
const overlap = 0.5 * (radSum - dist);
m1.mesh.position.x -= nx * overlap;
m1.mesh.position.z -= nz * overlap;
m2.mesh.position.x += nx * overlap;
m2.mesh.position.z += nz * overlap;
// Velocity resolution
// v1' = v1 - 2*m2/(m1+m2) * dot(v1-v2, n) * n
// Assuming equal mass = 1
const v1x = m1.velocity.x;
const v1z = m1.velocity.z;
const v2x = m2.velocity.x;
const v2z = m2.velocity.z;
const vRelX = v1x - v2x;
const vRelZ = v1z - v2z;
const vRelDotN = vRelX * nx + vRelZ * nz;
if (vRelDotN > 0) {
const impulse = vRelDotN; // Simplified for equal mass
m1.velocity.x -= impulse * nx;
m1.velocity.z -= impulse * nz;
m2.velocity.x += impulse * nx;
m2.velocity.z += impulse * nz;
}
}
}
}
} // End sub-steps
// 3. Energy Injection (Perpetual Motion)
if (shakeImpulse > 0) shakeImpulse *= 0.9; // Dampen shake
marbles.forEach(m => {
const speedSq = m.velocity.lengthSq();
// If too slow, boost gently
if (speedSq < CONFIG.minSpeed * CONFIG.minSpeed) {
m.velocity.multiplyScalar(1.01);
}
// Cap max speed (to prevent explosion)
if (speedSq > CONFIG.maxSpeed * CONFIG.maxSpeed) {
m.velocity.multiplyScalar(0.98);
}
// Visual Pulse based on impact/speed (Subtle)
// We fake this: spin creates visual variation
m.mesh.rotation.x += m.velocity.z * dt * 0.5;
m.mesh.rotation.z -= m.velocity.x * dt * 0.5;
});
}
// --- Camera Logic ---
functionupdateCamera() {
const time = Date.now() * 0.0005;
const target = newTHREE.Vector3(0, 0, 0);
if (cameraMode === 'table') {
// Low angle, slight drift
camera.position.set(0 + Math.sin(time*0.5)*2, 10, 25);
camera.lookAt(0, 0, 0);
} elseif (cameraMode === 'top') {
camera.position.set(0, 35, 0);
camera.lookAt(0, 0, 0);
camera.rotation.z = time * 0.05; // Slow spin
} elseif (cameraMode === 'rotate') {
const r = 28;
camera.position.x = Math.cos(time) * r;
camera.position.z = Math.sin(time) * r;
camera.position.y = 15;
camera.lookAt(0, 0, 0);
}
}
// --- Render Loop ---
functionanimate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1) * timeScale;
updatePhysics(dt);
updateCamera();
if(isDevMode) drawDebug();
document.getElementById('fps').innerText = Math.round(1 / (dt/timeScale || 1));
composer.render();
}
// --- Debug Helpers ---
functiondrawDebug() {
debugLines.clear();
const material = newTHREE.LineBasicMaterial({ color: 0xffff00 });
// Draw velocity vectors
const points = [];
marbles.forEach(m => {
points.push(m.mesh.position.clone());
points.push(m.mesh.position.clone().add(m.velocity.clone().multiplyScalar(0.2)));
});
const geometry = newTHREE.BufferGeometry().setFromPoints(points);
debugLines.add(newTHREE.LineSegments(geometry, material));
}
functiontoggleDebug() {
isDevMode = !isDevMode;
document.getElementById('debug-info').style.display = isDevMode ? 'block' : 'none';
if(!isDevMode) debugLines.clear();
}
functiononWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
// --- UI Logic ---
functionsetupUI(bloomPass) {
// Shake
document.getElementById('btn-shake').onclick = () => {
shakeImpulse = 20; // Big kick
};
// Speed
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.onclick = (e) => {
document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
timeScale = parseFloat(e.target.getAttribute('data-speed'));
};
});
// Camera
const camBtns = {
'cam-table': 'table',
'cam-top': 'top',
'cam-rotate': 'rotate'
};
Object.keys(camBtns).forEach(id => {
document.getElementById(id).onclick = () => {
document.querySelectorAll('#ui-bar button').forEach(b => {
if(camBtns[b.id]) b.classList.remove('active');
});
document.getElementById(id).classList.add('active');
cameraMode = camBtns[id];
};
});
// Bloom
document.getElementById('bloom-slider').oninput = (e) => {
bloomPass.strength = parseFloat(e.target.value);
};
}
init();
</script>
</body>
</html>
DeepSeek
也出现了两次逻辑错误,第三次输出的代码可以运行。
以下是视频演示,实现效果似乎有点问题,球没怎么运动,按钮点击也会出现明显长延时卡顿。页面几乎卡死,鼠标移动艰难。
源代码:
<!DOCTYPE html>
<htmllang="zh-CN">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>发光弹珠玻璃托盘模拟器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
background-color: #0a0a12;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #e0e0ff;
overflow: hidden;
height: 100vh;
width: 100vw;
position: relative;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
canvas {
display: block;
outline: none;
}
#controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
background: rgba(10, 10, 25, 0.7);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 16px;
width: 200px;
box-shadow: 08px32pxrgba(0, 0, 0, 0.3);
border: 1px solid rgba(100, 120, 255, 0.1);
transition: opacity 0.3s ease;
}
.control-group {
margin-bottom: 16px;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-title {
font-size: 13px;
margin-bottom: 8px;
color: #a0a8ff;
font-weight: 500;
letter-spacing: 0.5px;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.control-row:last-child {
margin-bottom: 0;
}
.control-btn {
flex: 1;
background: rgba(30, 40, 120, 0.4);
border: 1px solid rgba(80, 100, 255, 0.2);
color: #d0d8ff;
padding: 8px12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
text-align: center;
transition: all 0.2s ease;
}
.control-btn:hover {
background: rgba(40, 60, 180, 0.5);
border-color: rgba(100, 140, 255, 0.4);
}
.control-btn.active {
background: rgba(60, 100, 255, 0.5);
border-color: rgba(120, 160, 255, 0.7);
box-shadow: 0012pxrgba(80, 120, 255, 0.4);
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
.slider-value {
font-size: 12px;
min-width: 36px;
text-align: right;
color: #b0b8ff;
}
.slider {
flex: 1;
-webkit-appearance: none;
height: 4px;
background: rgba(30, 40, 120, 0.4);
border-radius: 2px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #7d8cff;
cursor: pointer;
box-shadow: 008pxrgba(125, 140, 255, 0.8);
}
#info {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
font-size: 13px;
background: rgba(10, 10, 25, 0.5);
backdrop-filter: blur(5px);
padding: 10px15px;
border-radius: 8px;
border: 1px solid rgba(100, 120, 255, 0.1);
max-width: 300px;
line-height: 1.5;
}
#infoh3 {
margin-bottom: 6px;
color: #a0a8ff;
font-size: 14px;
}
#infop {
margin-bottom: 4px;
opacity: 0.8;
}
#debug-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 50;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.debug-line {
stroke: #ff5555;
stroke-width: 1;
fill: none;
}
.debug-arrow {
stroke: #55ff55;
stroke-width: 2;
fill: none;
marker-end: url(#arrowhead);
}
.debug-point {
fill: #ffff55;
stroke: #ffff55;
stroke-width: 1;
}
#loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a12;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
transition: opacity 0.5s ease;
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(100, 120, 255, 0.3);
border-top: 3px solid #7d8cff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#loading-text {
font-size: 16px;
color: #a0a8ff;
margin-top: 10px;
}
#marbles-count {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(10, 10, 25, 0.7);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 10px15px;
font-size: 14px;
border: 1px solid rgba(100, 120, 255, 0.1);
}
.glow-text {
text-shadow: 0010pxrgba(125, 140, 255, 0.7);
}
.hidden {
display: none;
}
#error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(20, 0, 0, 0.9);
padding: 20px;
border-radius: 10px;
border: 2px solid #ff5555;
max-width: 500px;
z-index: 1001;
display: none;
}
</style>
</head>
<body>
<divid="loading">
<divclass="spinner"></div>
<divid="loading-text">加载物理模拟与渲染引擎...</div>
</div>
<divid="error"style="display:none;">
<h3>加载错误</h3>
<pid="error-message"></p>
<buttononclick="location.reload()">重新加载</button>
</div>
<divid="canvas-container"></div>
<divid="controls">
<divclass="control-group">
<divclass="control-title">动作控制</div>
<divclass="control-row">
<divid="shake-btn"class="control-btn">摇一摇</div>
<divid="reset-btn"class="control-btn">重置</div>
</div>
</div>
<divclass="control-group">
<divclass="control-title">时间速度</div>
<divclass="control-row">
<divid="speed-0.25"class="control-btn">0.25x</div>
<divid="speed-1"class="control-btn active">1x</div>
<divid="speed-2"class="control-btn">2x</div>
</div>
</div>
<divclass="control-group">
<divclass="control-title">相机视角</div>
<divclass="control-row">
<divid="cam-desktop"class="control-btn active">桌面</div>
<divid="cam-top"class="control-btn">俯视</div>
<divid="cam-orbit"class="control-btn">环绕</div>
</div>
</div>
<divclass="control-group">
<divclass="control-title">视觉效果</div>
<divclass="slider-container">
<span>柔光</span>
<inputid="bloom-slider"type="range"class="slider"min="0"max="2"step="0.1"value="1">
<divid="bloom-value"class="slider-value">1.0</div>
</div>
<divclass="slider-container">
<span>饱和度</span>
<inputid="saturation-slider"type="range"class="slider"min="0"max="2"step="0.1"value="1">
<divid="saturation-value"class="slider-value">1.0</div>
</div>
<divclass="slider-container">
<span>拖影</span>
<inputid="motionblur-slider"type="range"class="slider"min="0"max="1"step="0.1"value="0">
<divid="motionblur-value"class="slider-value">0.0</div>
</div>
</div>
</div>
<divid="marbles-count">
<spanclass="glow-text">弹珠: <spanid="marble-count">25</span></span>
</div>
<divid="info">
<h3>发光弹珠玻璃托盘</h3>
<p>• 弹珠自发荧光并照亮周围环境</p>
<p>• 真实物理碰撞与反射折射</p>
<p>• 按 <spanstyle="color:#7d8cff">D</span> 显示/隐藏调试信息</p>
<p>• 托盘为透明六边形玻璃材质</p>
</div>
<svgid="debug-overlay"xmlns="http://www.w3.org/2000/svg">
<defs>
<markerid="arrowhead"markerWidth="10"markerHeight="7"refX="9"refY="3.5"orient="auto">
<polygonpoints="0 0, 10 3.5, 0 7"fill="#55ff55"/>
</marker>
</defs>
</svg>
<!-- Three.js 核心库 -->
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<!-- 先加载所有需要的着色器 -->
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/CopyShader.js"></script>
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/LuminosityHighPassShader.js"></script>
<!-- FilmShader 用于胶片颗粒效果 -->
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/FilmShader.js"></script>
<!-- 后期处理 -->
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/EffectComposer.js"></script>
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/RenderPass.js"></script>
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/ShaderPass.js"></script>
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/UnrealBloomPass.js"></script>
<!-- 控制器 -->
<scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script>
// 等待DOM加载
document.addEventListener('DOMContentLoaded', function() {
// 主要Three.js变量
let scene, camera, renderer, composer;
let bloomPass, filmPass;
let controls;
let clock = newTHREE.Clock();
// 物理模拟变量
let marbles = [];
let marbleMeshes = [];
let tray, trayMesh;
let walls = [];
let wallMeshes = [];
// 调试变量
let debugMode = false;
let debugOverlay = document.getElementById('debug-overlay');
// 场景设置
const initialMarbleCount = 25;
let marbleCount = initialMarbleCount;
let timeScale = 1.0;
let currentCameraMode = 'desktop';
let cameraOrbitAngle = 0;
// 饱和度控制变量
let saturationValue = 1.0;
// 材质和颜色
const marbleColors = [
0xff5555, 0x55ff55, 0x5555ff, 0xffff55, 0xff55ff, 0x55ffff,
0xff8855, 0x88ff55, 0x5588ff, 0xffff88, 0xff88ff, 0x88ffff,
0xffaa00, 0xaa00ff, 0x00aaff, 0xaaff00
];
// 自定义饱和度着色器(如果Three.js的SaturationShader不可用)
constCustomSaturationShader = {
uniforms: {
"tDiffuse": { value: null },
"saturation": { value: 1.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float saturation;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
vec3 luma = vec3( 0.299, 0.587, 0.114 );
float v = dot( texel.xyz, luma );
gl_FragColor = vec4( mix( vec3( v ), texel.xyz, saturation ), texel.w );
}
`
};
// 检查Three.js是否加载完全
functioncheckThreeJsLoaded() {
if (typeofTHREE === 'undefined') {
document.getElementById('loading-text').textContent = '等待Three.js加载...';
setTimeout(checkThreeJsLoaded, 100);
return;
}
// 检查关键组件是否已加载
if (typeofTHREE.EffectComposer === 'undefined' ||
typeofTHREE.UnrealBloomPass === 'undefined') {
document.getElementById('loading-text').textContent = '等待后期处理库加载...';
setTimeout(checkThreeJsLoaded, 100);
return;
}
// Three.js已加载完成,开始初始化
init();
}
// 显示错误
functionshowError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error-message').textContent = message;
document.getElementById('error').style.display = 'block';
}
// 初始化场景
functioninit() {
try {
// 创建场景
scene = newTHREE.Scene();
scene.background = newTHREE.Color(0x050510);
scene.fog = newTHREE.Fog(0x050510, 15, 30);
// 创建相机
camera = newTHREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 12);
// 创建渲染器
renderer = newTHREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.8;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 创建后期处理
composer = newTHREE.EffectComposer(renderer);
const renderPass = newTHREE.RenderPass(scene, camera);
composer.addPass(renderPass);
// 发光效果 - 使用更稳定的参数
bloomPass = newTHREE.UnrealBloomPass(
newTHREE.Vector2(window.innerWidth, window.innerHeight),
1.0, 0.4, 0.85
);
composer.addPass(bloomPass);
// 胶片颗粒效果(代替拖影)
filmPass = newTHREE.ShaderPass(THREE.FilmShader);
filmPass.uniforms['nIntensity'].value = 0.05;
filmPass.uniforms['sIntensity'].value = 0.0; // 开始时没有拖影
filmPass.uniforms['grayscale'].value = 0;
composer.addPass(filmPass);
// 复制着色器(最终输出)
const copyPass = newTHREE.ShaderPass(THREE.CopyShader);
copyPass.renderToScreen = true;
composer.addPass(copyPass);
// 添加轨道控制(用于环绕视角)
controls = newTHREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enableZoom = true;
controls.enablePan = false;
controls.minDistance = 5;
controls.maxDistance = 30;
controls.maxPolarAngle = Math.PI / 2 - 0.1;
controls.enabled = false; // 默认禁用
// 添加灯光
setupLights();
// 创建玻璃托盘和桌面
createTray();
createTable();
// 创建弹珠
createMarbles();
// 添加窗口大小调整监听
window.addEventListener('resize', onWindowResize);
// 添加键盘监听
document.addEventListener('keydown', onKeyDown);
// 设置UI交互
setupUI();
// 隐藏加载界面
setTimeout(() => {
document.getElementById('loading').style.opacity = '0';
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 500);
}, 1000);
// 开始动画循环
animate();
} catch (error) {
console.error("初始化错误:", error);
showError('初始化错误: ' + error.message);
}
}
// 设置灯光
functionsetupLights() {
// 环境光
const ambientLight = newTHREE.AmbientLight(0x222244, 0.2);
scene.add(ambientLight);
// 顶部柔光
const topLight = newTHREE.DirectionalLight(0x4488ff, 0.3);
topLight.position.set(0, 10, 0);
topLight.castShadow = true;
topLight.shadow.mapSize.width = 1024;
topLight.shadow.mapSize.height = 1024;
topLight.shadow.camera.near = 0.5;
topLight.shadow.camera.far = 50;
scene.add(topLight);
// 侧边补光
const sideLight1 = newTHREE.DirectionalLight(0xff8844, 0.2);
sideLight1.position.set(10, 5, 10);
scene.add(sideLight1);
const sideLight2 = newTHREE.DirectionalLight(0x44ff88, 0.2);
sideLight2.position.set(-10, 3, -10);
scene.add(sideLight2);
// 添加一些点光源来增强弹珠的自发光效果
for (let i = 0; i < 8; i++) {
const pointLight = newTHREE.PointLight(0xffffff, 0.5, 10);
pointLight.position.set(
(Math.random() - 0.5) * 10,
2,
(Math.random() - 0.5) * 10
);
scene.add(pointLight);
}
}
// 创建玻璃托盘
functioncreateTray() {
// 六边形几何体
const hexRadius = 6;
const hexHeight = 1.5;
const wallThickness = 0.1;
// 创建六边形底部
const hexagonShape = newTHREE.Shape();
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const x = Math.cos(angle) * hexRadius;
const y = Math.sin(angle) * hexRadius;
if (i === 0) {
hexagonShape.moveTo(x, y);
} else {
hexagonShape.lineTo(x, y);
}
}
hexagonShape.closePath();
const extrudeSettings = {
depth: hexHeight,
bevelEnabled: false
};
const geometry = newTHREE.ExtrudeGeometry(hexagonShape, extrudeSettings);
// 玻璃材质 - 简化版本确保兼容性
const glassMaterial = newTHREE.MeshPhysicalMaterial({
color: 0xffffff,
roughness: 0.05,
metalness: 0,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
envMapIntensity: 1
});
// 检查是否有 transmission 属性(某些版本可能不支持)
if ('transmission'in glassMaterial) {
glassMaterial.transmission = 0.95;
glassMaterial.thickness = 0.5;
}
trayMesh = newTHREE.Mesh(geometry, glassMaterial);
trayMesh.position.y = hexHeight / 2;
trayMesh.castShadow = true;
trayMesh.receiveShadow = true;
scene.add(trayMesh);
// 创建六边形墙壁
const wallHeight = 1.2;
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const nextAngle = ((i + 1) * Math.PI) / 3;
const x1 = Math.cos(angle) * hexRadius;
const z1 = Math.sin(angle) * hexRadius;
const x2 = Math.cos(nextAngle) * hexRadius;
const z2 = Math.sin(nextAngle) * hexRadius;
// 墙壁位置(中点)
const wallX = (x1 + x2) / 2;
const wallZ = (z1 + z2) / 2;
// 墙壁长度
const wallLength = Math.sqrt((x2 - x1) ** 2 + (z2 - z1) ** 2);
// 墙壁角度
const wallAngle = Math.atan2(z2 - z1, x2 - x1);
// 墙壁几何体
const wallGeometry = newTHREE.BoxGeometry(wallLength, wallHeight, wallThickness);
const wall = newTHREE.Mesh(wallGeometry, glassMaterial);
wall.position.set(wallX, wallHeight / 2 + hexHeight, wallZ);
wall.rotation.y = wallAngle + Math.PI / 2;
wall.castShadow = true;
wall.receiveShadow = true;
scene.add(wall);
wallMeshes.push(wall);
walls.push({
mesh: wall,
p1: { x: x1, z: z1 },
p2: { x: x2, z: z2 },
normal: {
x: Math.cos(angle + Math.PI / 2),
z: Math.sin(angle + Math.PI / 2)
}
});
}
}
// 创建桌面
functioncreateTable() {
// 桌面
const tableGeometry = newTHREE.CylinderGeometry(15, 15, 0.5, 32);
const tableMaterial = newTHREE.MeshStandardMaterial({
color: 0x181820,
roughness: 0.8,
metalness: 0.2
});
const table = newTHREE.Mesh(tableGeometry, tableMaterial);
table.position.y = -0.25;
table.receiveShadow = true;
scene.add(table);
// 桌面微纹理
const tableTopGeometry = newTHREE.CircleGeometry(14.8, 64);
const tableTopMaterial = newTHREE.MeshStandardMaterial({
color: 0x202030,
roughness: 0.7,
metalness: 0.1
});
const tableTop = newTHREE.Mesh(tableTopGeometry, tableTopMaterial);
tableTop.rotation.x = -Math.PI / 2;
tableTop.position.y = 0.01;
tableTop.receiveShadow = true;
scene.add(tableTop);
}
// 创建弹珠
functioncreateMarbles() {
marbles = [];
marbleMeshes = [];
// 清空现有弹珠
marbleMeshes.forEach(mesh => scene.remove(mesh));
const marbleRadius = 0.4;
const hexRadius = 5.5;
for (let i = 0; i < marbleCount; i++) {
// 随机位置(在六边形内)
let x, z;
let validPosition = false;
let attempts = 0;
// 确保弹珠不会重叠
while (!validPosition && attempts < 100) {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * hexRadius * 0.8;
x = Math.cos(angle) * radius;
z = Math.sin(angle) * radius;
// 检查是否在六边形内
if (isPointInHexagon(x, z, hexRadius)) {
// 检查是否与其他弹珠重叠
let overlapping = false;
for (let j = 0; j < marbles.length; j++) {
const dx = x - marbles[j].x;
const dz = z - marbles[j].z;
const distance = Math.sqrt(dx * dx + dz * dz);
if (distance < marbleRadius * 2.2) {
overlapping = true;
break;
}
}
if (!overlapping) {
validPosition = true;
}
}
attempts++;
}
if (!validPosition) {
// 如果找不到合适位置,使用六边形中心附近
x = (Math.random() - 0.5) * 2;
z = (Math.random() - 0.5) * 2;
}
// 随机颜色(应用饱和度调整)
const baseColor = marbleColors[Math.floor(Math.random() * marbleColors.length)];
const saturatedColor = applySaturation(baseColor, saturationValue);
// 创建弹珠
const marble = {
x: x,
y: marbleRadius + 1.5, // 托盘高度 + 弹珠半径
z: z,
vx: (Math.random() - 0.5) * 0.5,
vy: 0,
vz: (Math.random() - 0.5) * 0.5,
radius: marbleRadius,
color: saturatedColor,
baseColor: baseColor,
mass: 1.0,
glow: 1.0 + Math.random() * 0.5,
id: i
};
marbles.push(marble);
// 创建弹珠网格
const geometry = newTHREE.SphereGeometry(marbleRadius, 24, 24);
// 自发光材质 - 使用更兼容的材质设置
const material = newTHREE.MeshStandardMaterial({
color: saturatedColor,
emissive: saturatedColor,
emissiveIntensity: 0.6,
roughness: 0.1,
metalness: 0.2
});
const mesh = newTHREE.Mesh(geometry, material);
mesh.position.set(marble.x, marble.y, marble.z);
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
marbleMeshes.push(mesh);
marble.mesh = mesh;
// 为每个弹珠添加点光源以增强自发光效果
const pointLight = newTHREE.PointLight(saturatedColor, 0.5, 3);
pointLight.position.set(marble.x, marble.y, marble.z);
scene.add(pointLight);
marble.light = pointLight;
}
document.getElementById('marble-count').textContent = marbleCount;
}
// 应用饱和度到颜色
functionapplySaturation(colorHex, saturation) {
if (saturation === 1.0) return colorHex;
// 将十六进制颜色转换为RGB
const r = (colorHex >> 16) & 0xFF;
const g = (colorHex >> 8) & 0xFF;
const b = colorHex & 0xFF;
// 计算灰度值
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
// 应用饱和度
const newR = gray + (r - gray) * saturation;
const newG = gray + (g - gray) * saturation;
const newB = gray + (b - gray) * saturation;
// 转换回十六进制
return (Math.round(newR) << 16) | (Math.round(newG) << 8) | Math.round(newB);
}
// 更新所有弹珠的颜色饱和度
functionupdateMarblesSaturation() {
marbles.forEach((marble, index) => {
const saturatedColor = applySaturation(marble.baseColor, saturationValue);
marble.color = saturatedColor;
if (marble.mesh) {
marble.mesh.material.color.setHex(saturatedColor);
marble.mesh.material.emissive.setHex(saturatedColor);
}
if (marble.light) {
marble.light.color.setHex(saturatedColor);
}
});
}
// 检查点是否在六边形内
functionisPointInHexagon(x, z, radius) {
// 简化版六边形碰撞检测
// 六边形可以近似看作六个三角形扇区
const angle = Math.atan2(z, x);
let normalizedAngle = angle;
if (normalizedAngle < 0) normalizedAngle += Math.PI * 2;
const sector = Math.floor(normalizedAngle / (Math.PI / 3));
const distance = Math.sqrt(x * x + z * z);
// 六边形内接圆半径
const innerRadius = radius * Math.cos(Math.PI / 6);
// 如果在内接圆内,肯定在六边形内
if (distance <= innerRadius) returntrue;
// 否则检查是否在六边形边界内
// 计算点到六边形中心与当前扇区两条边的距离
const sectorAngle = sector * Math.PI / 3;
const angleToSectorStart = normalizedAngle - sectorAngle;
// 六边形边界的极坐标方程
const maxDistanceAtAngle = radius * Math.cos(Math.PI / 6) / Math.cos(angleToSectorStart - Math.PI / 6);
return distance <= maxDistanceAtAngle;
}
// 物理模拟
functionsimulatePhysics(deltaTime) {
const scaledDeltaTime = deltaTime * timeScale;
const gravity = -9.8 * scaledDeltaTime;
const damping = 0.99;
const trayBottom = 1.5; // 托盘底部高度
const restitution = 0.85; // 弹性系数
const softness = 0.2; // 碰撞柔软度
// 更新每个弹珠
for (let i = 0; i < marbles.length; i++) {
const marble = marbles[i];
// 应用重力
marble.vy += gravity;
// 应用速度
marble.x += marble.vx * scaledDeltaTime;
marble.y += marble.vy * scaledDeltaTime;
marble.z += marble.vz * scaledDeltaTime;
// 与托盘底部碰撞
if (marble.y - marble.radius < trayBottom) {
marble.y = trayBottom + marble.radius;
marble.vy = -marble.vy * restitution;
// 添加柔软度 - 轻微的能量损失
marble.vx *= (1 - softness * 0.1);
marble.vz *= (1 - softness * 0.1);
// 轻微压缩效果(通过缩放实现)
if (marble.mesh) {
const compression = Math.abs(marble.vy) * 0.01;
marble.mesh.scale.y = 1 - Math.min(compression, 0.1);
marble.mesh.scale.x = 1 + Math.min(compression * 0.3, 0.03);
marble.mesh.scale.z = 1 + Math.min(compression * 0.3, 0.03);
}
}
// 与六边形墙壁碰撞
for (let w = 0; w < walls.length; w++) {
const wall = walls[w];
// 计算点到墙线的距离
const wallVecX = wall.p2.x - wall.p1.x;
const wallVecZ = wall.p2.z - wall.p1.z;
const pointVecX = marble.x - wall.p1.x;
const pointVecZ = marble.z - wall.p1.z;
const wallLengthSq = wallVecX * wallVecX + wallVecZ * wallVecZ;
const dot = pointVecX * wallVecX + pointVecZ * wallVecZ;
let t = Math.max(0, Math.min(1, dot / wallLengthSq));
const closestX = wall.p1.x + t * wallVecX;
const closestZ = wall.p1.z + t * wallVecZ;
const distX = marble.x - closestX;
const distZ = marble.z - closestZ;
const distance = Math.sqrt(distX * distX + distZ * distZ);
if (distance < marble.radius) {
// 碰撞响应
const overlap = marble.radius - distance;
const normalX = distX / distance;
const normalZ = distZ / distance;
// 将弹珠推出墙外
marble.x += normalX * overlap * 1.01;
marble.z += normalZ * overlap * 1.01;
// 计算速度在法线方向的分量
const velocityDotNormal = marble.vx * normalX + marble.vz * normalZ;
// 如果弹珠正在向墙移动
if (velocityDotNormal < 0) {
// 应用碰撞响应
const bounceX = normalX * velocityDotNormal;
const bounceZ = normalZ * velocityDotNormal;
marble.vx -= bounceX * (1 + restitution);
marble.vz -= bounceZ * (1 + restitution);
// 添加摩擦力
marble.vx *= damping;
marble.vz *= damping;
}
}
}
// 弹珠之间碰撞
for (let j = i + 1; j < marbles.length; j++) {
const marble2 = marbles[j];
const dx = marble2.x - marble.x;
const dy = marble2.y - marble.y;
const dz = marble2.z - marble.z;
const distanceSq = dx * dx + dy * dy + dz * dz;
const minDistance = marble.radius + marble2.radius;
const minDistanceSq = minDistance * minDistance;
if (distanceSq < minDistanceSq && distanceSq > 0.0001) {
const distance = Math.sqrt(distanceSq);
// 碰撞发生
const overlap = minDistance - distance;
const normalX = dx / distance;
const normalY = dy / distance;
const normalZ = dz / distance;
// 将弹珠分开
const separate = overlap * 0.5;
marble.x -= normalX * separate;
marble.y -= normalY * separate;
marble.z -= normalZ * separate;
marble2.x += normalX * separate;
marble2.y += normalY * separate;
marble2.z += normalZ * separate;
// 计算相对速度
const relVx = marble2.vx - marble.vx;
const relVy = marble2.vy - marble.vy;
const relVz = marble2.vz - marble.vz;
const velocityAlongNormal = relVx * normalX + relVy * normalY + relVz * normalZ;
// 如果弹珠正在靠近
if (velocityAlongNormal < 0) {
// 计算碰撞冲量
const impulse = 2 * velocityAlongNormal / (marble.mass + marble2.mass);
// 应用冲量
marble.vx += impulse * marble2.mass * normalX * restitution;
marble.vy += impulse * marble2.mass * normalY * restitution;
marble.vz += impulse * marble2.mass * normalZ * restitution;
marble2.vx -= impulse * marble.mass * normalX * restitution;
marble2.vy -= impulse * marble.mass * normalY * restitution;
marble2.vz -= impulse * marble.mass * normalZ * restitution;
// 添加柔软度效果
const softFactor = softness * 0.5;
marble.vx *= (1 - softFactor);
marble.vy *= (1 - softFactor);
marble.vz *= (1 - softFactor);
marble2.vx *= (1 - softFactor);
marble2.vy *= (1 - softFactor);
marble2.vz *= (1 - softFactor);
// 记录碰撞点用于调试
if (debugMode) {
marble.lastCollision = {
x: (marble.x + marble2.x) / 2,
y: (marble.y + marble2.y) / 2,
z: (marble.z + marble2.z) / 2,
time: Date.now()
};
marble2.lastCollision = marble.lastCollision;
}
}
}
}
// 速度衰减
marble.vx *= damping;
marble.vy *= damping;
marble.vz *= damping;
// 确保弹珠不会停止
const minSpeed = 0.01;
const speed = Math.sqrt(marble.vx * marble.vx + marble.vy * marble.vy + marble.vz * marble.vz);
if (speed < minSpeed && speed > 0) {
marble.vx *= minSpeed / speed;
marble.vy *= minSpeed / speed;
marble.vz *= minSpeed / speed;
}
// 更新弹珠网格位置
if (marble.mesh) {
marble.mesh.position.set(marble.x, marble.y, marble.z);
// 恢复弹珠形状(如果被压缩)
if (marble.mesh.scale.y < 1) {
marble.mesh.scale.y += 0.05 * scaledDeltaTime;
marble.mesh.scale.y = Math.min(marble.mesh.scale.y, 1);
}
if (marble.mesh.scale.x > 1) {
marble.mesh.scale.x -= 0.05 * scaledDeltaTime;
marble.mesh.scale.x = Math.max(marble.mesh.scale.x, 1);
}
if (marble.mesh.scale.z > 1) {
marble.mesh.scale.z -= 0.05 * scaledDeltaTime;
marble.mesh.scale.z = Math.max(marble.mesh.scale.z, 1);
}
}
// 更新弹珠光源位置
if (marble.light) {
marble.light.position.set(marble.x, marble.y, marble.z);
// 根据弹珠速度调整光强
const speedFactor = Math.min(speed * 5, 1);
marble.light.intensity = 0.3 + speedFactor * 0.5;
}
}
}
// 更新相机
functionupdateCamera() {
if (currentCameraMode === 'desktop') {
// 桌面视角
camera.position.x = 0;
camera.position.y = 8;
camera.position.z = 12;
camera.lookAt(0, 0, 0);
controls.enabled = false;
} elseif (currentCameraMode === 'top') {
// 俯视视角
camera.position.x = 0;
camera.position.y = 15;
camera.position.z = 0;
camera.lookAt(0, 0, 0);
controls.enabled = false;
} elseif (currentCameraMode === 'orbit') {
// 环绕视角
cameraOrbitAngle += 0.1 * timeScale;
const radius = 12;
camera.position.x = Math.sin(cameraOrbitAngle) * radius;
camera.position.z = Math.cos(cameraOrbitAngle) * radius;
camera.position.y = 6 + Math.sin(cameraOrbitAngle * 0.5) * 2;
camera.lookAt(0, 1, 0);
controls.enabled = false;
}
}
// 绘制调试信息
functiondrawDebugOverlay() {
// 清空SVG
debugOverlay.innerHTML = '<defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#55ff55"/></marker></defs>';
// 绘制六边形边界
const hexRadius = 6;
let path = '';
for (let i = 0; i <= 6; i++) {
const angle = (i * Math.PI) / 3;
const x = Math.cos(angle) * hexRadius;
const z = Math.sin(angle) * hexRadius;
// 转换到屏幕坐标
const screenPos = worldToScreen(x, 1.5, z);
if (i === 0) {
path = `M ${screenPos.x}${screenPos.y}`;
} else {
path += ` L ${screenPos.x}${screenPos.y}`;
}
}
debugOverlay.innerHTML += `<path class="debug-line" d="${path}"/>`;
// 绘制弹珠速度箭头
for (let i = 0; i < marbles.length; i++) {
const marble = marbles[i];
const speed = Math.sqrt(marble.vx * marble.vx + marble.vy * marble.vy + marble.vz * marble.vz);
if (speed > 0.1) {
const start = worldToScreen(marble.x, marble.y, marble.z);
const end = worldToScreen(
marble.x + marble.vx * 2,
marble.y + marble.vy * 2,
marble.z + marble.vz * 2
);
debugOverlay.innerHTML += `
<line class="debug-arrow"
x1="${start.x}" y1="${start.y}"
x2="${end.x}" y2="${end.y}"/>
`;
}
// 绘制碰撞点
if (marble.lastCollision && Date.now() - marble.lastCollision.time < 500) {
const point = worldToScreen(
marble.lastCollision.x,
marble.lastCollision.y,
marble.lastCollision.z
);
debugOverlay.innerHTML += `
<circle class="debug-point"
cx="${point.x}" cy="${point.y}" r="3"/>
`;
}
}
}
// 世界坐标转屏幕坐标
functionworldToScreen(x, y, z) {
const vector = newTHREE.Vector3(x, y, z);
vector.project(camera);
return {
x: (vector.x * 0.5 + 0.5) * window.innerWidth,
y: (-vector.y * 0.5 + 0.5) * window.innerHeight
};
}
// 窗口大小调整
functiononWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
// 键盘事件
functiononKeyDown(event) {
// 按D切换调试模式
if (event.key === 'd' || event.key === 'D') {
debugMode = !debugMode;
debugOverlay.style.opacity = debugMode ? '1' : '0';
}
// 按R重置
if (event.key === 'r' || event.key === 'R') {
resetMarbles();
}
// 按空格摇一摇
if (event.key === ' ') {
shakeTray();
event.preventDefault(); // 防止空格键滚动页面
}
}
// 设置UI交互
functionsetupUI() {
// 摇一摇按钮
document.getElementById('shake-btn').addEventListener('click', shakeTray);
// 重置按钮
document.getElementById('reset-btn').addEventListener('click', resetMarbles);
// 时间速度按钮
document.getElementById('speed-0.25').addEventListener('click', () =>setTimeScale(0.25));
document.getElementById('speed-1').addEventListener('click', () =>setTimeScale(1));
document.getElementById('speed-2').addEventListener('click', () =>setTimeScale(2));
// 相机视角按钮
document.getElementById('cam-desktop').addEventListener('click', () =>setCameraMode('desktop'));
document.getElementById('cam-top').addEventListener('click', () =>setCameraMode('top'));
document.getElementById('cam-orbit').addEventListener('click', () =>setCameraMode('orbit'));
// 滑块控制
document.getElementById('bloom-slider').addEventListener('input', function() {
const value = parseFloat(this.value);
bloomPass.strength = value;
document.getElementById('bloom-value').textContent = value.toFixed(1);
});
document.getElementById('saturation-slider').addEventListener('input', function() {
const value = parseFloat(this.value);
saturationValue = value;
updateMarblesSaturation();
document.getElementById('saturation-value').textContent = value.toFixed(1);
});
document.getElementById('motionblur-slider').addEventListener('input', function() {
const value = parseFloat(this.value);
// 通过调整胶片颗粒效果模拟拖影
filmPass.uniforms['sIntensity'].value = value * 0.3;
document.getElementById('motionblur-value').textContent = value.toFixed(1);
});
}
// 摇一摇功能
functionshakeTray() {
// 给所有弹珠施加随机力
marbles.forEach(marble => {
marble.vx += (Math.random() - 0.5) * 3;
marble.vy += Math.random() * 2;
marble.vz += (Math.random() - 0.5) * 3;
});
// 添加视觉反馈
const shakeBtn = document.getElementById('shake-btn');
shakeBtn.style.backgroundColor = 'rgba(80, 100, 255, 0.7)';
setTimeout(() => {
shakeBtn.style.backgroundColor = '';
}, 300);
}
// 重置弹珠
functionresetMarbles() {
createMarbles();
}
// 设置时间速度
functionsetTimeScale(scale) {
timeScale = scale;
// 更新按钮状态
document.querySelectorAll('.control-btn').forEach(btn => btn.classList.remove('active'));
if (scale === 0.25) document.getElementById('speed-0.25').classList.add('active');
if (scale === 1) document.getElementById('speed-1').classList.add('active');
if (scale === 2) document.getElementById('speed-2').classList.add('active');
}
// 设置相机模式
functionsetCameraMode(mode) {
currentCameraMode = mode;
// 更新按钮状态
document.querySelectorAll('.control-btn').forEach(btn => btn.classList.remove('active'));
if (mode === 'desktop') document.getElementById('cam-desktop').classList.add('active');
if (mode === 'top') document.getElementById('cam-top').classList.add('active');
if (mode === 'orbit') document.getElementById('cam-orbit').classList.add('active');
}
// 动画循环
functionanimate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
// 物理模拟
simulatePhysics(deltaTime);
// 更新相机
updateCamera();
// 更新轨道控制
if (controls.enabled) {
controls.update();
}
// 绘制调试信息
if (debugMode) {
drawDebugOverlay();
}
// 渲染
composer.render();
}
// 开始检查Three.js加载状态
checkThreeJsLoaded();
});
</script>
</body>
</html>
结语
时间有点仓促,就先写这么多吧,后面有了更多实测体验再补充。现在回头看,大模型的发展节奏很像软件从 demo 走向生产:早期是“能用”——你得容忍它偶尔跑偏、靠自己补齐;后来变成“可用”——你开始敢把它塞进流程里,让它承担一部分确定性的工作;而 GPT-5.2 这类更新更像在逼近下一阶段:不是单点能力更强,而是更稳定地产出可交付的结果,把“少说两句”换成“少返工几次”。如果说过去我们在讨论模型会不会写、会不会答,那么接下来更值得问的可能是:它能不能在真实工作里持续可靠地完成一段流程、留下可审计的痕迹、并让人类把精力从体力活挪回判断与创造——当“可用”成为默认,真正改变的也许不是效率,而是我们对工作的组织方式。
References
GPT-5.2:https://openai.com/index/introducing-gpt-5-2
[2]GDPval:https://openai.com/index/gdpval
[3]gdpval datasets:https://huggingface.co/datasets/openai/gdpval