news 2026/9/3 1:48:26

用Skill生成Three.js 3D特效网页:从粒子星云到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Skill生成Three.js 3D特效网页:从粒子星云到工程实践

一个 Skill,做出 3D 特效网页,太炫酷了:从 Skill 编写到 Three.js 粒子星云落地

最近在折腾 AI 编程工具时发现一个特别有意思的玩法:不用手写一大堆 Three.js 代码,也不用反复调整相机参数和粒子颜色,只需要让 AI 助手加载一个专门写好的Skill,然后描述“我要一个星空粒子特效”,它就能自动生成一个可以直接运行的 3D 网页。一开始我还以为这是某种“提示词魔法”,但真正动手拆解之后才发现,Skill的本质是一套结构化的规则文件 + 可复用的工具脚本,它把平时我们写 3D 页面时反复踩坑的那些经验——场景初始化、相机位置、粒子动画、鼠标交互、CDN 引入方式——全部固化成了标准流程。

本文就从零开始,带着大家完整做一遍:先搞清楚Skill是什么,再编写一个专门生成 3D 特效网页的Skill,最后用这个Skill生成一个基于 Three.js 的粒子星云动态页面。整个流程我会把每个文件、每段代码、每条命令都贴出来,并解释关键参数的作用,保证你照着操作就能跑起来。如果你之前对Skill和 3D 网页开发都只是听说过,这篇教程应该能帮你把两边串起来。

1. 背景与核心概念

1.1 什么是“Skill”,它和普通提示词有什么区别

Skill可以理解为给 AI 编程助手准备的一套“岗位说明书 + 工具包”。它通常包含一个描述文件,比如SKILL.md,里面写清楚这个 Skill 的适用场景、输出规范、工作步骤和注意事项;同时还可以附带一些可执行的脚本、模板文件或参考代码,放在resources目录下。

普通提示词的问题是“一次性”:你每次都要重新告诉 AI 要做什么、不要做什么、输出格式是什么,遇到复杂任务时,AI 很容易在前几步就偏离方向。而Skill把这些上下文固化下来,AI 一旦检测到用户需求匹配某个 Skill,就会自动加载对应的规则和资源,直接按照里面定义好的流程执行。对于 3D 网页这种“步骤固定、但要写的东西很多”的任务来说,Skill的价值非常明显:流程标准化、输出可预期、代码质量稳定。

最近 GitHub 和各大 AI 编程工具中,skillagent skillcodex skill这些词的热度一路走高,本质上就是因为大家发现:与其每次让 AI “自由发挥”,不如先用Skill把最佳实践沉淀下来。你可以把Skill理解成 AI 世界的“插件”,而本文要实现的,就是一个专门生成 3D 特效网页的插件。

1.2 为什么用 Three.js 做 3D 特效网页

Three.js 是目前 Web 端最流行的 3D 渲染库,它基于 WebGL,但把底层复杂的着色器、矩阵运算、缓冲区管理封装成了相对友好的 JavaScript API。我们不需要了解 GPU 管线细节,就能用SceneCameraMeshPoints这些对象搭建一个可交互的 3D 场景。

常见的 3D 特效网页,比如粒子星云、动态文字、产品 3D 展示、数据可视化大屏等,使用 Three.js 都能在浏览器里直接运行。相比 C4D 或 Blender 导出的静态视频,Three.js 做出来的页面是实时渲染的,用户拖拽鼠标时画面会实时变化,这种沉浸感特别适合活动页面、个人主页、产品介绍页。

本文的实战部分会做一个粒子星云效果:几千个粒子在空间中缓慢旋转,鼠标移动时视角和粒子运动方向会发生变化,视觉上像一片动态的星空。这个效果在 3D 特效网页里很常见,但涉及的 API 覆盖了BufferGeometryPointsMaterialrequestAnimationFrameraycaster周边的基础用法,很适合作为入门到进阶的过渡案例。

