阅读视图

发现新文章,点击刷新页面。

Three.js 收藏吃灰系列:一套代码,把你的 GLB 模型变成漫威级全息投影

Three.js 进阶实战:打造“锐利边缘”的赛博朋克特效

20260127_113057.gif

在 Three.js 的 3D 开发中,我们经常需要制作“扫描”、“全息”或者“能量护盾”的效果。最常用的手段是使用 菲涅尔(Fresnel) 效应。

但是,默认的菲涅尔效果往往呈现出一种“软绵绵”的雾化感,中心区域总是有一层洗不掉的朦胧。如果你想要一种硬核、锐利、仅保留物体轮廓的赛博朋克风格(Hard-surface Edge),就需要对 Shader 进行一些特殊的数学运算截断。

今天,我们就来拆解一套**“锐利边缘能量体”**的代码实现。

前排提示:本文涉及的特效逻辑,源自我正在开发的 3D 编辑器项目 Meteor3DEditor。如果你对 Web3D 引擎开发感兴趣,欢迎 Star 和体验!


核心难点:如何让菲涅尔变得“锐利”?

普通的菲涅尔公式通常是 1.0 - dot(viewDir, normal)。这会产生一个从边缘向中心平滑过渡的渐变。

为了得到锐利的边缘,我们需要在 Fragment Shader 中做三件事:

  1. 极高次幂压缩:把边缘压扁。
  2. 阈值截断:把中心的“雾气”彻底切除。
  3. 亮度重映射:把剩下的边缘提亮。

1. Shader 代码解析

这是核心的 ShaderMaterial 实现。请注意看 fragmentShader 中的注释部分:

JavaScript

const sharpFresnelShader = {
    uniforms: {
        color: { value: new THREE.Color(0x00ffff) }, // 赛博青色
        opacityMultiplier: { value: 1.0 }
    },
    vertexShader: `
        varying vec3 vWorldNormal;
        varying vec3 vViewDirection;
        void main() {
            // 获取世界坐标系下的法线
            vWorldNormal = normalize(mat3(modelMatrix) * normal);
            vec4 worldPosition = modelMatrix * vec4(position, 1.0);
            // 计算视线方向
            vViewDirection = normalize(cameraPosition - worldPosition.xyz);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform vec3 color;
        uniform float opacityMultiplier;
        varying vec3 vWorldNormal;
        varying vec3 vViewDirection;
        
        void main() {
            vec3 normal = normalize(vWorldNormal);
            vec3 viewDir = normalize(vViewDirection);

            // --- 基础菲涅尔 ---
            float dotProduct = dot(viewDir, normal);
            // 1.0 - abs(dot) 确保双面渲染时背面也能正确发光
            float fresnel = 1.0 - abs(dotProduct); 
            fresnel = clamp(fresnel, 0.0, 1.0);

            // --- 关键魔法区域 ---
            
            // 技巧1:使用 pow(6.0) 甚至更高。
            // 默认的菲涅尔可能是 pow(2.0),看起来很软。
            // 6.0 的指数会让函数曲线极速陡峭,只有最边缘的地方保留数值,其余迅速归零。
            fresnel = pow(fresnel, 6.0); 

            // 技巧2:阈值截断 (Threshold)
            // 即使是 pow(6.0),中心正对相机的地方可能还有 0.01 的微弱值。
            // 这种微弱值在叠加模式下会导致模型看起来脏脏的。
            // 我们减去 0.1,强制把这一部分变成纯黑(透明)。
            fresnel = max(fresnel - 0.1, 0.0); 
            
            // 技巧3:亮度补偿
            // 因为刚才减去了 0.1,最大值只剩 0.9 了,而且整体变暗。
            // 乘一个系数把边缘亮度拉爆。
            fresnel *= 2.5; 

            // ------------------

            float finalAlpha = fresnel * opacityMultiplier;
            
            // 颜色也乘以 alpha,配合 AdditiveBlending 食用效果更佳
            gl_FragColor = vec4(color * finalAlpha, finalAlpha);
        }
    `
};

2. 材质配置的关键点

Shader 写好了,Material 的配置同样重要。为了达到“全息投影”那种通透感,我们需要关闭深度写入,并开启叠加混合。

JavaScript

