使用 Three.js 的 WebGL 小实验。Three.js 全息影像效果,动画化模型进行过渡效果。”

HTML:
<div id="app"></div>
*,*::after,*::before {margin: 0;padding: 0;}body {box-sizing: border-box;}#app {position: fixed;top: 0;left: 0;z-index: -1;width: 100%;height: 100vh;}
JAVASCRIPT:
import * as THREE from "https://esm.sh/three";import { OrbitControls } from "https://esm.sh/three/addons/controls/OrbitControls.js";import gsap from "https://esm.sh/gsap";import GUI from "https://esm.sh/lil-gui";const vertexShader = `uniform float uTime;uniform float uProgress;uniform float uMinY;uniform float uMaxY;varying vec3 vPosition;varying vec3 vNormal;// returns 0.0 to 1.0 value.float random(vec2 st) {return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);}void main() {vec4 modelPosition = modelMatrix * vec4(position, 1.0);// base glitchfloat glitchTime = uTime - modelPosition.y;float glitchstrength = sin(glitchTime) * sin(glitchTime * 3.45) + sin(glitchTime * 8.76);glitchstrength /= 3.0;glitchstrength = smoothstep(0.5, 1.0, glitchstrength);glitchstrength *= 2.0;modelPosition.x += (random(modelPosition.xz + uTime) - 0.5) * glitchstrength;modelPosition.z += (random(modelPosition.xz + uTime) - 0.5) * glitchstrength;// glitch by progressfloat normalizedY = (modelPosition.y - uMinY) / (uMaxY - uMinY);float diff = abs(normalizedY - uProgress);float glitchStrength = smoothstep(0.02, 0.0, diff);glitchStrength *= 0.3;modelPosition.x += (random(modelPosition.xz + uTime) - 0.5) * glitchStrength;modelPosition.z += (random(modelPosition.xz + uTime) - 0.5) * glitchStrength;gl_Position = projectionMatrix * viewMatrix * modelPosition;vPosition = modelPosition.xyz;vec4 newNormal = modelMatrix * vec4(normal, 0.0);vNormal = newNormal.xyz;}`;const fragmentShader = `uniform float uTime;uniform float uIndex;uniform float uCurrentIndex;uniform float uNextIndex;uniform float uProgress;uniform vec3 uColor;uniform float uMinY;uniform float uMaxY;varying vec3 vPosition;varying vec3 vNormal;void main() {if(uIndex != uCurrentIndex && uIndex != uNextIndex) {discard;}float lines = 20.0;float offset = vPosition.y - uTime * 0.2;float density = mod(offset * lines, 1.0);density = pow(density, 3.0);vec3 viewDirection = normalize(vPosition - cameraPosition);float fresnel = 1.0 - abs(dot(normalize(vNormal), viewDirection));fresnel = pow(fresnel, 2.0);float falloff = smoothstep(0.8, 0.0, fresnel);float holographic = density * fresnel;holographic += fresnel * 1.25;holographic *= falloff;float normalizedY = (vPosition.y - uMinY) / (uMaxY - uMinY);if(uIndex == uCurrentIndex && normalizedY < uProgress) {discard;}if(uIndex == uNextIndex && normalizedY > uProgress) {discard;}gl_FragColor = vec4(uColor, holographic);#include <tonemapping_fragment>#include <colorspace_fragment>}`;const params = {color: "#00d5ff",stageColor: "#d4d4d4",ambientLight: "#ffffff",directionalLight: "#a4d5f4",currentIndex: 0,nextIndex: 1,};const uniforms = {uColor: new THREE.Uniform(new THREE.Color(params.color)),uTime: new THREE.Uniform(0),uProgress: new THREE.Uniform(0),uIndex: new THREE.Uniform(0),uCurrentIndex: new THREE.Uniform(params.currentIndex),uNextIndex: new THREE.Uniform(params.nextIndex),uMinY: new THREE.Uniform(0),uMaxY: new THREE.Uniform(0),};const initThree = (app) => {const size = {width: window.innerWidth,height: window.innerHeight,dpr: window.devicePixelRatio,};// ---------------------------------------// Sceneconst scene = new THREE.Scene();// Cameraconst camera = new THREE.PerspectiveCamera(35,size.width / size.height,0.1,100);camera.position.set(0, 4, -10);scene.add(camera);// Rendererconst renderer = new THREE.WebGLRenderer({ antialias: true });renderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.PCFSoftShadowMap;renderer.setSize(size.width, size.height);renderer.setPixelRatio(size.dpr);app.appendChild(renderer.domElement);// Controlsconst controls = new OrbitControls(camera, renderer.domElement);// ---------------------------------------// Lightsconst ambientLight = new THREE.AmbientLight(new THREE.Color(params.ambientLight),0.5);scene.add(ambientLight);const dLight = new THREE.DirectionalLight(new THREE.Color(params.directionalLight),1.0);dLight.position.set(0, 3, 1);scene.add(dLight);const pLight = new THREE.PointLight(new THREE.Color(params.color), 1, 10);pLight.position.set(0, -1.3, 0);scene.add(pLight);// ---------------------------------------const baseMaterial = new THREE.ShaderMaterial({vertexShader: vertexShader,fragmentShader: fragmentShader,uniforms,transparent: true,blending: THREE.AdditiveBlending,});baseMaterial.depthWrite = false;// Stageconst cylinder = new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 0.5, 128),new THREE.MeshStandardMaterial({color: new THREE.Color(params.stageColor),}));cylinder.position.set(0, -2, 0);scene.add(cylinder);// Geomsconst torusknotGeometry = new THREE.TorusKnotGeometry(1, 0.5, 128, 32);torusknotGeometry.computeBoundingBox();const icosahedronGeometry = new THREE.IcosahedronGeometry(2, 24);icosahedronGeometry.computeBoundingBox();const torusGeometry = new THREE.TorusGeometry(1.4, 0.5, 128, 32);torusGeometry.computeBoundingBox();// compute minY and maxYconst minY = Math.min(torusknotGeometry.boundingBox.min.y,icosahedronGeometry.boundingBox.min.y,torusGeometry.boundingBox.min.y);const maxY = Math.max(torusknotGeometry.boundingBox.max.y,icosahedronGeometry.boundingBox.max.y,torusGeometry.boundingBox.max.y);const margin = 0.1;const posY = 0.5;uniforms.uMinY.value = minY + posY - margin;uniforms.uMaxY.value = maxY + posY + margin;// Torus knotconst torusKnotMaterial = baseMaterial.clone();torusKnotMaterial.uniforms.uIndex.value = 0;const torusKnot = new THREE.Mesh(torusknotGeometry, torusKnotMaterial);torusKnot.position.y = posY;scene.add(torusKnot);// Icosahedronconst icosahedronMaterial = baseMaterial.clone();icosahedronMaterial.uniforms.uIndex.value = 1;const icosahedron = new THREE.Mesh(icosahedronGeometry, icosahedronMaterial);icosahedron.position.y = posY;scene.add(icosahedron);// Torusconst torusMaterial = baseMaterial.clone();torusMaterial.uniforms.uIndex.value = 2;const torus = new THREE.Mesh(torusGeometry, torusMaterial);torus.position.y = posY;scene.add(torus);const materials = [torusKnotMaterial, icosahedronMaterial, torusMaterial];// ---------------------------------------const clock = new THREE.Clock();const animate = () => {requestAnimationFrame(animate);const delta = clock.getDelta();const elapsedTime = clock.getElapsedTime();// update index and animate progressconst newIndex = Math.floor((elapsedTime * 0.25) % 3);if (newIndex !== params.currentIndex) {params.currentIndex = newIndex;params.nextIndex = newIndex === 2 ? 0 : newIndex + 1;materials.forEach((material) => {material.uniforms.uCurrentIndex.value = params.currentIndex;material.uniforms.uNextIndex.value = params.nextIndex;gsap.fromTo(material.uniforms.uProgress,{ value: 0 },{ value: 1, duration: 1.5, ease: "linear" });});}// Update modelstorusKnot.rotation.y += delta * 0.5;torusKnot.rotation.x += delta * 0.5;torus.rotation.y += delta * 0.5;torus.rotation.x += delta * 0.5;// Update uniformstorusMaterial.uniforms.uTime.value = elapsedTime;icosahedronMaterial.uniforms.uTime.value = elapsedTime;renderer.render(scene, camera);};animate();materials.forEach((material) => {material.uniforms.uCurrentIndex.value = params.currentIndex;material.uniforms.uNextIndex.value = params.nextIndex;gsap.fromTo(material.uniforms.uProgress,{ value: 0 },{ value: 1, duration: 1.5, ease: "linear" });});// ---------------------------------------// Resizewindow.addEventListener("resize", () => {size.width = window.innerWidth;size.height = window.innerHeight;size.dpr = window.devicePixelRatio;camera.aspect = size.width / size.height;camera.updateProjectionMatrix();renderer.setSize(size.width, size.height);renderer.setPixelRatio(size.dpr);});// ----------------------------------------// GUIconst gui = new GUI();gui.addColor(params, "color").name("hologram color").onChange((val) => {materials.forEach((material) => {material.uniforms.uColor.value = new THREE.Color(val);});pLight.color = new THREE.Color(val);});gui.addColor(params, "stageColor").name("stage color").onChange((val) => (cylinder.material.color = new THREE.Color(val)));gui.addColor(params, "ambientLight").name("ambient light").onChange((val) => (ambientLight.color = new THREE.Color(val)));gui.addColor(params, "directionalLight").name("directional light").onChange((val) => (dLight.color = new THREE.Color(val)));};const init = () => {const app = document.querySelector("#app");if (app) {initThree(app);}};document.addEventListener("DOMContentLoaded", init);
源码:
https://codepen.io/kyoko-nr/pen/bNGJrQe
体验:
https://codepen.io/kyoko-nr/full/bNGJrQe

