使用说明(文档最下方附个人演示视频)<使用此文档源码可达到视频效果,如需个人定制需组织提示词不断喂给AI校正优化>
-
保存代码:将下方的代码保存为
index.html。 -
运行环境:由于涉及摄像头权限,浏览器通常要求在 HTTPS 或 Localhost 环境下运行。
-
如果你有 VS Code,安装 "Live Server" 插件,右键
index.html-> "Open with Live Server"。 -
或者使用 Python:
python -m http.server。
-
-
授权:打开页面后允许摄像头权限。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebAR 圣诞树 - 交互修复版</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Consolas', monospace; }
#canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#video-input { display: none; } /* 隐藏原始视频 */
/* 调试面板 */
#debug-panel {
position: absolute; top: 10px; left: 10px; z-index: 20;
background: rgba(0, 20, 0, 0.8); border: 1px solid #00ff00;
color: #00ff00; padding: 10px; font-size: 12px;
pointer-events: none; width: 220px;
}
.bar-container { display: flex; align-items: center; margin-bottom: 4px; }
.bar-label { width: 60px; }
.bar-value { color: #fff; font-weight: bold; }
/* 引导提示 */
#instruction {
position: absolute; bottom: 20px; width: 100%; text-align: center;
color: rgba(255, 255, 255, 0.5); font-size: 14px; z-index: 10;
pointer-events: none; text-shadow: 0 0 5px #000;
}
#loading {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: #d4af37; font-size: 20px; z-index: 30; text-shadow: 0 0 10px #d4af37;
}
</style>
<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
<script src="https://unpkg.com/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="debug-panel">
<div style="border-bottom:1px solid #00ff00; margin-bottom:5px; padding-bottom:5px;">系统状态监视器</div>
<div class="bar-container"><span class="bar-label">手部检测:</span> <span id="debug-detected" class="bar-value">NO</span></div>
<div class="bar-container"><span class="bar-label">当前手势:</span> <span id="debug-gesture" class="bar-value" style="color:yellow">等待...</span></div>
<div class="bar-container"><span class="bar-label">控制旋转:</span> <span id="debug-rotate" class="bar-value">0.0</span></div>
<hr style="border-color: #004400;">
<div>手指状态 (1=直, 0=弯):</div>
<div id="debug-fingers">👍[?] ☝️[?] 🖕[?] 💍[?] 🤙[?]</div>
</div>
<div id="loading">正在装载 25,000 个光子...<br>请授予摄像头权限</div>
<div id="instruction">✊握拳:凝聚圣诞树 | 🖐张开:散开星云 | ↔️左右移动手:旋转场景</div>
<video id="video-input"></video>
<div id="canvas-container"></div>
<script type="x-shader/x-vertex" id="vertexshader">
attribute float size;
attribute vec3 customColor;
attribute float blinkOffset; // 闪烁偏移量
varying vec3 vColor;
varying float vBlink;
uniform float uTime;
uniform float uPixelRatio;
void main() {
vColor = customColor;
// 计算闪烁因子:基于时间的 Sin 波
vBlink = 0.8 + 0.4 * sin(uTime * 3.0 + blinkOffset);
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
// 距离衰减公式:离得越远粒子越小
gl_PointSize = size * uPixelRatio * ( 200.0 / -mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
varying vec3 vColor;
varying float vBlink;
void main() {
// 创建圆形柔化粒子
vec2 xy = gl_PointCoord.xy - vec2(0.5);
float dist = length(xy);
if ( dist > 0.5 ) discard;
// 核心高亮,边缘柔和
float glow = 1.0 - smoothstep(0.1, 0.5, dist);
// 混合颜色、发光强度和闪烁
gl_FragColor = vec4( vColor * 1.5, glow * vBlink );
}
</script>
<script>
// --- 参数配置 ---
const CONFIG = {
count: 25000,
colors: {
leaf: [0x0f2e18, 0x1b4020, 0x2f5e36], // 深浅不一的绿色
ornament: [0xff0000, 0xd4af37, 0xffffff, 0x00ccff] // 红金白蓝装饰
},
camZ: 60
};
let scene, camera, renderer, material;
let particles, positions, targetPositions, currentPositions;
let time = 0;
// 交互状态
let interaction = {
hasHand: false,
gesture: 'cloud', // 'tree' or 'cloud'
rotSpeed: 0
};
init();
initMediaPipe();
function init() {
// 1. 场景
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000500, 0.01); // 墨绿色深邃背景
camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = CONFIG.camZ;
camera.position.y = 10;
camera.lookAt(0, 10, 0);
// 2. 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 3. 粒子系统初始化
initParticles();
// 4. 事件
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
}
function initParticles() {
const geometry = new THREE.BufferGeometry();
positions = new Float32Array(CONFIG.count * 3);
const colors = new Float32Array(CONFIG.count * 3);
const sizes = new Float32Array(CONFIG.count);
const blinkOffsets = new Float32Array(CONFIG.count);
currentPositions = [];
targetPositions = [];
for (let i = 0; i < CONFIG.count; i++) {
// 初始形态:散乱的云
const x = (Math.random() - 0.5) * 100;
const y = (Math.random() - 0.5) * 60 + 10;
const z = (Math.random() - 0.5) * 80;
positions[i*3] = x;
positions[i*3+1] = y;
positions[i*3+2] = z;
currentPositions.push(new THREE.Vector3(x, y, z));
targetPositions.push(new THREE.Vector3(x, y, z)); // 初始目标也是云
// 颜色分配逻辑:80%是树叶,20%是彩灯
const isOrnament = Math.random() > 0.85;
const colorHex = isOrnament
? CONFIG.colors.ornament[Math.floor(Math.random() * CONFIG.colors.ornament.length)]
: CONFIG.colors.leaf[Math.floor(Math.random() * CONFIG.colors.leaf.length)];
const color = new THREE.Color(colorHex);
colors[i*3] = color.r;
colors[i*3+1] = color.g;
colors[i*3+2] = color.b;
// 尺寸:彩灯大一点,树叶小一点
sizes[i] = isOrnament ? (1.5 + Math.random()) : (0.6 + Math.random() * 0.4);
blinkOffsets[i] = Math.random() * 10;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('blinkOffset', new THREE.BufferAttribute(blinkOffsets, 1));
material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uPixelRatio: { value: window.devicePixelRatio }
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
calculateShape('cloud'); // 默认形态
}
// --- 核心算法:生成逼真的分层圣诞树 ---
function calculateShape(type) {
if (type === 'tree') {
const layers = 18; // 树的层数
const treeHeight = 55;
const baseRadius = 25;
for (let i = 0; i < CONFIG.count; i++) {
// 将粒子分配到各个层级
// 使用 pow 使得上方层级粒子少,下方多
const layerIndex = Math.floor(Math.pow(Math.random(), 0.8) * layers);
const layerRatio = layerIndex / layers; // 0(顶) -> 1(底)
// 每层的中心高度
const layerY = (1 - layerRatio) * treeHeight - 15;
// 该层的半径 (线性扩大)
const layerMaxR = layerRatio * baseRadius;
// 在层内分布:倾向于外圈(树枝末端)但也有内部填充
const rRatio = Math.sqrt(Math.random());
const r = rRatio * layerMaxR;
const theta = Math.random() * Math.PI * 2;
// 核心:枝叶下垂效果 (Gravity Droop)
// 离中心越远,下垂越厉害
const droop = rRatio * rRatio * 4.0;
targetPositions[i].set(
r * Math.cos(theta),
layerY - droop, // 高度受下垂影响
r * Math.sin(theta)
);
}
} else {
// 散开的星云
for (let i = 0; i < CONFIG.count; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 20 + Math.random() * 40;
const y = (Math.random() - 0.5) * 50 + 10;
targetPositions[i].set(
Math.cos(angle) * r,
y,
Math.sin(angle) * r
);
}
}
}
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
time += 0.015;
material.uniforms.uTime.value = time;
// 1. 场景旋转控制
if (interaction.hasHand) {
scene.rotation.y += interaction.rotSpeed;
} else {
scene.rotation.y += 0.002; // 无人时自动慢转
}
// 2. 粒子物理 (平滑插值)
const positions = particles.geometry.attributes.position.array;
for (let i = 0; i < CONFIG.count; i++) {
const current = currentPositions[i];
const target = targetPositions[i];
// 简单的 Ease-out 插值,不用复杂的弹簧以保证性能和形状稳定性
current.x += (target.x - current.x) * 0.06;
current.y += (target.y - current.y) * 0.06;
current.z += (target.z - current.z) * 0.06;
positions[i*3] = current.x;
positions[i*3+1] = current.y;
positions[i*3+2] = current.z;
}
particles.geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
// --- MediaPipe 逻辑 (修复手势判定) ---
function initMediaPipe() {
const videoElement = document.getElementById('video-input');
const hands = new Hands({locateFile: (file) => `https://unpkg.com/@mediapipe/hands/${file}`});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.6, // 稍微降低以提高检出率
minTrackingConfidence: 0.6
});
hands.onResults(onHandsResults);
const cameraUtils = new Camera(videoElement, {
onFrame: async () => { await hands.send({image: videoElement}); },
width: 640, height: 480
});
cameraUtils.start();
}
function onHandsResults(results) {
document.getElementById('loading').style.display = 'none';
const debugDetected = document.getElementById('debug-detected');
const debugGesture = document.getElementById('debug-gesture');
const debugRotate = document.getElementById('debug-rotate');
const debugFingers = document.getElementById('debug-fingers');
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
interaction.hasHand = true;
debugDetected.innerText = "YES";
debugDetected.style.color = "#00ff00";
const lm = results.multiHandLandmarks[0];
const palmX = (lm[0].x + lm[9].x) / 2; // 手掌中心 X 坐标
// --- 修复1: 移动逻辑 ---
// 0 (左) -- 0.5 (中) -- 1.0 (右)
// 映射到旋转速度:左边转,右边转,中间停
const deadZone = 0.15; // 中心死区
let rot = 0;
if (palmX < 0.5 - deadZone) rot = -0.02 * (0.5 - deadZone - palmX) * 10;
if (palmX > 0.5 + deadZone) rot = 0.02 * (palmX - 0.5 - deadZone) * 10;
interaction.rotSpeed = rot;
debugRotate.innerText = rot.toFixed(3);
// --- 修复2: 手势判定逻辑 (基于指尖与指关节距离) ---
const wrist = lm[0];
// 判断手指是否伸直:比较 指尖(Tip)到手腕距离 vs 指关节(PIP)到手腕距离
// 如果 Tip 远大于 PIP,说明伸直了。如果差不多或更小,说明弯曲了。
function isStraight(tipIdx, pipIdx) {
const dTip = Math.hypot(lm[tipIdx].x - wrist.x, lm[tipIdx].y - wrist.y);
const dPip = Math.hypot(lm[pipIdx].x - wrist.x, lm[pipIdx].y - wrist.y);
return dTip > dPip * 1.2; // 1.2 系数容错
}
const f_thumb = isStraight(4, 2);
const f_index = isStraight(8, 6);
const f_middle = isStraight(12, 10);
const f_ring = isStraight(16, 14);
const f_pinky = isStraight(20, 18);
// 更新 UI 状态
const statusStr = (f) => f ? "1" : "0";
debugFingers.innerText = `👍[${statusStr(f_thumb)}] ☝️[${statusStr(f_index)}] 🖕[${statusStr(f_middle)}] 💍[${statusStr(f_ring)}] 🤙[${statusStr(f_pinky)}]`;
// 计算伸直的手指数量 (不含大拇指,因为大拇指有时很难判断)
const extendedCount = (f_index?1:0) + (f_middle?1:0) + (f_ring?1:0) + (f_pinky?1:0);
// --- 状态机切换 ---
if (extendedCount <= 1) {
// 0或1根手指伸直 -> 判定为握拳 (容错率高)
if (interaction.gesture !== 'tree') {
interaction.gesture = 'tree';
calculateShape('tree');
debugGesture.innerText = "握拳 (凝聚)";
debugGesture.style.color = "#00ffff";
}
} else if (extendedCount >= 3) {
// 3根以上手指伸直 -> 判定为张开
if (interaction.gesture !== 'cloud') {
interaction.gesture = 'cloud';
calculateShape('cloud');
debugGesture.innerText = "张开 (散开)";
debugGesture.style.color = "#ff00ff";
}
}
} else {
interaction.hasHand = false;
debugDetected.innerText = "NO";
debugDetected.style.color = "red";
debugGesture.innerText = "---";
interaction.rotSpeed = 0;
}
}
</script>
</body>
</html>


评论