react前端艺术——赛博烟雾

201 阅读4分钟

艺术品展示

smokke.GIF

代码结构

  • 组件传值接口
  • 烟雾组件全局变量定义
  • 烟雾位置移动函数
  • 烟雾不同颜色改变函数
  • 动画函数
  • 初始化webgl
interface Iprops {
    src: string;
    opacity: number;
    smokeSrc: string;
    smokeOpacity: number;
    height: string;
    width: string;
}
const CanvasSmoke = () => {
    const location = useLocation();
    const props: Iprops = location.state || {};
    const mount = useRef<HTMLDivElement>(null);
    const clockRef = useRef(new THREE.Clock());
    ......

    const evolveSmoke = useCallback(() => {
       ......
    }, []);
    const colorchange = useCallback(() => {
       ......
    }, []);
    const animate = useCallback(() => {
        ......
    }, [evolveSmoke, colorchange]);

    useEffect(() => {
       ......
    }, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);
    return (
        <div
            style={{
                height: props.height || '100vh',
                width: props.width || '100vw',
                margin: '0'
            }}
            ref={mount}
        />
    );
};

export default CanvasSmoke;

烟雾位置移动

  1. 获取粒子数量
  2. 循环给每个粒子围绕Z轴以及Y轴旋转,其中deltaRef是时间间隔
  3. 直到数量为0停止

evolveSmoke

 const evolveSmoke = useCallback(() => {
        let sp = smokeParticlesRef.current.length;
        while (sp--) {
            smokeParticlesRef.current[sp].rotation.z += deltaRef.current * 0.5;
            smokeParticlesRef.current[sp].rotation.y += 0.02;
            // smokeParticlesRef.current[sp].rotation.x += 0.02;
        }
    }, []);

烟雾设置不同颜色

  1. 设置随机颜色
  2. 关键说明

saturation 设为 1.0(最大饱和度) 0.0 = 灰色 0.5 = 中等饱和度 1.0 = 最大饱和度

// 高饱和度,较亮 const saturation = 1.0; const lightness = 0.6;

// 高饱和度,较暗 const saturation = 1.0; const lightness = 0.4;

colorchange

const colorchange = useCallback(() => {
        timeRef.current += deltaRef.current;
        const hue = (timeRef.current * 0.1 + 0.01) % 1;//色相
        const saturation = 1.0; // 饱和度:1.0 表示最饱和
        const lightness = 0.5; // 亮度:0.5 表示中等亮度

        colorRef.current = new THREE.Color().setHSL(hue, saturation, lightness);
    }, []);

动画

  1. 移动位置
  2. 设置颜色
  3. 更改文字材质颜色
  4. 渲染
const animate = useCallback(() => {
       if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !meshRef.current) return;

       deltaRef.current = clockRef.current.getDelta();
       frameIdRef.current = requestAnimationFrame(animate);
       evolveSmoke();
       colorchange();

       meshRef.current.rotation.x += 0.005;
       meshRef.current.rotation.y += 0.01;
       cubeSineDriverRef.current += 0.01;
       meshRef.current.position.z = 100 + Math.sin(cubeSineDriverRef.current) * 500;

       if (textRef.current?.material instanceof THREE.MeshLambertMaterial) {
           textRef.current.material.color = colorRef.current;
           textRef.current.material.needsUpdate = true;
       }

       rendererRef.current.render(sceneRef.current, cameraRef.current);
   }, [evolveSmoke, colorchange]);

初始化

  1. 初始setup

  2. 设置渲染区域大小,相机位置,场景内容,背景材质

  3. 第一个textMaterial主要渲染文字

  • 创建一个 300x300 的平面几何体用于显示文字
  • 加载纹理
  • 创建材质
  • 创建网格
  • 设置文字的位置
  • 添加到场景
  • 保存文字网格的引用

重要特性解释:

  1. 材质特性:
  • color: 基础颜色(从 colorRef 获取)

  • opacity: 透明度(从 props 获取)

  • transparent: 启用透明度处理

  • blending: 使用加法混合模式,使重叠部分更亮

  1. AdditiveBlending(加法混合):
  • 将前景色与背景色的RGB值相加

  • 创造出发光的效果

  • 适合创建霓虹、光效等视觉效果

  1. 位置设置:
  • text.position.z = 800 将文字放在场景较远处

  • 这样文字会在烟雾效果的后方显示

  1. 引用保存:
  • 使用 textRef 保存文字网格的引用

  • 方便后续对文字进行动画或其他操作

  1. 第二个textMaterial主要渲染烟雾

  • 设置300*300的烟雾切片

  • 对每个切片循环设置不同的颜色 保存烟雾网格的引用

  1. 渲染
  2. 卸载动画以及渲染器