1.3 一个 Skill 解决 3D 特效网页的完整链路

我们用Skill生成 3D 网页,本质上是要建立一条从“用户描述”到“可运行代码”的流水线。用户只需要说“我想要一个紫色调的 3D 粒子星空”,Skill内部就会完成下面这些事:

  1. 解析场景类型:识别是粒子效果、几何体动画,还是文字特效。
  2. 确定技术方案:默认使用 Three.js,可选 CDN 方式或 Vite 工程方式。
  3. 生成完整代码:包括 HTML 结构、Three.js 场景初始化、粒子系统、动画循环、鼠标交互。
  4. 输出运行指引:告诉用户如何启动本地服务器、如何修改参数。

听起来很神奇,但实现起来并不复杂,核心就是一个写得很规范的SKILL.md文件加上一个生成脚本。下面我们就动手开始做。

2. 环境准备与版本说明

这个项目不依赖重型环境,主要用到 Node.js 和现代浏览器。建议环境如下:

  • 操作系统:Windows 10/11、macOS、Linux 均可,本文命令以 macOS/Linux 终端为主,Windows 可改用 PowerShell 或 Git Bash。
  • Node.js:建议 18 或更高版本。本文的脚本使用 ESM 语法,低版本 Node 需要调整。
  • 浏览器:Chrome、Edge 或 Firefox 最新版,用于预览 3D 页面。
  • AI 编程工具:支持 Skill 的 Claude Code、Cursor、Windsurf 等。本文以通用 Skill 规范为例,如果你使用的工具目录结构不同,可以按对应工具的官方文档调整。

如果 Node.js 版本较低,可以先升级,或者把脚本中的 ESM 语法改为 CommonJS。本文示例以常见环境为例,重点演示配置思路,版本需要根据你的项目实际情况调整。

先建一个总目录,所有文件都放在里面:

mkdir skill-3d-web cd skill-3d-web

后续所有路径都基于这个skill-3d-web目录展开。

3. 编写一个生成 3D 特效网页的 Skill

3.1 Skill 的目录结构

一个标准的 Skill 通常由两部分组成:描述文件SKILL.md和资源目录resources。我们还需要一个output目录来存放生成的网页。整体结构如下:

skill-3d-web/ ├── README.md ├── skill/ │ ├── SKILL.md │ └── resources/ │ └── generate-3d-scene.mjs └── output/

说明:SKILL.md是 AI 助手加载这个 Skill 时需要阅读的核心文件;resources目录存放生成网页时可能用到的脚本和模板;output是脚本生成的网页输出目录。

3.2 编写 SKILL.md

SKILL.md的作用是告诉 AI 助手:什么时候使用这个 Skill,使用时的步骤是什么,生成的结果应该满足什么要求。一个有效的SKILL.md不需要写很长,但一定要把关键规则写清楚,尤其是“输出物必须能直接运行”这条。

下面是完整的skill/SKILL.md内容:

--- name: 3d-scene-generator description: 当用户要求生成 3D 特效网页、3D 粒子效果、3D 交互页面或 Three.js 场景时,使用该 Skill。也可用于生成活动页、个人主页、产品展示页中的 3D 视觉模块。 --- # 3D 场景生成 Skill ## 适用场景 - 用户想要一个 3D 粒子星空、粒子漩涡、动态几何体页面 - 用户想要一个带鼠标交互的 3D 网页 - 用户想要一个基于 Three.js 的单页面 3D 特效 ## 输出要求 1. 默认生成两个文件:`index.html` 和 `main.js` 2. 使用 Three.js,优先通过 CDN 方式引入,版本使用 0.160.0 以上 3. 页面必须包含: - 一个自动旋转或缓动的 3D 场景 - 粒子系统或几何体对象 - 鼠标移动交互 - 窗口尺寸变化的响应处理 4. 代码必须完整可运行,不能使用“省略”“...” 5. 生成后给出启动本地服务器的命令 ## 工作步骤 1. 询问或推断用户的 3D 场景主题:粒子星空、粒子漩涡、几何体动画 2. 询问主色调,默认使用蓝紫色渐变 3. 运行 `resources/generate-3d-scene.mjs` 脚本生成网页文件 4. 检查生成结果,确认没有缺少文件 5. 输出运行说明

