“使用 Three.js 的 WebGL 小实验。Three.js 全息影像效果,动画化模型进行过渡效果。”
HTML:
*,
*::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;
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);
float 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;
float 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);
}`;
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,
};
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
35,
size.width / size.height,
0.1,
100
);
camera.position.set(0, 4, -10);
scene.add(camera);
const 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);
const controls = new OrbitControls(camera, renderer.domElement);
const 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;
const 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);
const 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();
const 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;
const torusKnotMaterial = baseMaterial.clone();
torusKnotMaterial.uniforms.uIndex.value = 0;
const torusKnot = new THREE.Mesh(torusknotGeometry, torusKnotMaterial);
torusKnot.position.y = posY;
scene.add(torusKnot);
const icosahedronMaterial = baseMaterial.clone();
icosahedronMaterial.uniforms.uIndex.value = 1;
const icosahedron = new THREE.Mesh(icosahedronGeometry, icosahedronMaterial);
icosahedron.position.y = posY;
scene.add(icosahedron);
const 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();
const 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" }
);
});
}
torusKnot.rotation.y += delta * 0.5;
torusKnot.rotation.x += delta * 0.5;
torus.rotation.y += delta * 0.5;
torus.rotation.x += delta * 0.5;
torusMaterial.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" }
);
});
window.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);
});
const 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
阅读原文:原文链接
该文章在 2025/4/21 10:31:37 编辑过