useEffect(() => {
        const mountElement = mount.current;
        if (!mountElement) return;

        const width = mountElement.clientWidth;
        const height = mountElement.clientHeight;

        //Three.js setup
        rendererRef.current = new THREE.WebGLRenderer();
        rendererRef.current.setSize(width, height);

        sceneRef.current = new THREE.Scene();
        cameraRef.current = new THREE.PerspectiveCamera(75, width / height, 1, 10000);
        cameraRef.current.position.z = 1000;
        sceneRef.current.add(cameraRef.current);
        let geometry = new THREE.BoxGeometry(200, 200, 200);
        let material = new THREE.MeshLambertMaterial({
            color: 0xaa6666,
            wireframe: false
        });

        meshRef.current = new THREE.Mesh(geometry, material);
        // 创建一个 300x300 的平面几何体用于显示文字
let textGeo = new THREE.PlaneGeometry(300, 300);

// 加载纹理
new THREE.TextureLoader().load(
    props.src,  // 纹理图片的源路径
    // 成功加载后的回调函数
    (textTexture) => {
        // 创建材质
        let textMaterial = new THREE.MeshLambertMaterial({
            color: colorRef.current,         // 使用引用的颜色
            opacity: props.opacity,          // 透明度
            map: textTexture,               // 加载的纹理
            transparent: true,              // 启用透明
            blending: THREE.AdditiveBlending // 加法混合模式
        });

        // 创建网格(结合几何体和材质)
        let text = new THREE.Mesh(textGeo, textMaterial);
        
        // 设置文字在 z 轴上的位置(距离相机 800 单位)
        text.position.z = 800;
        
        // 添加到场景
        sceneRef.current.add(text);
        
        // 保存文字网格的引用
        textRef.current = text;
    },
    undefined,  // 加载进度回调(这里未使用)
    (err) => { // 加载失败回调
        console.log('load failed.');
        console.log(err);
    }
);

        let light = new THREE.DirectionalLight(0xffffff, 0.5);
        light.position.set(-1, 0, 1);
        sceneRef.current.add(light);
        new THREE.TextureLoader().load(
            props.smokeSrc,
            (smokeTexture) => {
                for (let p = 0; p < 150; p++) {
                    const hue = Math.random();
                    const saturation = 1.0;
                    const lightness = 0.5;

                    const color = new THREE.Color().setHSL(hue, saturation, lightness);
                    let smokeMaterial = new THREE.MeshLambertMaterial({
                        color: color,
                        map: smokeTexture,
                        opacity: props.smokeOpacity,
                        transparent: true,
                        blending: THREE.AdditiveBlending
                    });
                    let smokeGeo = new THREE.PlaneGeometry(300, 300);
                    let particle = new THREE.Mesh(smokeGeo, smokeMaterial);
                    particle.position.set(
                        Math.random() * 500 - 250,
                        Math.random() * 500 - 250,
                        Math.random() * 1000 - 100
                    );
                    particle.rotation.z = Math.random() * 360;
                    sceneRef.current.add(particle);
                    smokeParticlesRef.current.push(particle);
                }
            },
            undefined,
            (err) => {
                console.log('load failed.');
                console.log(err);
            }
        );

        mountElement.appendChild(rendererRef.current.domElement);
        // remember these initial values
        // let tanFOV = Math.tan(((Math.PI / 180) * this.camera.fov) / 2);
        // let windowHeight = height;

        animate();
        return () => {
            if (frameIdRef.current) {
                cancelAnimationFrame(frameIdRef.current);
            }
            if (mountElement && rendererRef.current) {
                mountElement.removeChild(rendererRef.current.domElement);
            }
        };
    }, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);

完整内容

import React, {useCallback, useEffect, useRef} from 'react';
import {useLocation} from 'react-router-dom';
import * as THREE from 'three';

