免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Three.js实现3D滑动变阻器:建模、拖拽与欧姆定律模拟

Three.js实现3D滑动变阻器:建模、拖拽与欧姆定律模拟 在物理课上滑动变阻器是电学实验里的“常客”在 3D 可视化项目中它却是非常适合拿来练手的交互模型。本文会从一个完整的 Vibecoding自然语言驱动编程项目入手带大家从零搭建一个可拖拽、可观察电流与亮度变化的 3D 互动版滑动变阻器。项目基于 Three.js 实现包含完整的场景搭建、模型组合、射线检测拖拽和电路状态模拟。如果你对前端 3D 交互感兴趣或者想系统了解怎么用 AI 辅助开发一个“能上手把玩”的小项目那这篇文章会很适合你。读完你会掌握 Three.js 场景组织、鼠标/触摸拖拽原理、欧姆定律的可视化映射以及一套适合复制到其他 3D 项目中的工程组织方式。1. 项目背景与效果演示1.1 什么是 VibecodingVibecoding 是最近热度很高的编程方式中文常被翻译成“氛围编程”或“自然语言驱动编程”。简单说就是开发者用自然语言把需求描述给 AI 编程助手AI 生成初版代码人在这个基础上审阅、调整、测试反复迭代直至功能完成。它不是“完全不用写代码”而是把工作重心从“逐行敲语法”转移到“定义需求、拆解边界、组织架构、验证结果”上。对这种协作模式开发者的判断力反而更重要因为 AI 生成的代码可能逻辑正确但坐标系搞反了、交互状态没处理好、运行性能不佳这些都需要人来兜底。本文的“滑动变阻器 3D 互动版”就是一个典型 Vibecoding 项目需求描述很直观核心难点集中在 3D 场景搭建、交互检测和数值联动非常适合拆成多个小任务交给 AI 生成再由开发者逐步整合和打磨。1.2 为什么是“滑动变阻器”滑动变阻器有时也会被写成“滑动变祖器”本质上是一个可以通过滑动滑块改变接入电路电阻值的元件。它在初中物理和电子电路实验中非常常见结构也很有辨识度底座、支架和绝缘瓷管。绕在瓷管上的电阻丝。可以在导轨上滑动的金属滑块。两个接线柱用于接入电路。把它做成 3D 互动版本至少有三个好处结构建模有层次感底座、导轨、线圈、滑块、接线柱都是规则几何体适合用 Three.js 基础几何体拼装。交互逻辑清晰滑块沿一个方向滑动天然对应鼠标拖拽或键盘控制适合用来演示射线检测和平面求交。物理模拟直观滑块位置对应电阻值再结合欧姆定律就可以驱动灯泡亮度、电流表读数是一个“小而完整”的可视化案例。1.3 最终效果项目完成后的预期效果页面显示一个逼真的 3D 滑动变阻器旁边有一个小灯泡。用户可以用鼠标拖拽橙色滑块或按键盘左/右方向键微调滑块位置。滑块位置变化后右侧面板实时显示滑块位置、电阻值、回路电流、灯泡亮度。灯泡发光强度会随着电阻变化同步变化模拟真实电路中的亮度反馈。用户还可以按住鼠标右键或使用鼠标中键旋转视角从不同角度观察模型结构。这个项目不需要后端服务也不需要数据库纯前端即可运行非常适合作为 Three.js 入门和交互设计的练手项目。2. 环境准备与技术选型2.1 技术栈说明本项目的核心依赖是 Three.js它是一个基于 WebGL 的 3D 渲染库封装了场景、相机、灯光、几何体、材质、射线检测等常用能力。相比直接用原生 WebGL 开发Three.js 的开发效率高很多也是目前前端 3D 可视化的主流方案。项目构建工具使用 Vite它启动速度快、配置简单适合快速搭建一个纯前端 3D 演示项目。开发语言使用 JavaScript不需要 TypeScript降低上手门槛。如果你平时使用 TypeScript 较多也可以把代码迁移到.ts逻辑基本相同。2.2 初始化项目首先确保本机已安装 Node.js建议使用 LTS 版本例如 18 或 20。打开终端执行npm create vitelatest slide-rheostat -- --template vanilla cd slide-rheostat npm install npm install three0.160.0这里创建的是 Vite 的 vanilla 模板默认结构比较干净适合我们手动添加 Three.js 代码。安装完成后执行npm run dev浏览器访问控制台输出的本地地址如果能看到 Vite 的默认页面说明环境搭建成功。接下来我们替换默认代码。版本提示Three.js 版本更新比较快不同版本之间 API 可能有细微差异。本文示例以three0.160.0为主如果你使用更高版本整体 API 基本兼容但遇到个别报错时请优先查看当前版本的迁移说明。2.3 项目文件结构本项目的最终目录结构如下slide-rheostat/ ├── index.html ├── package.json ├── src/ │ ├── main.js │ └── style.css我们不需要修改main.js之外的额外模块全部代码都集中在一个文件中便于阅读和复制。这个小项目的代码量虽然不大但足够覆盖一个 3D 交互项目的核心环节。3. 核心原理拆解3.1 滑动变阻器的结构与视觉建模在开始写代码之前先思考滑动变阻器的物理结构对应 Three.js 里的哪些几何体。底座一个长条形的长方体放在最下方。可以用BoxGeometry实现。支架左右两端各一个竖直的长方体起到支撑和固定作用。导轨两根平行的细圆柱从左边延伸到右边滑块在上面滑动。可以用圆柱体旋转 90 度得到。线圈一圈一圈绕在瓷管上的电阻丝在 3D 中可以用多个圆环TorusGeometry沿 X 轴排列来模拟。滑块包括上方的手柄和下方的金属触片可以组合多个长方体或圆柱体。接线柱在两端各放一个圆柱体顶部加一个球体模拟拧紧螺丝的造型。灯泡用一个发光半透明的球体表示并预留发光材质的emissiveIntensity属性用来动态控制亮度。把这些几何体放到一个Group里方便统一旋转和平移。Three.js 中的Group类似于一个容器子对象的位置都是相对容器的局部坐标这样我们可以整体操作整个滑动变阻器模型。3.2 Three.js 场景与坐标规划Three.js 使用右手坐标系X 轴向右。Y 轴向上。Z 轴指向屏幕外。滑动变阻器的主体是沿 X 方向伸展的。底座放在地面上Y 轴正方向向上。这样滑块沿 X 轴左右移动时只需要改变滑块容器sliderGroup.position.x。在建模之前先把坐标规划清楚可以避免后续调整模型时反复返工。本文的坐标约定如下底座中心在(0, 0.15, 0)高度 0.3。导轨高度约为y 0.85。滑块接触片高度约为y 1.06手柄高度约为y 1.38。滑块活动范围minX -1.6maxX 1.6。灯泡放在右侧例如(3.6, 0.8, 0.6)。这套坐标不是唯一方案但在建模前先规划会让代码更整齐。3.3 拖拽交互与射线检测让滑块可以被鼠标拖拽核心是 Three.js 的Raycaster。原理很简单用户鼠标点击屏幕浏览器提供clientX和clientY。将屏幕坐标换算为 Three.js 的标准化设备坐标NDC范围在[-1, 1]。调用raycaster.setFromCamera(pointer, camera)从相机位置发射一条射线。射线与场景中的网格对象求交判断是否命中了滑块。如果命中了滑块就进入拖拽状态。拖拽过程中我们不直接计算滑块的新位置而是让射线与一个虚拟平面求交取交点的 X 坐标再夹紧到滑动范围内。这样可以避免滑块跟随鼠标时出现忽快忽慢、上下跳动的问题。这里还有一个关键细节拖拽过程中必须临时禁用OrbitControls否则你拖动滑块时相机视角也会跟着旋转交互体验会非常糟糕。3.4 欧姆定律驱动的电路模拟滑动变阻器在电路中通常和灯泡、电源串联。为了让互动效果更真实我们用一个简化的电路模型电源电压U 3V。灯泡固定内阻R_lamp 5Ω。滑动变阻器电阻R_slide随滑块位置在0.5Ω ~ 10Ω之间变化。滑块在最左端时电阻最大向右滑动电阻逐渐减小。根据欧姆定律回路电流为I U / (R_lamp R_slide)灯泡亮度与电流成正比。电流越大灯泡越亮。在 Three.js 中我们可以动态设置灯泡材质的emissiveIntensity属性让发光效果随电流变化。这套逻辑不需要真实现场电路仿真但对可视化演示来说已经足够直观。更重要的是它把滑块位置、电阻数值、电流数值、灯泡亮度串成了一条清晰的反馈链。4. 完整代码实现下面进入核心实战环节。整体代码分为index.html、src/style.css、src/main.js三个文件你可以直接复制到 Vite 项目中使用。4.1 index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleVibecoding - 滑动变阻器 3D 互动版/title /head body div idhud h2滑动变阻器 · 3D 互动版/h2 p拖拽橙色滑块或使用键盘左右方向键/p table tr td滑块位置/td td idposition50%/td /tr tr td滑动电阻/td td idresistance5.25 Ω/td /tr tr td回路电流/td td idcurrent0.585 A/td /tr tr td灯泡亮度/td td idbrightness97%/td /tr /table p classtip右键拖拽可以旋转视角滚轮缩放/p /div script typemodule src/src/main.js/script /body /htmlindex.html里最关键的是一块 HUD 浮层用来实时显示状态数据。main.js中会通过document.getElementById(position)等方式更新这些文本内容。4.2 src/style.css* { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; font-family: Segoe UI, Microsoft YaHei, sans-serif; background: #111827; } canvas { display: block; } #hud { position: fixed; top: 20px; right: 20px; width: 240px; background: rgba(17, 24, 39, 0.85); color: #f9fafb; border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 12px; padding: 16px 18px; backdrop-filter: blur(6px); z-index: 10; user-select: none; } #hud h2 { font-size: 16px; margin-bottom: 6px; color: #fbbf24; } #hud p { font-size: 12px; color: #9ca3af; margin-bottom: 8px; } #hud table { width: 100%; border-collapse: collapse; font-size: 13px; } #hud td { padding: 4px 0; } #hud td:last-child { text-align: right; font-weight: 600; color: #fbbf24; } #hud .tip { margin-top: 10px; font-size: 12px; color: #6b7280; }CSS 部分重点在于 HUD 浮层的位置和半透明效果。backdrop-filter: blur(6px)可以做出磨砂玻璃的质感如果你不需要这种效果去掉这一行即可。4.3 src/main.js场景、相机、灯光下面进入核心代码。先导入 Three.js 和 OrbitControlsimport * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; // 1. 场景 const scene new THREE.Scene(); scene.background new THREE.Color(0x111827); // 2. 相机 const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.set(5, 4, 8); camera.lookAt(0, 1, 0); // 3. 渲染器 const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled true; document.body.appendChild(renderer.domElement); // 4. 轨道控制器 const controls new OrbitControls(camera, renderer.domElement); controls.target.set(0, 1, 0); controls.enableDamping true; controls.update(); // 5. 灯光 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const dirLight new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(5, 8, 6); dirLight.castShadow true; scene.add(dirLight);这部分的要点PerspectiveCamera是透视相机适合模拟人眼观察效果45 表示视角大小。OrbitControls提供旋转、缩放、平移相机的能力适合 3D 可视化项目。环境光负责整体照明平行光负责产生明暗对比和阴影。4.4 src/main.js创建滑动变阻器模型先创建一个总的分组容器rheostatGroup后续所有模型都挂在它下面const rheostatGroup new THREE.Group(); scene.add(rheostatGroup);创建底座、支架和导轨// 底座 const baseGeo new THREE.BoxGeometry(5.2, 0.3, 1.8); const baseMat new THREE.MeshStandardMaterial({ color: 0x92400e, roughness: 0.7 }); const base new THREE.Mesh(baseGeo, baseMat); base.position.y 0.15; base.receiveShadow true; rheostatGroup.add(base); // 左右支架 const bracketGeo new THREE.BoxGeometry(0.25, 1.2, 1.1); const bracketMat new THREE.MeshStandardMaterial({ color: 0x6b7280, metalness: 0.4, roughness: 0.6 }); const bracketL new THREE.Mesh(bracketGeo, bracketMat); bracketL.position.set(-2.15, 0.75, 0); rheostatGroup.add(bracketL); const bracketR bracketL.clone(); bracketR.position.x 2.15; rheostatGroup.add(bracketR); // 两根导轨 const railGeo new THREE.CylinderGeometry(0.06, 0.06, 4.4, 16); const railMat new THREE.MeshStandardMaterial({ color: 0x9ca3af, metalness: 0.8, roughness: 0.3 }); const rail1 new THREE.Mesh(railGeo, railMat); rail1.rotation.z Math.PI / 2; rail1.position.set(0, 0.85, 0.55); rheostatGroup.add(rail1); const rail2 rail1.clone(); rail2.position.set(0, 0.85, -0.55); rheostatGroup.add(rail2);这里用到了MeshStandardMaterial它支持金属度和粗糙度参数。导轨的metalness调高一点会有金属反光质感。接下来创建电阻线圈。用多个圆环沿 X 轴排列模拟绕在瓷管上的电阻丝const coilGroup new THREE.Group(); const ringGeo new THREE.TorusGeometry(0.32, 0.045, 12, 24); const coilMat new THREE.MeshStandardMaterial({ color: 0xc084fc, roughness: 0.4, metalness: 0.3 }); const turns 30; const coilStartX -1.7; const coilEndX 1.7; for (let i 0; i turns; i) { const ring new THREE.Mesh(ringGeo, coilMat); // 绕 Y 轴旋转 90 度让圆环平面垂直于 X 轴 ring.rotation.y Math.PI / 2; ring.position.x coilStartX i * ((coilEndX - coilStartX) / (turns - 1)); ring.position.y 1.0; coilGroup.add(ring); } rheostatGroup.add(coilGroup);这里的关键是ring.rotation.y Math.PI / 2。默认TorusGeometry的圆环面在 XY 平面旋转 90 度后圆环面朝 YZ 方向才能像电阻丝一样一圈圈沿 X 轴排开。创建滑块。滑块是一个独立的Group之后我们只需要修改sliderGroup.position.x就能移动它const sliderGroup new THREE.Group(); // 手柄 const handleGeo new THREE.BoxGeometry(0.35, 0.55, 0.45); const handleMat new THREE.MeshStandardMaterial({ color: 0xf59e0b, roughness: 0.5 }); const handle new THREE.Mesh(handleGeo, handleMat); handle.position.y 1.38; sliderGroup.add(handle); // 金属触片 const contactGeo new THREE.BoxGeometry(0.45, 0.08, 0.72); const contactMat new THREE.MeshStandardMaterial({ color: 0xd1d5db, metalness: 0.9, roughness: 0.2 }); const contact new THREE.Mesh(contactGeo, contactMat); contact.position.y 1.06; sliderGroup.add(contact); sliderGroup.position.set(0, 0, 0); rheostatGroup.add(sliderGroup);创建两个接线柱。为了方便复用封装成一个函数function createTerminal(x, z) { const group new THREE.Group(); const postGeo new THREE.CylinderGeometry(0.12, 0.12, 0.5, 16); const postMat new THREE.MeshStandardMaterial({ color: 0xfbbf24, metalness: 0.8, roughness: 0.3 }); const post new THREE.Mesh(postGeo, postMat); post.position.y 1.55; group.add(post); const knobMat new THREE.MeshStandardMaterial({ color: 0xfcd34d, metalness: 0.6, roughness: 0.4 }); const knob new THREE.Mesh(new THREE.SphereGeometry(0.14, 16, 12), knobMat); knob.position.y 1.85; group.add(knob); group.position.set(x, 0, z); return group; } rheostatGroup.add(createTerminal(-2.15, 0.7)); rheostatGroup.add(createTerminal(2.15, 0.7));4.5 src/main.js添加灯泡与导线灯泡放在右侧用来显示亮度变化。这里把发光球体的材质保存到全局变量bulbMat便于后续动态更新发光强度const lampGroup new THREE.Group(); lampGroup.position.set(3.6, 0.8, 0.6); // 灯泡底座 const lampBaseGeo new THREE.CylinderGeometry(0.2, 0.3, 0.5, 16); const lampBaseMat new THREE.MeshStandardMaterial({ color: 0x9ca3af, metalness: 0.5, roughness: 0.4 }); const lampBase new THREE.Mesh(lampBaseGeo, lampBaseMat); lampBase.position.y 0.25; lampGroup.add(lampBase); // 发光灯泡 const bulbGeo new THREE.SphereGeometry(0.32, 24, 24); const bulbMat new THREE.MeshStandardMaterial({ color: 0xfff7ed, emissive: 0xfff1c4, emissiveIntensity: 0.1, transparent: true, opacity: 0.9 }); const bulb new THREE.Mesh(bulbGeo, bulbMat); bulb.position.y 0.9; lampGroup.add(bulb); rheostatGroup.add(lampGroup);再添加两根导线让模型更完整。导线可以用CatmullRomCurve3生成曲线再用TubeGeometry生成管道网格function createWire(from, to, color 0xf87171) { const points [ from, new THREE.Vector3( (from.x to.x) / 2, Math.max(from.y, to.y) 0.4, (from.z to.z) / 2 ), to ]; const curve new THREE.CatmullRomCurve3(points); const tubeGeo new THREE.TubeGeometry(curve, 32, 0.025, 6, false); const tubeMat new THREE.MeshStandardMaterial({ color, roughness: 0.6 }); return new THREE.Mesh(tubeGeo, tubeMat); } const wireFromRight new THREE.Vector3(2.15, 1.55, 0.7); const wireToBulb new THREE.Vector3(3.6, 0.55, 0.6); rheostatGroup.add(createWire(wireFromRight, wireToBulb));CatmullRomCurve3会根据三个或三个以上的点生成一条平滑曲线。这里用“起点、中点抬高、终点”三个点来模拟导线稍微拱起的效果。4.6 src/main.js拖拽交互与控制现在实现最关键的一步让滑块可以被鼠标拖拽同时支持键盘控制。完整代码如下// 交互状态与常量 const pointer new THREE.Vector2(); const raycaster new THREE.Raycaster(); const MIN_X -1.6; const MAX_X 1.6; const sliderY 1.2; const dragPlane new THREE.Plane(new THREE.Vector3(0, 1, 0), -sliderY); let isDragging false; // 电路参数 const VOLTAGE 3; const MIN_RESISTANCE 0.5; const MAX_RESISTANCE 10; const LAMP_RESISTANCE 5; function getPointerNDC(event) { const rect renderer.domElement.getBoundingClientRect(); pointer.x ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y -((event.clientY - rect.top) / rect.height) * 2 1; } function onPointerDown(event) { getPointerNDC(event); raycaster.setFromCamera(pointer, camera); const hits raycaster.intersectObject(sliderGroup, true); if (hits.length 0) { isDragging true; controls.enabled false; renderer.domElement.style.cursor grabbing; } } function onPointerMove(event) { getPointerNDC(event); if (isDragging) { raycaster.setFromCamera(pointer, camera); const hit new THREE.Vector3(); const intersected raycaster.ray.intersectPlane(dragPlane, hit); if (intersected) { sliderGroup.position.x THREE.MathUtils.clamp(hit.x, MIN_X, MAX_X); updateState(); } } } function onPointerUp() { isDragging false; controls.enabled true; renderer.domElement.style.cursor default; } function onKeyDown(event) { if (event.key ArrowLeft) { sliderGroup.position.x THREE.MathUtils.clamp( sliderGroup.position.x - 0.1, MIN_X, MAX_X ); updateState(); event.preventDefault(); } if (event.key ArrowRight) { sliderGroup.position.x THREE.MathUtils.clamp( sliderGroup.position.x 0.1, MIN_X, MAX_X ); updateState(); event.preventDefault(); } } renderer.domElement.addEventListener(pointerdown, onPointerDown); window.addEventListener(pointermove, onPointerMove); window.addEventListener(pointerup, onPointerUp); window.addEventListener(keydown, onKeyDown);代码中有一个细节值得强调dragPlane的构造参数。THREE.Plane(normal, constant)的数学含义是满足dot(normal, point) -constant的点的集合。因为我们要在y 1.2这个高度上做拖拽平面所以传入 new THREE.Plane(new THREE.Vector3(0, 1, 0),
返回列表