做柜子设计的网站设计,杭州小程序开发定制,做ppt的软件,成都设计电商网站上述引用内容#xff0c;本文将基于 React Three.js GLSL 的相关知识#xff0c;实现 Apple 2025 动态热成像 logo 效果。通过本文的阅读和学习#xff0c;你将学习到的知识点包括#xff1a;离屏渲染技术 FBO、交互事件与动态参数控制、Leva 控制面板的应用、视频纹理、…上述引用内容本文将基于 React Three.js GLSL 的相关知识实现 Apple 2025 动态热成像 logo 效果。通过本文的阅读和学习你将学习到的知识点包括离屏渲染技术 FBO、交互事件与动态参数控制、Leva 控制面板的应用、视频纹理、遮罩纹理、着色器材质的使用、热成像动画着色器实现和应用等。效果本文页面实现效果如下图所示页面页面中心由 Apple 热成像动态图标构成图标上面由橙色和蓝色渐变色动态流动页面底部为蓝色渐变文案。preview当使用鼠标 ️ 或触控板 网页上按压或拖动 Logo 时可以看到颜色随手势展开变化看起来像是模拟真实热量轨迹。ctrl本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新。 代码仓库地址gitgithub.com:dragonir/threejs-odessey.git实现本文代码实现效果参考自https://github.com/vladmdgolam/apple-event-2025实现内容模块旨在对其核心知识点进行汇总归纳学习通过相同的原理并举一反三实现专属自己的热成像动态 logo 。① 资源引入以下是实现苹果热成像所需的主要依赖资源其中OrthographicCamera用于创建平行投影相机、LinearFilter 是纹理采样过滤方式的常量用于在控制纹理在放大缩小时的平滑过渡效果、ShaderMaterial 用于通过 GLSL创建自定义的着色器材质是实现本案例效果的关键、VideoTexture可以将视频元素作为数据源创建动态的视频纹理、Leva 是一个轻量级的前端调试工具库主要用于快速创建交互式控制面板方便开发者在开发过程中实时调试热成像的各种参数等。其他的依赖都是创建三维场景必须的一些内容具体作用可自行查阅。import { OrthographicCamera, DoubleSide, LinearFilter, Mesh, RGBFormat, RepeatWrapping, ShaderMaterial, Texture, TextureLoader, VideoTexture, } from threeimport { Leva, levaStore, useControls } from leva② 页面场景初始化 HeatmapScene使用 React Three Fiber 初始化场景、相机等其中 Leva 组件用于动态可视化调试着色器的多种参数Scene 组件用于渲染 logo 场景是整个交互可视化效果的核心统筹层。return (divLeva hidden{levaHidden} /InfoPanel onToggleControls{() setLevaHidden((p) !p)} onRandomizeColors{randomizeColors} /div ref{containerRef} classNamew-[560px] h-[560px] touch-none select-noneCanvasorthographiccamera{{ position: [0, 0, 1], left: -2, right: 2, top: 2, bottom: -2, near: -1, far: 1, }}gl{{ antialias: true, alpha: true, outputColorSpace: srgb }}flatScene containerRef{containerRef} //Canvas/divdiv classNamedragonirdragonir/div/div)③ 实现动态热力图网格 HeatMeshHeatMesh 组件它主要通过视频纹理 VideoTexture、绘制纹理 drawTexture 和遮罩纹理 maskTexture 作为数据源使用着色器材质 ShaderMaterial 渲染一个平面网格 planeGeometr实现了可实时调整的热力图效果。ShaderMaterial 通过传入自定义顶点着色器和片元着色器实现复杂的热力图色彩映射和动态效果。export const HeatMesh ({ drawTexture }: { drawTexture: Texture | null }) {const timeRef useRef(0)const videoRef useRefHTMLVideoElement | null(null)const [videoTexture, setVideoTexture] useStateVideoTexture | null(null)// Leva 控制面板着色器参数power强度、opacity透明度、颜色映射、混合与过渡等参数可实时调整。const { power, opacity, color1, blend1, fade1,maxBlend4 ...} useControls(Heat Map, {})// 遮罩纹理const maskTexture useLoader(TextureLoader, /logo.png)useEffect(() {if (maskTexture) {maskTexture.wrapS maskTexture.wrapT RepeatWrappingmaskTexture.needsUpdate true}}, [maskTexture])// 视频纹理useEffect(() {const video document.createElement(video)video.src /apple.mp4video.loop truevideo.playsInline truevideo.autoplay truevideo.preload autoconst onVideoLoad () {const texture new VideoTexture(video)texture.minFilter LinearFiltertexture.magFilter LinearFiltertexture.format RGBFormatsetVideoTexture(texture)}}, [])// 着色器材质const material useMemo(() {return new ShaderMaterial({uniforms: {blendVideo: { value: 1.0 },drawMap: { value: drawTexture },textureMap: { value: videoTexture || maskTexture },maskMap: { value: maskTexture },opacity: { value: opacity },amount: { value: 1.0 },color1: { value: color1 },blend: { value: [blend1, blend2, blend3, blend4] },fade: { value: [fade1, fade2, fade3, fade4] },power: { value: power },rnd: { value: 0 },maxBlend: { value: [maxBlend1, maxBlend2, maxBlend3, maxBlend4] },heat: { value: [0, 0, 0, 1.02] },stretch: { value: [1, 1, 0, 0] },},vertexShader: heatVertexShader,fragmentShader: heatFragmentShader,transparent: true,side: DoubleSide,})}, [...])// 动态更新与渲染,通过 useFrame 钩子每帧更新时间和随机值使热力图呈现动态变化useFrame((_, delta) {timeRef.current deltaif (material) {material.uniforms.rnd.value Math.random()material.uniforms.amount.value 1.0}})// 渲染一个平面网格应用自定义着色器材质作为热力图的载体return (meshplaneGeometry /primitive object{material} //mesh)}其中 heatVertexShader 和 heatFragmentShader 是着色器材质的顶点着色器和片元着色器它们的详细内容见文章最后的着色器模块。顶点着色器 heatVertexShader处理网格顶点的位置变换片元着色器 heatFragmentShader根据输入纹理的像素值结合颜色映射参数计算每个像素的最终颜色实现热力图效果。遮罩纹理图片预览mask④ 实现绘制渲染器组件 DrawRendererDrawRenderer 组件的主要作用是实时处理动态绘制输入通过双 FBO交替渲染机制通过缓冲和自定义着色器实现渲染累积与渐隐效果并接收外部输入的绘制位置、方向、强度等参数通过自定义着色器实时更新绘制纹理并将结果传递给外部使用。// 通过引入 useFBO 创建帧缓冲对象用于在GPU上存储和处理绘制纹理。import { useFBO } from react-three/dreiconst fboParams {type: FloatType,format: RGBAFormat,minFilter: LinearFilter,magFilter: LinearFilter,}export const DrawRenderer ({ size 256, position, direction, drawAmount, onTextureUpdate, sizeDamping, fadeDamping, radiusSize }) {const { size: canvasSize } useThree()const dynamicRadius radiusSizeconst fboA useFBO(size, size, fboParams)const fboB useFBO(size, size, fboParams)const renderTargets useMemo(() ({current: fboA,previous: fboB}), [fboA, fboB])const { drawScene, drawCamera, material } useMemo(() {const drawScene new Scene()const drawCamera new OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10)drawCamera.position.z 1// 通过 ShaderMaterial 定义绘制的核心逻辑着色器接收外部参数并更新 FBO 纹理const material new ShaderMaterial({uniforms: {uRadius: { value: [-8, 0.9, dynamicRadius] },uPosition: { value: [0, 0] },uDirection: { value: [0, 0, 0, 0] },uResolution: { value: [canvasSize.width, canvasSize.height, 1] },uTexture: { value: renderTargets.previous.texture },uSizeDamping: { value: sizeDamping },uFadeDamping: { value: fadeDamping },uDraw: { value: 0 },},// 处理平面顶点的坐标转换确保与 FBO 纹理坐标对齐vertexShader: drawVertexShader,// 根据输入的 uPosition uRadius等参数在上一帧纹理uTexture的基础上绘制新的渐变并应用衰减uFadeDamping使旧渐变渐消失实现动态流动效果fragmentShader: drawFragmentShader,depthTest: false,transparent: true,})// 创建一个平面网格作为绘制的画布const mesh new Mesh(new PlaneGeometry(1, 1), material)drawScene.add(mesh)return { drawScene, drawCamera, material }}, [renderTargets, dynamicRadius, sizeDamping, fadeDamping, canvasSize])// Update 着色器变量参数同步通过 useEffect 将外部传入的 position、direction、drawAmount等参数实时更新到着色器的 uniforms 中useEffect(() {material.uniforms.uRadius.value[2] dynamicRadiusmaterial.uniforms.uPosition.value positionmaterial.uniforms.uDirection.value directionmaterial.uniforms.uDraw.value drawAmount}, [material, dynamicRadius, position, direction, drawAmount])// 帧循环每帧执行以下操作将上一帧的FBO纹理previous作为输入传递给着色器切换渲染目标到当前FBO current渲染绘制场景交换current和previous的角色准备下一帧的累积useFrame(({ gl }) {const currentTarget renderTargets.currentconst previousTarget renderTargets.previousmaterial.uniforms.uTexture.value previousTarget.textureconst originalTarget gl.getRenderTarget()gl.setRenderTarget(currentTarget)gl.clear()gl.render(drawScene, drawCamera)gl.setRenderTarget(originalTarget)const temp renderTargets.currentrenderTargets.current renderTargets.previousrenderTargets.previous temp// 通过 onTextureUpdate回调将当前 FBO 的纹理传递给外部onTextureUpdate(currentTarget.texture)})// 组件本身不渲染任何可见元素仅负责后台处理绘制纹理return null} 帧缓冲对象 FBO 与双缓冲机制FBO 作用FBO 是 GPU 上的离屏渲染目标用于存储中间绘制结果,避免直接渲染到屏幕提高效率双缓冲设计创建两个 FBOfboA 和 fboB通过 renderTargets 管理当前帧 current 和上一帧previous每帧将上一帧的 FBO 纹理作为输入绘制新内容到当前 FBO然后交换两者的角色实现绘制效果的热力图的渐隐效果。⑤ 创建渲染场景组件 SceneScene 组件是整个交互可视化效果的核心统筹组件它主要实现的功能包括整合鼠标交互、参数控制、绘制渲染DrawRenderer 与热力图渲染 HeatMesh实现鼠标 hover 或者 移动时生成动态热力图。最终实现的效果是用户在画布上移动鼠标鼠标轨迹会实时生成带有热力渐变的动态效果且效果可通过 Leva 面板参数可以实时调整。import { DrawRenderer } from ./DrawRendererimport { HeatMesh } from ./HeatMeshexport const Scene ({ containerRef, }: { containerRef: React.RefObjectHTMLDivElement | null }) {const [mouse, setMouse] useState[number, number]([0, 0])const [heatAmount, setHeatAmount] useState(0)const [drawTexture, setDrawTexture] useStateTexture | null(null)const heatRef useRef(0)const lastMousePos useRef[number, number]([0, 0])const lastTime useRef(performance.now())const holdRef useRef(false)const { camera, size } useThree((state) ({ camera: state.camera, size: state.size }))// Leva 控制参数增加const { sizeDamping, fadeDamping, heatSensitivity, heatDecay, radiusSize } useControls(Hover Heat,{// 控制粗细的变化平滑度sizeDamping: { value: 0.8, min: 0.0, max: 1.0, step: 0.01 },// 控制消失的速度fadeDamping: { value: 0.98, min: 0.9, max: 1.0, step: 0.001 },// 鼠标移动时热度累积的快慢heatSensitivity: { value: 0.25, min: 0.1, max: 2.0, step: 0.05 },// 鼠标停止后热度下降的快慢heatDecay: { value: 0.92, min: 0.8, max: 0.99, step: 0.01 },// 控制单次绘制的范围大小radiusSize: { value: 75, min: 20, max: 300, step: 5 },})// 根据画布尺寸计算相机的宽高比动态设置 等参数确保相机的投影矩阵实时更新useEffect(() {if (camera camera instanceof OrthographicCamera) {const aspect size.width / size.heightlet width, heightif (aspect 1) {height 1width aspect} else {width 1height 1 / aspect}camera.left -width / 2camera.right width / 2camera.top height / 2camera.bottom -height / 2camera.near -1camera.far 1camera.updateProjectionMatrix()}}, [camera, size])// 通过pointermove/pointerleave事件监听鼠标在容器内的位置计算鼠标相对于容器的归一化坐标const handleDOMPointerMove useCallback((e: PointerEvent) {if (containerRef.current) {const rect containerRef.current.getBoundingClientRect()const clientX e.clientX - rect.xconst clientY e.clientY - rect.yconst normalizedX clientX / rect.widthconst normalizedY clientY / rect.heightconst x 2 * (normalizedX - 0.5)const y 2 * -(normalizedY - 0.5)holdRef.current truesetMouse([x, y])lastMousePos.current [x, y]lastTime.current performance.now()}},[containerRef])const handleDOMPointerLeave useCallback(() {holdRef.current false}, [])// 鼠标事件监听useEffect(() {const canvas containerRef.currentif (!canvas) returncanvas.addEventListener(pointermove, handleDOMPointerMove)canvas.addEventListener(pointerleave, handleDOMPointerLeave)return () {canvas.removeEventListener(pointermove, handleDOMPointerMove)canvas.removeEventListener(pointerleave, handleDOMPointerLeave)}}, [handleDOMPointerMove, handleDOMPointerLeave, containerRef])useFrame((_, delta) {// 热度累积当鼠标在容器内移动holdRef.current true时根据heatSensitivity和帧间隔delta计算热度增量heatRef.current持续累积最大限制为1.3避免强度溢出if (holdRef.current) {const heatIncrease heatSensitivity * delta * 60heatRef.current heatIncreaseheatRef.current Math.min(1.3, heatRef.current)setHeatAmount(heatRef.current)// 热度衰减当鼠标离开容器pointerleave或停止移动时热度值按 heatDecay衰减系数逐步降低直到低于0.001时清零} else if (heatRef.current 0) {heatRef.current * heatDecayheatRef.current heatRef.current 0.001 ? 0 : heatRef.currentsetHeatAmount(heatRef.current)}// 延迟重置鼠标停止移动后通过50ms延迟将 holdRef设为false避免因短暂停顿导致热度突然中断模拟自然残留感if (holdRef.current) {setTimeout(() {holdRef.current false}, 50)}})const direction useMemo[number, number, number, number](() {return [0, 0, 0, 100]}, [])const drawPosition useMemo[number, number](() {const x 0.5 * mouse[0] 0.5const y 0.5 * mouse[1] 0.5return [x, y]}, [mouse])// 向 DrawRenderer 传递绘制数据接收绘制结果并传递给 HeatMeshreturn (DrawRenderersize{256}position{drawPosition}direction{direction}drawAmount{heatAmount}onTextureUpdate{setDrawTexture}sizeDamping{sizeDamping}fadeDamping{fadeDamping}radiusSize{radiusSize}/HeatMesh drawTexture{drawTexture} //)}通过 Leva 控制面板动态调节着色器参数。leva⑥ 自定义颜色功能实现可以通过如下的方法生成随机色彩并将生成的参数传递到着色器可以实现热力图 logo 颜色的动态切换。const randomizeColors useCallback(() {const hslToHex (h: number, s: number, l: number) {s / 100l / 100const k (n: number) (n h / 30) % 12const a s * Math.min(l, 1 - l)const f (n: number) l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))const toHex (x: number) Math.round(255 * x).toString(16).padStart(2, 0)return #${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}}// 生成6种随机颜色 color2..color7color1保持黑色const base Math.floor(Math.random() * 360)const steps [15, 35, 55, 85, 140, 200]const palette steps.map((step, i) hslToHex((base step) % 360, 80 - i * 4, 50 (i - 3) * 3))const keys [color2, color3, color4, color5, color6, color7] as constkeys.forEach((key, i) {levaStore.setValueAtPath(Heat Map.${key}, palette[i], false)})}, [])color1⑦ 着色器 draw.fragprecision highp float;uniform float uDraw;uniform vec3 uRadius;uniform vec3 uResolution;uniform vec2 uPosition;uniform vec4 uDirection;uniform float uSizeDamping;uniform float uFadeDamping;uniform sampler2D uTexture;varying vec2 vUv;void main() {float aspect uResolution.x / uResolution.y;vec2 pos uPosition;pos.y / aspect;vec2 uv vUv;uv.y / aspect;float dist distance(pos, uv) / (uRadius.z / uResolution.x);dist smoothstep(uRadius.x, uRadius.y, dist);vec3 dir uDirection.xyz * uDirection.w;vec2 offset vec2((-dir.x) * (1.0-dist), (dir.y) * (1.0-dist));vec2 uvt vUv;vec4 color texture2D(uTexture, uvt (offset * 0.01));color * uFadeDamping;color.r offset.x;color.g offset.y;color.rg clamp(color.rg, -1.0, 1.0);float d uDraw;color.b d * (1.0-dist);gl_FragColor vec4(color.rgb, 1.0);} heat.fragprecision highp isampler2D;precision highp usampler2D;uniform sampler2D drawMap;uniform sampler2D textureMap;uniform sampler2D maskMap;uniform float amount;uniform float opacity;uniform vec3 color1;uniform vec3 color2;uniform vec3 color3;uniform vec3 color4;uniform vec3 color5;uniform vec3 color6;uniform vec3 color7;uniform vec4 blend;uniform vec4 fade;uniform vec4 maxBlend;uniform float power;varying vec2 vUv;varying vec4 vClipPosition;vec3 linearRgbToLuminance(vec3 linearRgb){float finalColor dot(linearRgb, vec3(0.2126729, 0.7151522, 0.0721750));return vec3(finalColor);}vec3 saturation(vec3 color, float saturation){return mix(linearRgbToLuminance(color), color, saturation);}vec3 gradient(float t) {float p1 blend.x;float p2 blend.y;float p3 blend.z;float p4 blend.w;float p5 maxBlend.x;float p6 maxBlend.y;float f1 fade.x;float f2 fade.y;float f3 fade.z;float f4 fade.w;float f5 maxBlend.z;float f6 maxBlend.w;float blend1 smoothstep(p1 - f1 * 0.5, p1 f1 * 0.5, t);float blend2 smoothstep(p2 - f2 * 0.5, p2 f2 * 0.5, t);float blend3 smoothstep(p3 - f3 * 0.5, p3 f3 * 0.5, t);float blend4 smoothstep(p4 - f4 * 0.5, p4 f4 * 0.5, t);float blend5 smoothstep(p5 - f5 * 0.5, p5 f5 * 0.5, t);float blend6 smoothstep(p6 - f6 * 0.5, p6 f6 * 0.5, t);vec3 color color1;color mix(color, color2, blend1);color mix(color, color3, blend2);color mix(color, color4, blend3);color mix(color, color5, blend4);color mix(color, color6, blend5);color mix(color, color7, blend6);return color;}void main() {vec2 duv vClipPosition.xy/vClipPosition.w;duv 0.5 duv * 0.5;vec2 uv vUv;uv - 0.5;uv 0.5;float o clamp(opacity, 0.0, 1.0);float a clamp(amount, 0.0, 1.0);float v o * a;vec4 tex texture2D(maskMap, uv);float mask tex.g;float logo smoothstep(0.58, 0.6, 1.0-tex.b);vec2 wuv uv;vec3 draw texture2D(drawMap, duv).rgb;float heatDraw draw.b;heatDraw * mix(0.1, 1.0, mask);vec2 offset2 draw.rg * 0.01;vec3 video textureLod(textureMap, wuv offset2, 0.0).rgb;float h mix(pow(1.0-video.r, 1.5), 1.0, 0.2) * 1.25;heatDraw * h;float map video.r;map pow(map, power);float msk smoothstep(0.2, 0.5, uv.y);map mix( map * 0.91, map, msk);map mix(0.0, map, v);float fade2 distance(vUv, vec2(0.5, 0.52));fade2 smoothstep(0.5, 0.62, 1.0-fade2);vec3 finalColor gradient(map heatDraw);finalColor saturation(finalColor, 1.3);finalColor * fade2;finalColor mix(vec3(0.0), finalColor, a);gl_FragColor vec4(finalColor, 1.0);}总结 本项目代码主要由 4 个核心组件构成其中HeatmapScene是全局容器作为顶层组件管理 Three.js 、Leva 控制面板和其他页面信息通过 levaStore 全局管理热力图颜色参数传递容器引用给子组件。Scene交互与统筹处理鼠标交互计算热度值模拟鼠标轨迹的累积与衰减串联 DrawRenderer 和 HeatMesh传递交互参数。DrawRenderer绘制处理使用双帧缓冲 FBO 实现离屏绘制高效累积鼠标轨迹通过自定义着色器处理轨迹的绘制、渐隐与衰减输出处理后的绘制纹理 drawTexture 给 HeatMesh。HeatMesh热力图渲染基于 DrawRenderer输出的纹理结合视频纹理和遮罩纹理通过自定义着色器生成热力图效果。 本文中主要包含的新知识点如下Three.js 离屏渲染技术 FBO通过 useFBO 创建帧缓冲对象实现 GPU 层面的离屏绘制避免直接操作 DOM 提升性能双缓冲机制 fboA/fboB 交替渲染实现绘制轨迹的累积与动态更新。交互事件与动态参数控制鼠标键盘事件监听将用户输入转换为可量化的参数。Leva 控制面板通过 useControls 实时调整视觉参数提升开发灵活性。视频纹理、遮罩纹理、着色器材质的使用等。