这个文件的核心是「输出要求」和「工作步骤」两部分。输出要求定义了代码质量底线,工作步骤则约束了 AI 的行为顺序,避免 AI 不问需求就直接写一堆用户不需要的代码。

3.3 编写生成脚本

generate-3d-scene.mjs是一个 Node.js 脚本,作用是根据用户传入的主题和颜色,自动生成一套完整的 3D 网页。这个脚本的初衷是:让 AI 在执行 Skill 时有一个稳定的“生成器”,而不是每次都在对话里随机拼接代码。

脚本代码如下:

// 文件路径:skill/resources/generate-3d-scene.mjs import fs from 'node:fs'; import path from 'node:path'; const args = process.argv.slice(2); const sceneType = args[0] || 'particle-star'; const title = args[1] || '3D Star Field'; const colorHex = args[2] || '#7c5cff'; const outputDir = path.resolve('output', '3d-scene'); fs.mkdirSync(outputDir, { recursive: true }); const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>${title}</title> <style> * { margin: 0; padding: 0; } body { overflow: hidden; background: #050510; } canvas { display: block; } #info { position: fixed; top: 20px; left: 20px; color: rgba(255,255,255,0.8); font-family: "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 14px; pointer-events: none; z-index: 10; } </style> </head> <body> <div id="info">${title} · 移动鼠标交互</div> <script type="module" src="./main.js"></script> </body> </html> `; fs.writeFileSync(path.join(outputDir, 'index.html'), html, 'utf-8'); let mainJs = ''; if (sceneType === 'particle-star') { mainJs = ` import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050510); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const particleCount = 2600; const positions = new Float32Array(particleCount * 3); const velocities = new Float32Array(particleCount); for (let i = 0; i < particleCount; i++) { const radius = 2.5 + Math.random() * 3; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta); positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta) * 0.6; positions[i * 3 + 2] = radius * Math.cos(phi); velocities[i] = 0.005 + Math.random() * 0.01; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ color: ${JSON.stringify(colorHex)}, size: 0.035, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false }); const points = new THREE.Points(geometry, material); scene.add(points); let mouseX = 0; let mouseY = 0; window.addEventListener('mousemove', (e) => { mouseX = (e.clientX / window.innerWidth) * 2 - 1; mouseY = -(e.clientY / window.innerHeight) * 2 + 1; }); function animate() { requestAnimationFrame(animate); const time = performance.now() * 0.001; const pos = geometry.attributes.position.array; for (let i = 0; i < particleCount; i++) { const idx = i * 3; const angle = velocities[i] * time; const x = pos[idx]; const z = pos[idx + 2]; pos[idx] = x * Math.cos(angle) - z * Math.sin(angle); pos[idx + 2] = x * Math.sin(angle) + z * Math.cos(angle); } geometry.attributes.position.needsUpdate = true; points.rotation.y += 0.0008; points.rotation.x = mouseY * 0.2; camera.position.x += (mouseX * 1.2 - camera.position.x) * 0.05; camera.lookAt(0, 0, 0); renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); `; } else if (sceneType === 'particle-vortex') { mainJs = ` import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x080818); const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 3, 8); camera.lookAt(0, 0, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const particleCount = 4000; const positions = new Float32Array(particleCount * 3); const speeds = new Float32Array(particleCount); for (let i = 0; i < particleCount; i++) { const angle = Math.random() * Math.PI * 2; const radius = Math.random() * 4; positions[i * 3] = Math.cos(angle) * radius; positions[i * 3 + 1] = (Math.random() - 0.5) * 5; positions[i * 3 + 2] = Math.sin(angle) * radius; speeds[i] = 0.02 + Math.random() * 0.04; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ color: ${JSON.stringify(colorHex)}, size: 0.03, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false }); const points = new THREE.Points(geometry, material); scene.add(points); let mouseX = 0; let mouseY = 0; window.addEventListener('mousemove', (e) => { mouseX = (e.clientX / window.innerWidth) * 2 - 1; mouseY = -(e.clientY / window.innerHeight) * 2 + 1; }); function animate() { requestAnimationFrame(animate); const time = performance.now() * 0.001; const pos = geometry.attributes.position.array; for (let i = 0; i < particleCount; i++) { const idx = i * 3; const currentAngle = Math.atan2(pos[idx + 2], pos[idx]); const r = Math.sqrt(pos[idx] * pos[idx] + pos[idx + 2] * pos[idx + 2]); const nextAngle = currentAngle + speeds[i] * 0.01; pos[idx] = Math.cos(nextAngle) * r; pos[idx + 2] = Math.sin(nextAngle) * r; } geometry.attributes.position.needsUpdate = true; points.rotation.y = time * 0.1; camera.position.x += (mouseX * 2 - camera.position.x) * 0.04; camera.position.y += (mouseY * 2 - camera.position.y) * 0.04; camera.lookAt(0, 0, 0); renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); `; } fs.writeFileSync(path.join(outputDir, 'main.js'), mainJs, 'utf-8'); console.log('生成完成!'); console.log('输出目录:', path.resolve(outputDir)); console.log('启动方式: cd output/3d-scene && npx serve . 或 python3 -m http.server 8080');