interface Iprops {
    src: string;
    opacity: number;
    smokeSrc: string;
    smokeOpacity: number;
    height: string;
    width: string;
}
const CanvasSmoke = () => {
    const location = useLocation();
    const props: Iprops = location.state || {};
    const mount = useRef<HTMLDivElement>(null);
    const clockRef = useRef(new THREE.Clock());
    const rendererRef = useRef<THREE.WebGLRenderer>(new THREE.WebGLRenderer());
    const sceneRef = useRef<THREE.Scene>(new THREE.Scene());
    const cameraRef = useRef<THREE.PerspectiveCamera>(new THREE.PerspectiveCamera());
    const meshRef = useRef<THREE.Mesh>(new THREE.Mesh());
    const smokeParticlesRef = useRef<THREE.Mesh[]>([]);
    const cubeSineDriverRef = useRef(0);
    const frameIdRef = useRef<number | undefined>(undefined);
    const deltaRef = useRef(0);
    const colorRef = useRef<THREE.Color>(new THREE.Color());
    const timeRef = useRef(0);
    const textRef = useRef<THREE.Mesh>(new THREE.Mesh());

    const evolveSmoke = useCallback(() => {
        let sp = smokeParticlesRef.current.length;
        while (sp--) {
            smokeParticlesRef.current[sp].rotation.z += deltaRef.current * 0.5;
            smokeParticlesRef.current[sp].rotation.y += 0.02;
            // smokeParticlesRef.current[sp].rotation.x += 0.02;
        }
    }, []);
    const colorchange = useCallback(() => {
        timeRef.current += deltaRef.current;
        const hue = (timeRef.current * 0.1 + 0.01) % 1;
        const saturation = 1.0; // 饱和度:1.0 表示最饱和
        const lightness = 0.5; // 亮度:0.5 表示中等亮度

        colorRef.current = new THREE.Color().setHSL(hue, saturation, lightness);
    }, []);
    const animate = useCallback(() => {
        if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !meshRef.current) return;

        deltaRef.current = clockRef.current.getDelta();
        frameIdRef.current = requestAnimationFrame(animate);
        evolveSmoke();
        colorchange();

        meshRef.current.rotation.x += 0.005;
        meshRef.current.rotation.y += 0.01;
        cubeSineDriverRef.current += 0.01;
        meshRef.current.position.z = 100 + Math.sin(cubeSineDriverRef.current) * 500;

        if (textRef.current?.material instanceof THREE.MeshLambertMaterial) {
            textRef.current.material.color = colorRef.current;
            textRef.current.material.needsUpdate = true;
        }

        rendererRef.current.render(sceneRef.current, cameraRef.current);
    }, [evolveSmoke, colorchange]);

    useEffect(() => {
        const mountElement = mount.current;
        if (!mountElement) return;

        const width = mountElement.clientWidth;
        const height = mountElement.clientHeight;

        //Three.js setup
        rendererRef.current = new THREE.WebGLRenderer();
        rendererRef.current.setSize(width, height);

        sceneRef.current = new THREE.Scene();
        cameraRef.current = new THREE.PerspectiveCamera(75, width / height, 1, 10000);
        cameraRef.current.position.z = 1000;
        sceneRef.current.add(cameraRef.current);
        let geometry = new THREE.BoxGeometry(200, 200, 200);
        let material = new THREE.MeshLambertMaterial({
            color: 0xaa6666,
            wireframe: false
        });

        meshRef.current = new THREE.Mesh(geometry, material);
        let textGeo = new THREE.PlaneGeometry(300, 300);

        new THREE.TextureLoader().load(
            props.src,
            (textTexture) => {
                let textMaterial = new THREE.MeshLambertMaterial({
                    color: colorRef.current,
                    opacity: props.opacity,
                    map: textTexture,
                    transparent: true,
                    blending: THREE.AdditiveBlending
                });
                let text = new THREE.Mesh(textGeo, textMaterial);
                text.position.z = 800;
                sceneRef.current.add(text);
                textRef.current = text;
            },
            undefined,
            (err) => {
                console.log('load failed.');
                console.log(err);
            }
        );

        let light = new THREE.DirectionalLight(0xffffff, 0.5);
        light.position.set(-1, 0, 1);
        sceneRef.current.add(light);
        new THREE.TextureLoader().load(
            props.smokeSrc,
            (smokeTexture) => {
                for (let p = 0; p < 150; p++) {
                    const hue = Math.random();
                    const saturation = 1.0;
                    const lightness = 0.5;

                    const color = new THREE.Color().setHSL(hue, saturation, lightness);
                    let smokeMaterial = new THREE.MeshLambertMaterial({
                        color: color,
                        map: smokeTexture,
                        opacity: props.smokeOpacity,
                        transparent: true,
                        blending: THREE.AdditiveBlending
                    });
                    let smokeGeo = new THREE.PlaneGeometry(300, 300);
                    let particle = new THREE.Mesh(smokeGeo, smokeMaterial);
                    particle.position.set(
                        Math.random() * 500 - 250,
                        Math.random() * 500 - 250,
                        Math.random() * 1000 - 100
                    );
                    particle.rotation.z = Math.random() * 360;
                    sceneRef.current.add(particle);
                    smokeParticlesRef.current.push(particle);
                }
            },
            undefined,
            (err) => {
                console.log('load failed.');
                console.log(err);
            }
        );

        mountElement.appendChild(rendererRef.current.domElement);
        // remember these initial values
        // let tanFOV = Math.tan(((Math.PI / 180) * this.camera.fov) / 2);
        // let windowHeight = height;

        animate();
        return () => {
            if (frameIdRef.current) {
                cancelAnimationFrame(frameIdRef.current);
            }
            if (mountElement && rendererRef.current) {
                mountElement.removeChild(rendererRef.current.domElement);
            }
        };
    }, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);
    return (
        <div
            style={{
                height: props.height || '100vh',
                width: props.width || '100vw',
                margin: '0'
            }}
            ref={mount}
        />
    );
};

export default CanvasSmoke;