Gemini 3 粒子特效
·
在VS Code中安装Live Server插件后创建文件index.html和main.js文件
(内容由Gemini 3生成)
HTML 部分:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 粒子手势控制</title>
<style>
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: sans-serif;
overflow: hidden;
}
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
canvas {
display: block;
}
/* 隐藏用于手势识别的视频元素 */
#video-input {
display: none;
}
/* 全屏按钮 */
#fullscreen-btn {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 10px 15px;
cursor: pointer;
z-index: 100;
border-radius: 5px;
font-size: 14px;
}
.lil-gui {
z-index: 101 !important;
}
</style>
</head>
<body>
<video id="video-input"></video>
<div id="canvas-container"></div>
<button id="fullscreen-btn">全屏</button>
<script src="https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.19.1/dist/lil-gui.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.1/camera_utils.js" crossorigin="anonymous"></script>
<script src="main.js"></script>
</body>
</html>
JS部分:
// --- main.js 文件最顶部(在 window.addEventListener 之外)---
var modelData = {}; // 确保它是全局可访问的
// ...
window.addEventListener('load', () => {
// ...
// ... (所有其他代码)
// ...
});
// --- main.js (最终稳定版) ---
window.addEventListener('load', () => {
// --- 核心变量和状态 ---
let scene, camera, renderer;
let particles, particleMaterial;
const particleCount = 20000;
let modelData = {};
// 状态和手动控制变量
let controls = {
morphing: false,
handOpenness: 0.0,
useManualControl: false
};
// UI 设置
const settings = {
particleColor: '#00ffaa',
shape: 'heart',
};
// --- 调试:测试立方体 ---
function initTestCube() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 0, 0);
scene.add(cube);
}
// --- 粒子初始化 ---
function initParticles() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3).fill(0);
const targets = new Float32Array(particleCount * 3).fill(0);
const randoms = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
// 存储用于扩散的随机方向
randoms[i3 + 0] = (Math.random() - 0.5) * 2.0;
randoms[i3 + 1] = (Math.random() - 0.5) * 2.0;
randoms[i3 + 2] = (Math.random() - 0.5) * 2.0;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aTarget', new THREE.BufferAttribute(targets, 3));
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 3));
// 着色器材质
particleMaterial = new THREE.ShaderMaterial({
uniforms: {
uHandOpenness: { value: 0.0 },
uColor: { value: new THREE.Color(settings.particleColor) },
uMorphProgress: { value: 0.0 }
},
vertexShader: `
attribute vec3 aTarget;
attribute vec3 aRandom;
uniform float uHandOpenness;
uniform float uMorphProgress;
void main() {
vec3 morphedPosition = mix(position, aTarget, uMorphProgress);
float diffusion = uHandOpenness * 10.0;
vec3 finalPosition = morphedPosition + (aRandom * diffusion);
vec4 modelViewPosition = modelViewMatrix * vec4(finalPosition, 1.0);
gl_Position = projectionMatrix * modelViewPosition;
gl_PointSize = 3.0;
}
`,
fragmentShader: `
uniform vec3 uColor;
void main() {
float f = length(gl_PointCoord - vec2(0.5, 0.5));
if (f > 0.5) discard;
gl_FragColor = vec4(uColor, 1.0);
}
`,
blending: THREE.AdditiveBlending,
depthWrite: false,
transparent: true
});
particles = new THREE.Points(geometry, particleMaterial);
scene.add(particles);
}
// --- 模型数据生成 ---
// --- 替换 main.js 中的 generateModelData 函数 ---
function generateModelData() {
// --- 1. 爱心 (heart): 保持不变 ---
modelData.heart = new Float32Array(particleCount * 3);
const scale = 5;
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
const t = Math.random() * Math.PI * 2;
const x = scale * 1.6 * Math.pow(Math.sin(t), 3);
const y = scale * (1.3 * Math.cos(t) - 0.5 * Math.cos(2*t) - 0.2 * Math.cos(3*t) - 0.1 * Math.cos(4*t));
const z = (Math.random() - 0.5) * 0.5;
modelData.heart[i3 + 0] = x;
modelData.heart[i3 + 1] = y;
modelData.heart[i3 + 2] = z;
}
// --- 2. 花朵 (flower): 新增数据生成逻辑 ---
modelData.flower = new Float32Array(particleCount * 3);
const flowerScale = 6;
const petalCount = 8; // 花瓣数量
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
// 使用球坐标生成点
const r = Math.random() * 0.7 + 0.3; // 半径
const theta = Math.random() * Math.PI * 2; // 角度 (0到360度)
const phi = Math.random() * Math.PI; // 角度 (0到180度)
// 生成一个标准球体上的点
let x = r * Math.sin(phi) * Math.cos(theta);
let y = r * Math.sin(phi) * Math.sin(theta);
let z = r * Math.cos(phi);
// 引入花瓣形状的扭曲 (根据theta角度周期性变化)
const petalFactor = (1.0 + Math.cos(theta * petalCount)) * 0.5;
// 在径向上扭曲点,形成花瓣
x *= (petalFactor * 0.6 + 0.4);
y *= (petalFactor * 0.6 + 0.4);
// 赋予花朵模型数据
modelData.flower[i3 + 0] = x * flowerScale;
modelData.flower[i3 + 1] = y * flowerScale;
modelData.flower[i3 + 2] = z * flowerScale * 0.5; // 压扁一点
}
// --- 3. 其他占位模型 (saturn, buddha) ---
modelData.saturn = new Float32Array(particleCount * 3).fill(0);
modelData.buddha = new Float32Array(particleCount * 3).fill(0);
}
// --- 模型切换函数 ---
function switchModel(modelName, immediate = false) {
if (modelName === 'fireworks') {
console.warn("此版本未实现烟花物理着色器。");
return;
}
const newTargets = modelData[modelName] || new Float32Array(particleCount * 3).fill(0);
const geom = particles.geometry;
// 将当前的目标 (aTarget) 复制到起始位置 (position)
geom.attributes.position.copy(geom.attributes.aTarget);
geom.attributes.position.needsUpdate = true;
// 设置新的目标位置
geom.setAttribute('aTarget', new THREE.BufferAttribute(newTargets, 3));
geom.attributes.aTarget.needsUpdate = true;
if (immediate) {
particleMaterial.uniforms.uMorphProgress.value = 1.0;
controls.morphing = false;
} else {
particleMaterial.uniforms.uMorphProgress.value = 0.0;
controls.morphing = true;
}
}
// --- MediaPipe 结果回调 ---
function onHandResults(results) {
if (controls.useManualControl) return;
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
const wrist = landmarks[0];
const fingerTips = [landmarks[4], landmarks[8], landmarks[12], landmarks[16], landmarks[20]];
let totalDistance = 0;
for (const tip of fingerTips) {
const dx = tip.x - wrist.x;
const dy = tip.y - wrist.y;
totalDistance += Math.sqrt(dx*dx + dy*dy);
}
const avgDistance = totalDistance / fingerTips.length;
const minOpen = 0.15;
const maxOpen = 0.4;
let openness = (avgDistance - minOpen) / (maxOpen - minOpen);
controls.handOpenness = Math.max(0.0, Math.min(1.0, openness));
} else {
// 没有检测到手,缓慢归零
controls.handOpenness = controls.handOpenness * 0.95;
}
}
// --- 初始化 MediaPipe (强化错误捕获) ---
function initMediaPipe() {
try {
const videoElement = document.getElementById('video-input');
if (typeof Hands === 'undefined' || typeof Camera === 'undefined') {
console.error("MediaPipe 库 (Hands/Camera) 未加载,请检查 index.html 中的 CDN 链接。");
alert("MediaPipe 库加载失败!请尝试使用手动控制。");
controls.useManualControl = true;
return;
}
const hands = new Hands({
// 使用官方推荐的 locateFile 路径
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/${file}`
});
hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.5 });
hands.onResults(onHandResults);
const mpCamera = new Camera(videoElement, {
onFrame: async () => { await hands.send({ image: videoElement }); },
width: 640, height: 480
});
mpCamera.start();
console.log("MediaPipe 尝试启动摄像头成功。");
} catch (e) {
console.error("MediaPipe 启动时发生致命错误,可能原因:摄像头权限拒绝或 CDN 链接错误。", e);
alert("摄像头启动失败!已自动切换到手动控制模式。");
controls.useManualControl = true;
}
}
// --- UI 控制 ---
function initGUI() {
const gui = new lil.GUI();
gui.addColor(settings, 'particleColor')
.name('粒子颜色')
.onChange((value) => { particleMaterial.uniforms.uColor.value.set(value); });
// 修复 ReferenceError: switchModel is not defined
gui.add(settings, 'shape', ['heart', 'saturn', 'buddha', 'flower', 'fireworks'])
.name('选择模型')
.onChange(switchModel); // 直接引用函数名
const handFolder = gui.addFolder('手势控制 (调试)');
handFolder.add(controls, 'useManualControl')
.name('手动模式 (开/关)');
handFolder.add(controls, 'handOpenness', 0.0, 1.0, 0.01)
.name('手动张合度')
.step(0.01)
.listen(); // 监听值,即使摄像头在运行时也能看到变化
handFolder.open();
}
// --- 窗口大小调整 ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- 全屏控制 ---
function initFullscreen() {
const fullscreenBtn = document.getElementById('fullscreen-btn');
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});
}
// --- 场景主初始化函数 ---
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
const container = document.getElementById('canvas-container');
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
// 初始化顺序:先几何,后工具
initTestCube();
initParticles();
generateModelData();
initGUI();
initFullscreen(); // 修复 ReferenceError: initFullscreen is not defined
initMediaPipe();
switchModel(settings.shape, true);
window.addEventListener('resize', onWindowResize);
animate();
}
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
// 确定用于扩散的最终值
const particleDiffusionValue = controls.useManualControl
? controls.handOpenness
: controls.handOpenness;
// 实时平滑更新 uniform
const currentOpenness = particleMaterial.uniforms.uHandOpenness.value;
particleMaterial.uniforms.uHandOpenness.value +=
(particleDiffusionValue - currentOpenness) * 0.15;
// 旋转
const testCube = scene.children.find(child => child.isMesh);
if(testCube) { testCube.rotation.y += 0.01; }
particles.rotation.y += 0.0005;
renderer.render(scene, camera);
}
// --- 启动! ---
init();
});
更多推荐
所有评论(0)