这个脚本涵盖了两种场景:particle-star(粒子星空)和particle-vortex(粒子漩涡)。你可以按需扩展更多场景,例如text-3dmesh-wave等。

脚本里有几个关键点需要特别说明:

使用 CDN 方式引入 Three.js。这里用的是unpkg上的 ESM 模块地址,配合<script type="module">可以在不安装 npm 依赖的情况下直接在浏览器运行。对于演示项目和活动页面来说非常方便;如果要做生产级项目,建议改用 npm 安装并配合 Vite 打包。

粒子位置使用 BufferGeometry 的 Float32Array。直接操作positions数组并设置needsUpdate = true,是 Three.js 中做粒子动画的标准做法,性能远高于每帧重新创建几何体对象。

动画循环与鼠标交互requestAnimationFrame是浏览器动画的标准驱动方式;鼠标位置先归一化到-11,再通过一个缓动系数(例如0.05)控制相机位置,就能做出平滑跟随效果,而不是生硬地跳变。

3.4 添加 README.md

在项目根目录放一个README.md,方便以后使用和分享:

# Skill 生成 3D 特效网页 这是一个演示项目:通过自定义 Skill 生成基于 Three.js 的 3D 特效网页。 ## 使用方法 1. 将 `skill` 目录放入你的 AI 工具 Skill 目录。 2. 在 AI 对话中描述你想要的 3D 网页。 3. AI 会调用 `skill/resources/generate-3d-scene.mjs` 生成页面。 4. 进入 `output/3d-scene` 目录启动本地服务器查看效果。 ## 手动运行脚本 直接执行: bash node skill/resources/generate-3d-scene.mjs particle-star "My 3D Star" "#7c5cff"

4. 用 Skill 生成并运行 3D 特效网页

4.1 模拟 AI 执行 Skill 的过程

如果你想先手动测试脚本,不需要真的打开 AI 工具,直接在终端执行:

node skill/resources/generate-3d-scene.mjs particle-star "My 3D Star" "#7c5cff"

执行成功后,终端会输出:

生成完成! 输出目录: /你的路径/skill-3d-web/output/3d-scene 启动方式: cd output/3d-scene && npx serve . 或 python3 -m http.server 8080

