|
|
@@ -80,6 +80,20 @@ const AiorzWebARLanding = () => {
|
|
|
|
|
|
const shapesRef = useRef<Record<string, Float32Array>>({});
|
|
|
|
|
|
+ // Meteor System Refs
|
|
|
+ const meteorsRef = useRef<{
|
|
|
+ meteors: Array<{
|
|
|
+ head: any; // 头部点
|
|
|
+ tail: any; // 尾巴线条
|
|
|
+ headPos: { x: number; y: number; z: number };
|
|
|
+ velocity: { x: number; y: number; z: number };
|
|
|
+ tailPoints: Array<{ x: number; y: number; z: number }>;
|
|
|
+ startDelay: number; // 延迟启动
|
|
|
+ }>;
|
|
|
+ count: number;
|
|
|
+ frameCount?: number; // 帧计数器
|
|
|
+ } | null>(null);
|
|
|
+
|
|
|
// Configuration
|
|
|
const CONFIG = {
|
|
|
particleCount: 16000,
|
|
|
@@ -154,8 +168,11 @@ const AiorzWebARLanding = () => {
|
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
rendererRef.current = renderer;
|
|
|
|
|
|
+ console.log('[ThreeJS] Initialized, creating particles and meteors');
|
|
|
createParticles(THREE);
|
|
|
initShapes(THREE);
|
|
|
+ createMeteors(THREE);
|
|
|
+ console.log('[ThreeJS] All systems initialized');
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
@@ -227,6 +244,194 @@ const AiorzWebARLanding = () => {
|
|
|
particlesDataRef.current = { positions, velocities, colors, targetPositions, targetColors };
|
|
|
};
|
|
|
|
|
|
+ // ==================== 2.5. Meteor System ====================
|
|
|
+ const createMeteors = (THREE: any) => {
|
|
|
+ console.log('[Meteor] createMeteors called');
|
|
|
+ const meteorCount = 250; // 大量流星数量
|
|
|
+
|
|
|
+ const camera = cameraRef.current;
|
|
|
+ if (!camera) {
|
|
|
+ console.error('[Meteor] Camera not initialized!');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const distance = 15;
|
|
|
+ const fov = 75;
|
|
|
+ const aspect = window.innerWidth / window.innerHeight;
|
|
|
+ const vFov = (fov * Math.PI) / 180;
|
|
|
+ const heightAtDistance = 2 * Math.tan(vFov / 2) * distance;
|
|
|
+ const widthAtDistance = heightAtDistance * aspect;
|
|
|
+
|
|
|
+ const baseAngle = Math.PI / 4; // 45度角
|
|
|
+ const baseSpeed = 0.01; // 缓慢的星际速度(减半)
|
|
|
+ const tailLength = 20; // 尾巴长度(点数)
|
|
|
+
|
|
|
+ const meteors: Array<{
|
|
|
+ head: any;
|
|
|
+ tail: any;
|
|
|
+ headPos: { x: number; y: number; z: number };
|
|
|
+ velocity: { x: number; y: number; z: number };
|
|
|
+ tailPoints: Array<{ x: number; y: number; z: number }>;
|
|
|
+ startDelay: number; // 延迟启动,让流星错开出现
|
|
|
+ }> = [];
|
|
|
+
|
|
|
+ // 创建头部发光纹理(参考 liuxing.html:纯白光点带阴影效果)
|
|
|
+ const headCanvas = document.createElement('canvas');
|
|
|
+ headCanvas.width = 32;
|
|
|
+ headCanvas.height = 32;
|
|
|
+ const headCtx = headCanvas.getContext('2d');
|
|
|
+ if (headCtx) {
|
|
|
+ // 参考 liuxing.html:头部是纯白光点
|
|
|
+ const grad = headCtx.createRadialGradient(16, 16, 0, 16, 16, 16);
|
|
|
+ grad.addColorStop(0, 'rgba(255, 255, 255, 1)'); // 纯白核心(最亮)
|
|
|
+ grad.addColorStop(0.3, 'rgba(255, 255, 255, 0.8)'); // 白色光晕
|
|
|
+ grad.addColorStop(0.6, 'rgba(173, 216, 230, 0.4)'); // 中段带点蓝(参考 liuxing.html)
|
|
|
+ grad.addColorStop(1, 'rgba(255, 255, 255, 0)'); // 透明边缘
|
|
|
+ headCtx.fillStyle = grad;
|
|
|
+ headCtx.fillRect(0, 0, 32, 32);
|
|
|
+ }
|
|
|
+ const headTexture = new THREE.Texture(headCanvas);
|
|
|
+ headTexture.needsUpdate = true;
|
|
|
+
|
|
|
+ // 为每个流星创建头部和尾巴
|
|
|
+ for (let m = 0; m < meteorCount; m++) {
|
|
|
+ // 起始位置:从屏幕右上角外开始,分散在不同位置
|
|
|
+ // 让流星从屏幕外开始,这样它们会陆续进入视野
|
|
|
+ // 调整位置范围,确保流星在可见区域内或附近
|
|
|
+ const startX = widthAtDistance * (0.3 + Math.random() * 1.2); // 从屏幕内到屏幕外
|
|
|
+ const startY = heightAtDistance * (-0.2 + Math.random() * 0.8); // 从屏幕上方外到屏幕内
|
|
|
+ const startZ = distance - 2 - Math.random() * 6; // 更靠近相机
|
|
|
+
|
|
|
+ // 速度(略有变化)
|
|
|
+ const speedVariation = 0.8 + Math.random() * 0.4;
|
|
|
+ const velX = -Math.cos(baseAngle) * baseSpeed * speedVariation;
|
|
|
+ const velY = -Math.sin(baseAngle) * baseSpeed * speedVariation;
|
|
|
+ const velZ = 0;
|
|
|
+
|
|
|
+ // 延迟启动,让流星错开出现(大部分立即显示,少数延迟)
|
|
|
+ const startDelay = m < 50 ? 0 : Math.random() * 10; // 前50个立即显示,其他的短延迟
|
|
|
+
|
|
|
+ // 创建头部点
|
|
|
+ const headGeometry = new THREE.BufferGeometry();
|
|
|
+ const headPositions = new Float32Array([startX, startY, startZ]);
|
|
|
+ headGeometry.setAttribute('position', new THREE.BufferAttribute(headPositions, 3));
|
|
|
+
|
|
|
+ const headMaterial = new THREE.PointsMaterial({
|
|
|
+ size: 3.0, // 参考 liuxing.html:头部光点较小(1.5像素半径,这里用3.0)
|
|
|
+ map: headTexture,
|
|
|
+ transparent: true,
|
|
|
+ opacity: 1.0,
|
|
|
+ blending: THREE.AdditiveBlending,
|
|
|
+ depthWrite: false,
|
|
|
+ color: new THREE.Color(1, 1, 1) // 纯白色(参考 liuxing.html)
|
|
|
+ });
|
|
|
+
|
|
|
+ const head = new THREE.Points(headGeometry, headMaterial);
|
|
|
+
|
|
|
+ // 创建尾巴线条(使用渐变颜色)
|
|
|
+ // 倒着飞:尾巴在前面(运动方向的前方),头部在后面
|
|
|
+ const tailPoints: Array<{ x: number; y: number; z: number }> = [];
|
|
|
+ for (let t = 0; t < tailLength; t++) {
|
|
|
+ const tailOffset = t * 0.5; // 增加尾巴点间距,让尾巴更长
|
|
|
+ // 尾巴在运动方向的前方(向左下方向)
|
|
|
+ tailPoints.push({
|
|
|
+ x: startX - Math.cos(baseAngle) * tailOffset, // 向左下方向延伸
|
|
|
+ y: startY - Math.sin(baseAngle) * tailOffset,
|
|
|
+ z: startZ
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建尾巴线条(使用渐变颜色)
|
|
|
+ const tailGeometry = new THREE.BufferGeometry();
|
|
|
+ const tailPositions = new Float32Array(tailLength * 3);
|
|
|
+ const tailColors = new Float32Array(tailLength * 3);
|
|
|
+
|
|
|
+ for (let t = 0; t < tailLength; t++) {
|
|
|
+ const idx = t * 3;
|
|
|
+ tailPositions[idx] = tailPoints[t].x;
|
|
|
+ tailPositions[idx + 1] = tailPoints[t].y;
|
|
|
+ tailPositions[idx + 2] = tailPoints[t].z;
|
|
|
+
|
|
|
+ // 颜色渐变:倒着飞,尾部在前面(最亮),头部在后面(较暗)
|
|
|
+ // 反转渐变:从尾部(t=0,最亮)到头部(t=tailLength-1,较暗)
|
|
|
+ const progress = t / (tailLength - 1);
|
|
|
+ const reverseProgress = 1.0 - progress; // 反转进度:尾部是0,头部是1
|
|
|
+ let r, g, b, alpha;
|
|
|
+
|
|
|
+ if (reverseProgress <= 0.1) {
|
|
|
+ // 尾部区域(倒着飞时在前面):纯白,高不透明度
|
|
|
+ const localProgress = reverseProgress / 0.1;
|
|
|
+ alpha = 1.0 - localProgress * 0.2; // 1.0 到 0.8
|
|
|
+ r = 1.0;
|
|
|
+ g = 1.0;
|
|
|
+ b = 1.0;
|
|
|
+ } else if (reverseProgress <= 0.4) {
|
|
|
+ // 中段:从白色过渡到淡蓝色
|
|
|
+ const localProgress = (reverseProgress - 0.1) / 0.3;
|
|
|
+ alpha = 0.8 - localProgress * 0.4; // 0.8 到 0.4
|
|
|
+ r = 1.0 - localProgress * 0.25; // 1.0 到 0.75 (173/255)
|
|
|
+ g = 1.0 - localProgress * 0.15; // 1.0 到 0.85 (216/255)
|
|
|
+ b = 1.0 - localProgress * 0.1; // 1.0 到 0.9 (230/255)
|
|
|
+ } else {
|
|
|
+ // 头部(倒着飞时在后面):快速变透明
|
|
|
+ const localProgress = (reverseProgress - 0.4) / 0.6;
|
|
|
+ alpha = 0.4 * (1.0 - localProgress); // 0.4 到 0
|
|
|
+ r = 0.75 + localProgress * 0.25; // 保持淡蓝白色
|
|
|
+ g = 0.85 + localProgress * 0.15;
|
|
|
+ b = 0.9 + localProgress * 0.1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 alpha 应用到 RGB(因为 vertexColors 不支持单独的 alpha)
|
|
|
+ r = r * alpha;
|
|
|
+ g = g * alpha;
|
|
|
+ b = b * alpha;
|
|
|
+
|
|
|
+ tailColors[idx] = r;
|
|
|
+ tailColors[idx + 1] = g;
|
|
|
+ tailColors[idx + 2] = b;
|
|
|
+ }
|
|
|
+
|
|
|
+ tailGeometry.setAttribute('position', new THREE.BufferAttribute(tailPositions, 3));
|
|
|
+ tailGeometry.setAttribute('color', new THREE.BufferAttribute(tailColors, 3));
|
|
|
+
|
|
|
+ const tailMaterial = new THREE.LineBasicMaterial({
|
|
|
+ vertexColors: true,
|
|
|
+ transparent: true,
|
|
|
+ opacity: 1.0, // 参考 liuxing.html:使用 vertexColors 控制透明度
|
|
|
+ blending: THREE.AdditiveBlending,
|
|
|
+ linewidth: 2, // 参考 liuxing.html:lineWidth = 2
|
|
|
+ depthWrite: false
|
|
|
+ });
|
|
|
+
|
|
|
+ const tail = new THREE.Line(tailGeometry, tailMaterial);
|
|
|
+
|
|
|
+ // 根据延迟决定是否立即显示
|
|
|
+ head.visible = startDelay === 0;
|
|
|
+ tail.visible = startDelay === 0;
|
|
|
+
|
|
|
+ sceneRef.current.add(head);
|
|
|
+ sceneRef.current.add(tail);
|
|
|
+
|
|
|
+ meteors.push({
|
|
|
+ head,
|
|
|
+ tail,
|
|
|
+ headPos: { x: startX, y: startY, z: startZ },
|
|
|
+ velocity: { x: velX, y: velY, z: velZ },
|
|
|
+ tailPoints,
|
|
|
+ startDelay
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[Meteor] Created', meteorCount, 'meteors with heads and tails');
|
|
|
+
|
|
|
+ meteorsRef.current = {
|
|
|
+ meteors,
|
|
|
+ count: meteorCount
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('[Meteor] Meteor system initialized successfully');
|
|
|
+ };
|
|
|
+
|
|
|
// ==================== 3. Shape Generation ====================
|
|
|
const initShapes = (THREE: any) => {
|
|
|
const count = CONFIG.particleCount;
|
|
|
@@ -554,6 +759,139 @@ const AiorzWebARLanding = () => {
|
|
|
particles.geometry.attributes.color.needsUpdate = true;
|
|
|
particles.rotation.y += 0.001;
|
|
|
|
|
|
+ // Update meteors - 恒定流星动画(带尾巴)
|
|
|
+ if (meteorsRef.current) {
|
|
|
+ const { meteors, count } = meteorsRef.current;
|
|
|
+
|
|
|
+ // 计算可见区域(3D 空间)
|
|
|
+ const distance = 15;
|
|
|
+ const fov = 75;
|
|
|
+ const aspect = window.innerWidth / window.innerHeight;
|
|
|
+ const vFov = (fov * Math.PI) / 180;
|
|
|
+ const heightAtDistance = 2 * Math.tan(vFov / 2) * distance;
|
|
|
+ const widthAtDistance = heightAtDistance * aspect;
|
|
|
+
|
|
|
+ const baseAngle = Math.PI / 4; // 45度角
|
|
|
+ const tailLength = 20;
|
|
|
+
|
|
|
+ // 使用帧计数器来跟踪延迟
|
|
|
+ if (!meteorsRef.current.frameCount) {
|
|
|
+ meteorsRef.current.frameCount = 0;
|
|
|
+ }
|
|
|
+ meteorsRef.current.frameCount++;
|
|
|
+
|
|
|
+ for (let m = 0; m < count; m++) {
|
|
|
+ const meteor = meteors[m];
|
|
|
+
|
|
|
+ // 检查是否到了启动时间
|
|
|
+ if (meteorsRef.current.frameCount < meteor.startDelay) {
|
|
|
+ continue; // 还没到启动时间,跳过
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启动流星(显示)
|
|
|
+ if (!meteor.head.visible) {
|
|
|
+ meteor.head.visible = true;
|
|
|
+ meteor.tail.visible = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const vel = meteor.velocity;
|
|
|
+
|
|
|
+ // 更新头部位置
|
|
|
+ meteor.headPos.x += vel.x;
|
|
|
+ meteor.headPos.y += vel.y;
|
|
|
+ meteor.headPos.z += vel.z;
|
|
|
+
|
|
|
+ // 更新头部点位置(倒着飞:头部在后面)
|
|
|
+ const headPositions = meteor.head.geometry.attributes.position;
|
|
|
+ // 头部在运动方向的后方
|
|
|
+ headPositions.array[0] = meteor.headPos.x + Math.cos(baseAngle + Math.PI) * 2.0; // 头部在后面
|
|
|
+ headPositions.array[1] = meteor.headPos.y + Math.sin(baseAngle + Math.PI) * 2.0;
|
|
|
+ headPositions.array[2] = meteor.headPos.z;
|
|
|
+ headPositions.needsUpdate = true;
|
|
|
+
|
|
|
+ // 更新尾巴:倒着飞,尾巴在前面(运动方向的前方)
|
|
|
+ const tailPositions = meteor.tail.geometry.attributes.position;
|
|
|
+ const tailColors = meteor.tail.geometry.attributes.color;
|
|
|
+
|
|
|
+ for (let t = 0; t < tailLength; t++) {
|
|
|
+ const idx = t * 3;
|
|
|
+ const tailOffset = t * 0.5; // 增加尾巴间距
|
|
|
+
|
|
|
+ // 尾巴点位置:在运动方向的前方(向左下方向延伸)
|
|
|
+ tailPositions.array[idx] = meteor.headPos.x - Math.cos(baseAngle) * tailOffset;
|
|
|
+ tailPositions.array[idx + 1] = meteor.headPos.y - Math.sin(baseAngle) * tailOffset;
|
|
|
+ tailPositions.array[idx + 2] = meteor.headPos.z;
|
|
|
+
|
|
|
+ // 更新尾巴颜色渐变(倒着飞:尾部在前面最亮,头部在后面较暗)
|
|
|
+ const progress = t / (tailLength - 1);
|
|
|
+ const reverseProgress = 1.0 - progress; // 反转进度
|
|
|
+ let r, g, b, alpha;
|
|
|
+
|
|
|
+ if (reverseProgress <= 0.1) {
|
|
|
+ // 尾部区域(倒着飞时在前面):纯白,高不透明度
|
|
|
+ const localProgress = reverseProgress / 0.1;
|
|
|
+ alpha = 1.0 - localProgress * 0.2; // 1.0 到 0.8
|
|
|
+ r = 1.0;
|
|
|
+ g = 1.0;
|
|
|
+ b = 1.0;
|
|
|
+ } else if (reverseProgress <= 0.4) {
|
|
|
+ // 中段:从白色过渡到淡蓝色
|
|
|
+ const localProgress = (reverseProgress - 0.1) / 0.3;
|
|
|
+ alpha = 0.8 - localProgress * 0.4; // 0.8 到 0.4
|
|
|
+ r = 1.0 - localProgress * 0.25; // 1.0 到 0.75 (173/255)
|
|
|
+ g = 1.0 - localProgress * 0.15; // 1.0 到 0.85 (216/255)
|
|
|
+ b = 1.0 - localProgress * 0.1; // 1.0 到 0.9 (230/255)
|
|
|
+ } else {
|
|
|
+ // 头部(倒着飞时在后面):快速变透明
|
|
|
+ const localProgress = (reverseProgress - 0.4) / 0.6;
|
|
|
+ alpha = 0.4 * (1.0 - localProgress); // 0.4 到 0
|
|
|
+ r = 0.75 + localProgress * 0.25; // 保持淡蓝白色
|
|
|
+ g = 0.85 + localProgress * 0.15;
|
|
|
+ b = 0.9 + localProgress * 0.1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 alpha 应用到 RGB
|
|
|
+ r = r * alpha;
|
|
|
+ g = g * alpha;
|
|
|
+ b = b * alpha;
|
|
|
+
|
|
|
+ tailColors.array[idx] = r;
|
|
|
+ tailColors.array[idx + 1] = g;
|
|
|
+ tailColors.array[idx + 2] = b;
|
|
|
+ }
|
|
|
+
|
|
|
+ tailPositions.needsUpdate = true;
|
|
|
+ tailColors.needsUpdate = true;
|
|
|
+
|
|
|
+ // 如果流星飞出屏幕,重置到右上角外
|
|
|
+ const leftBound = -widthAtDistance * 0.8;
|
|
|
+ const bottomBound = -heightAtDistance * 0.8;
|
|
|
+
|
|
|
+ if (meteor.headPos.x < leftBound || meteor.headPos.y < bottomBound) {
|
|
|
+ // 从右上角外重新开始(确保在可见区域)
|
|
|
+ meteor.headPos.x = widthAtDistance * (0.3 + Math.random() * 1.2);
|
|
|
+ meteor.headPos.y = heightAtDistance * (-0.2 + Math.random() * 0.8);
|
|
|
+ meteor.headPos.z = distance - 2 - Math.random() * 6;
|
|
|
+
|
|
|
+ // 重置延迟,让流星错开出现(减少延迟,增加频率)
|
|
|
+ meteor.startDelay = meteorsRef.current.frameCount + Math.random() * 5; // 非常短的延迟
|
|
|
+
|
|
|
+ // 立即显示(因为延迟很短)
|
|
|
+ meteor.head.visible = true;
|
|
|
+ meteor.tail.visible = true;
|
|
|
+
|
|
|
+ // 重置尾巴点(倒着飞:尾巴在前面)
|
|
|
+ for (let t = 0; t < tailLength; t++) {
|
|
|
+ meteor.tailPoints[t] = {
|
|
|
+ x: meteor.headPos.x - Math.cos(baseAngle) * t * 0.5, // 尾巴在运动方向前方
|
|
|
+ y: meteor.headPos.y - Math.sin(baseAngle) * t * 0.5,
|
|
|
+ z: meteor.headPos.z
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
rendererRef.current.render(sceneRef.current, cameraRef.current);
|
|
|
};
|
|
|
|