this.energyMaterial = new THREE.ShaderMaterial({
    uniforms: THREE.UniformsUtils.clone(sharpFresnelShader.uniforms),
    vertexShader: sharpFresnelShader.vertexShader,
    fragmentShader: sharpFresnelShader.fragmentShader,
    transparent: true,
    side: THREE.DoubleSide,      // 双面渲染:让模型内部的结构线条也能透出来,增加复杂度
    depthWrite: false,           // 关闭深度写入:这是实现“透明重叠”不穿帮的关键
    blending: THREE.AdditiveBlending // 叠加混合:重叠的部分会变亮(变白),模拟光的物理叠加
});

动态系统:上升的能量残影

光有一个静止的边缘是不够帅的。我们需要让它动起来。这里我们实现了一个简单的粒子系统思想:不断克隆模型,向上发射,然后消失。

系统逻辑 (RisingEnergySystem)

这个类负责管理所有的“残影(Ghost)”。

  1. Spawn (生成) :每隔几百毫秒,克隆一次目标模型。

  2. Update (更新)

    • 将所有残影沿 Y 轴向上移动。
    • 随着时间流逝,减小 opacityMultiplier (透明度)。
    • 当生命周期结束,从场景移除并销毁内存。

JavaScript

spawn(originPosition, originRotation, scale) {
    if (this.sourceGeoGroups.length === 0) return;

    const ghostGroup = new THREE.Group();
    
    // 技巧:每次生成时微调颜色,让能量场色彩更丰富
    const instanceMat = this.energyMaterial.clone();
    instanceMat.uniforms.color.value.setHSL(0.5 + Math.random() * 0.1, 1.0, 0.5);

    this.sourceGeoGroups.forEach(template => {
        const mesh = template.clone();
        mesh.material = instanceMat;
        ghostGroup.add(mesh);
    });

    // ...设置位置、旋转、缩放...
    
    // 稍微向上偏移一点出生点,避免和实体模型完全重叠导致 Z-Fighting 闪烁
    ghostGroup.position.y += 0.05;

    this.scene.add(ghostGroup);
    this.ghosts.push({
        mesh: ghostGroup,
        materialBtn: instanceMat, 
        life: 1.0,
        speed: 1.5
    });
}

为什么会有“扫描”的感觉?

因为我们使用了 AdditiveBlending(叠加混合)。

当新的残影生成时,它离实体模型很近。实体模型本身可能有一个基础亮度,加上残影的亮度,重叠部分会爆亮。随着残影上升,它与本体分离,光线被“拉”开了。这种高亮 -> 分离 -> 消失的过程,视觉上就形成了强烈的能量扫描感。

![效果图占位:建议放一张局部特写,展示边缘重叠变亮的效果]


性能优化小贴士

在代码中,有一个细节处理:

JavaScript

// update 循环中
ghost.materialBtn.dispose(); // 清理内存

由于我们不断地 new ShaderMaterial(或者 clone),这会创建大量的 WebGL Program。如果不手动 dispose,Three.js 可能会保留这些材质引用,导致内存泄漏。虽然在 Demo 级别无所谓,但在生产环境中,建议使用材质池(Material Pool)或者InstancedMesh来复用资源,避免频繁创建销毁。

完整体验与源码

这个特效非常适合用于展示高科技感的 3D 模型,比如无人机、芯片、或者飞船内部结构。

如果你想看完整的运行效果,或者想直接 Copy 源码运行,我已经整理在文章开头了。而如果你想在一个可视化的环境中直接调整这些 Shader 参数,而不用每次都改代码,欢迎尝试我开发的 Web3D 编辑器:

🌌 Meteor3DEditor

Meteor3D 是一个基于 Web 的轻量级 3D 编辑器,旨在简化 Three.js 的场景搭建和特效调试。

如果你觉得这个特效对你有帮助,欢迎来 GitHub 点个 Star ⭐️,也欢迎提 Issue 交流更多的 Shader 技巧!


总结

  1. Pow(6.0) 制造锐利边缘。
  2. Minus Offset 去除中心雾化。
  3. AdditiveBlending 制造光的叠加感。
  4. DoubleSide 增加模型内部细节的透视感。

希望这篇文章能给你的 WebGL 开发带来一点灵感!

❌