这时,output/3d-scene目录下会多出index.htmlmain.js两个文件,目录结构如下:

output/3d-scene/ ├── index.html └── main.js

这里要注意:因为main.js里使用import * as THREE from 'https://unpkg.com/...',属于 ES Module,直接用file://协议打开index.html会碰到跨域限制,必须在本地启动一个静态服务器来访问。

4.2 启动本地服务器

常用的启动方式有两种。

第一种,使用 Node.js 的npx serve

cd output/3d-scene npx serve .

第二种,使用 Python 自带的 HTTP 服务器:

cd output/3d-scene python3 -m http.server 8080

启动后,浏览器访问http://localhost:8080,就能看到 3D 粒子星空页面。移动鼠标时,画面会随着鼠标位置产生视角偏移;粒子本身会缓慢旋转,视觉上像是漂浮在深邃宇宙中的星云。

4.3 生成的网页代码解读

为了方便你在没有脚本的情况下也能理解页面逻辑,这里贴出main.js的核心片段并逐段解释。

场景、相机、渲染器初始化

const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050510); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement);

PerspectiveCamera是透视相机,第一个参数是视野角度75,值越大看到的范围越大;第二个参数是宽高比;第三、四个参数是近裁剪面和远裁剪面。antialias: true开启抗锯齿,避免粒子边缘出现明显锯齿。

生成粒子位置

const particleCount = 2600; const positions = new Float32Array(particleCount * 3); const velocities = new Float32Array(particleCount); for (let i = 0; i < particleCount; i++) { const radius = 2.5 + Math.random() * 3; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta); positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta) * 0.6; positions[i * 3 + 2] = radius * Math.cos(phi); velocities[i] = 0.005 + Math.random() * 0.01; }

这里把 2600 个粒子分布在半径2.55.5的球壳上。Math.acos(2 * Math.random() - 1)是生成球面上均匀分布点的标准写法,避免粒子聚集在球体两极。Y 轴方向乘以0.6是为了把球体在高度上压扁一点,让整体更接近星云的扁平形状。

动画循环

const time = performance.now() * 0.001; const pos = geometry.attributes.position.array; for (let i = 0; i < particleCount; i++) { const idx = i * 3; const angle = velocities[i] * time; const x = pos[idx]; const z = pos[idx + 2]; pos[idx] = x * Math.cos(angle) - z * Math.sin(angle); pos[idx + 2] = x * Math.sin(angle) + z * Math.cos(angle); } geometry.attributes.position.needsUpdate = true;

这里的time是当前时间(秒),每个粒子按自己的velocity围绕 Y 轴旋转。把xz先取出来,再用旋转公式写入新值,可以避免在计算过程中使用被覆盖过的数据。如果不设置needsUpdate = true,Three.js 不会重新上传顶点数据,画面就会静止不动。

鼠标交互与相机缓动

let mouseX = 0; let mouseY = 0; window.addEventListener('mousemove', (e) => { mouseX = (e.clientX / window.innerWidth) * 2 - 1; mouseY = -(e.clientY / window.innerHeight) * 2 + 1; }); camera.position.x += (mouseX * 1.2 - camera.position.x) * 0.05; camera.lookAt(0, 0, 0);

mouseXmouseY的范围是-11。每帧让相机位置向目标位置靠近5%,这是一种非常经典的缓动写法。如果直接让相机位置等于鼠标位置,画面会显得很生硬;用这种“每帧逼近一部分”的方式,移动会变得平滑自然,这也是很多 3D 交互页面的细节所在。

5. 常见问题与排查思路

在跑 3D 网页的过程中,大家容易在几个地方卡住。下面用表格整理一下高频问题:

问题现象常见原因解决思路
浏览器打开index.html后画面全黑使用 ES Module 加载外部 CDN,file://协议被浏览器拦截启动本地静态服务器,例如npx serve .python3 -m http.server 8080
控制台报错Failed to fetch dynamically imported moduleunpkg 地址无法访问,或网络环境不稳更换为cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js或其他可用镜像
粒子不运动,只显示静态球体忘记设置geometry.attributes.position.needsUpdate = true在每次修改Float32Array的顶点数据后,将needsUpdate置为true
鼠标移动时画面卡顿严重粒子数量过多,或使用低端设备时devicePixelRatio过高降低particleCount,或将renderer.setPixelRatio的值限制为1
页面在手机端显示不全没有适配窗口尺寸变化添加resize事件监听,更新相机宽高比和渲染器尺寸
场景中粒子颜色不是想要的PointsMaterialcolor设置为0xffffff时,粒子不会发光使用带透明度和AdditiveBlending的材质,如示例中的color: '#7c5cff'

如果遇到启动失败,还可以按下面的顺序排查:

  1. 确认当前目录是否正确。终端中的路径需要在output/3d-scene目录下。
  2. 确认 Node.js 版本。脚本使用的是 ESM 语法,Node 18 及以上一般没问题。
  3. 确认浏览器没有禁用本地服务器端口。如果8080被占用,换一个端口试试。
  4. 在浏览器控制台查看报错信息。大多数问题时控制台都会给出具体提示。

6. 最佳实践与工程建议

6.1 Skill 设计建议

Skill不只是把需求描述写长一点,而是要把“稳定产出”作为目标。这里有几个经验可以分享。

明确输出物清单。SKILL.md里写清楚“生成哪些文件、必须包含哪几块内容”,能有效避免 AI 在生成过程中偷懒或漏掉关键部分。比如本文就要求页面必须包含自动旋转、粒子系统、鼠标交互、窗口响应,少任何一个都不合格。

把可复用脚本和描述文件分离。如果生成逻辑很复杂,不要全都塞进SKILL.md。把核心生成器放在resources目录下,SKILL.md只描述“何时调用、怎么调用、结果格式是什么”。这样既方便调试,也方便其他 Skill 复用同一个生成器。

参数设计要收敛。生成脚本目前只接收三个参数:场景类型、标题、颜色。参数越少,越不容易出错。以后要扩展时,建议通过配置文件而不是不断增加命令行参数来管理。

6.2 Three.js 网页开发建议

优先使用 CDN 或构建工具。个人演示项目适合用 CDN 方式,零安装直接运行;但正式项目建议使用 Vite + Three.js,可以更好地处理模块加载、代码分割和资源压缩。

粒子数量不是越多越好。粒子数量从 1000 增加到 5000,视觉差异明显;但从 5000 增加到 10000,画面提升有限,性能却会明显下降。建议在开发时用devicePixelRatio和粒子数量做组合测试,找到当前设备的最佳平衡点。

动画中尽量复用对象。不要把BufferGeometryMaterialPoints放在动画循环里反复创建。正确做法是在初始化阶段创建一次,动画循环中只更新数据。

鼠标交互要加缓动。直接让相机或物体跟随鼠标位置,用户体验会比较僵硬。本文使用camera.position.x += (targetX - camera.position.x) * 0.05的方式,让移动更顺滑,这是 3D 交互页面的通用技巧。

6.3 工程与安全边界

使用Skill自动生成代码时,要注意以下边界:

  • 如果需要自动执行命令,务必确认命令不会对系统造成破坏。本文脚本只做文件写入和打印输出,没有删除操作。
  • 生成脚本的输出目录应该是相对路径,避免写入系统目录。
  • 如果要在生产环境部署,建议把生成代码纳入版本管理,并增加 Code Review 环节,而不是完全信任自动化生成的结果。
  • 涉及外部 CDN 资源时,建议记录版本号,避免 CDN 内容变化导致页面异常。

7. 总结与扩展方向

本文围绕“一个 Skill 做出 3D 特效网页”这个主题,完整拆解了从 Skill 概念、目录结构、SKILL.md编写、Node.js 生成脚本,到最终生成一个基于 Three.js 的粒子星空页面全过程。你现在应该能理解:Skill并不是某种黑魔法,它就是一套约定好的规则和工具,让 AI 助手在生成 3D 网页时不再“自由发挥”,而是严格按标准流程输出可运行的代码。

你可以继续在几个方向扩展:

  • 把生成脚本扩展出更多场景类型,比如 3D 动态文字、几何体波浪、全景图片浏览等。
  • 把输出从静态 HTML 升级为 Vite 工程,方便后续二次开发和部署。
  • SKILL.md中增加更多参数校验规则,让 AI 在生成之前先确认用户需求,减少无效输出。
  • 把多个 Skill 组合起来,比如用一个 Skill 做页面骨架,另一个 Skill 做 3D 特效模块,实现更复杂的活动页面。

如果按照本文步骤操作后成功跑出粒子星空,下一步可以试着把颜色改成你自己喜欢的渐变,或者把粒子数量调整到 5000 看看视觉差异。如果遇到报错,优先看第 5 节的排查表格,大部分问题都能在本地解决。希望这篇教程对你有帮助,也欢迎你在实际操作中改出更有意思的 3D 页面。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 1:48:24

中医舌诊AI数据集构建与YOLOv8目标检测实战指南

简介&#xff1a;本资源是面向中医智能诊断、医学图像分析及计算机视觉初学者的舌像目标检测专用数据集&#xff0c;聚焦舌头区域五类常见舌苔/舌质状态识别任务。压缩包共2000个文件&#xff0c;含800张高质量JPG舌像图、800份Pascal VOC格式XML标注&#xff08;含坐标与类别&…

作者头像 李华
网站建设 2026/9/3 1:47:10

神经网络解耦控制:让PID在多变量耦合系统中稳定发挥

简介&#xff1a;这是一份面向多变量系统控制方向的实用资源&#xff0c;整合了PID神经元网络解耦控制算法的完整MATLAB实现。压缩包共6个.m脚本文件&#xff0c;总大小13KB&#xff0c;覆盖系统建模、控制器构建、粒子群参数优化、解耦矩阵计算及仿真绘图等环节。已有三百二十…

作者头像 李华
网站建设 2026/9/3 1:46:37

Flet文件上传组件开发指南:从原理到实践

简介&#xff1a;本资源是一套基于Flet前端框架与FastAPI后端服务协同实现的文件上传系统模板&#xff0c;面向Python全栈初学者及轻量级Web应用开发者&#xff0c;解决前后端联调中文件上传、进度反馈与本地持久化保存的核心痛点。适用于文档管理、媒体库搭建、团队项目文件共…

作者头像 李华
网站建设 2026/9/3 1:45:57

ZYNQ 7020双核AMP开发实战:从内存规划到核间通信

简介&#xff1a;本资源是面向嵌入式开发工程师与ZYNQ平台学习者的双核AMP&#xff08;Asymmetric Multi-Processing&#xff09;驱动实战项目&#xff0c;聚焦ZYNQ 7020 SoC在Xilinx SDK环境下实现ARM Cortex-A9双核协同驱动开发&#xff0c;解决多核任务划分、中断隔离、核间…

作者头像 李华
网站建设 2026/9/3 1:44:47

Kotlin构建工具演进:从Amper到Toolchain的整合与迁移指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 1:44:40

PMSM无感控制:扩展卡尔曼滤波(EKF)原理、实现与调参实战

简介&#xff1a;本资源是一个面向电机控制算法工程师与高校电力电子方向研究生的永磁同步电机&#xff08;PMSM&#xff09;状态观测器实践模型&#xff0c;聚焦于非线性系统下转速、位置、反电动势及磁链等关键变量的实时估计问题。采用扩展卡尔曼滤波&#xff08;EKF&#x…

作者头像 李华