👨‍⚕️ 主页: gis分享者
👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨‍⚕️ 收录于专栏:threejs gis工程师



一、🍀前言

本文详细介绍如何基于threejs在三维场景中使用ShaderMaterial自定义着色器材质,打造虹彩编织球体,亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️THREE.ShaderMaterial

THREE.ShaderMaterial使用自定义shader渲染的材质。 shader是一个用GLSL编写的小程序 ,在GPU上运行。

1.1.1 ☘️注意事项

  • ShaderMaterial 只有使用 WebGLRenderer 才可以绘制正常, 因为 vertexShader 和
    fragmentShader 属性中GLSL代码必须使用WebGL来编译并运行在GPU中。
  • 从 THREE r72开始,不再支持在ShaderMaterial中直接分配属性。 必须使用
    BufferGeometry实例,使用BufferAttribute实例来定义自定义属性。
  • 从 THREE r77开始,WebGLRenderTarget 或 WebGLCubeRenderTarget
    实例不再被用作uniforms。 必须使用它们的texture 属性。
  • 内置attributes和uniforms与代码一起传递到shaders。
    如果您不希望WebGLProgram向shader代码添加任何内容,则可以使用RawShaderMaterial而不是此类。
  • 您可以使用指令#pragma unroll_loop_start,#pragma unroll_loop_end
    以便通过shader预处理器在GLSL中展开for循环。 该指令必须放在循环的正上方。循环格式必须与定义的标准相对应。
  • 循环必须标准化normalized。
  • 循环变量必须是i。
  • 对于给定的迭代,值 UNROLLED_LOOP_INDEX 将替换为 i 的显式值,并且可以在预处理器语句中使用。
#pragma unroll_loop_start
for ( int i = 0; i < 10; i ++ ) {

	// ...

}
#pragma unroll_loop_end

代码示例

const material = new THREE.ShaderMaterial( {
	uniforms: {
		time: { value: 1.0 },
		resolution: { value: new THREE.Vector2() }
	},
	vertexShader: document.getElementById( 'vertexShader' ).textContent,
	fragmentShader: document.getElementById( 'fragmentShader' ).textContent
} );

1.1.2 ☘️构造函数

ShaderMaterial( parameters : Object )
parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。 材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。

1.1.3 ☘️属性

共有属性请参见其基类Material

.clipping : Boolean
定义此材质是否支持剪裁; 如果渲染器传递clippingPlanes uniform,则为true。默认值为false。

.defaultAttributeValues : Object
当渲染的几何体不包含这些属性但材质包含这些属性时,这些默认值将传递给shaders。这可以避免在缓冲区数据丢失时出错。

this.defaultAttributeValues = {
	'color': [ 1, 1, 1 ],
	'uv': [ 0, 0 ],
	'uv2': [ 0, 0 ]
};

.defines : Object
使用 #define 指令在GLSL代码为顶点着色器和片段着色器定义自定义常量;每个键/值对产生一行定义语句:

defines: {
	FOO: 15,
	BAR: true
}

这将在GLSL代码中产生如下定义语句:

#define FOO 15
#define BAR true

.extensions : Object
一个有如下属性的对象:

this.extensions = {
	derivatives: false, // set to use derivatives
	fragDepth: false, // set to use fragment depth values
	drawBuffers: false, // set to use draw buffers
	shaderTextureLOD: false // set to use shader texture LOD
};

.fog : Boolean
定义材质颜色是否受全局雾设置的影响; 如果将fog uniforms传递给shader,则为true。默认值为false。

.fragmentShader : String
片元着色器的GLSL代码。这是shader程序的实际代码。在上面的例子中, vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。

.glslVersion : String
定义自定义着色器代码的 GLSL 版本。仅与 WebGL 2 相关,以便定义是否指定 GLSL 3.0。有效值为 THREE.GLSL1 或 THREE.GLSL3。默认为空。

.index0AttributeName : String
如果设置,则调用gl.bindAttribLocation 将通用顶点索引绑定到属性变量。默认值未定义。

.isShaderMaterial : Boolean
只读标志,用于检查给定对象是否属于 ShaderMaterial 类型。

.lights : Boolean
材质是否受到光照的影响。默认值为 false。如果传递与光照相关的uniform数据到这个材质,则为true。默认是false。

.linewidth : Float
控制线框宽度。默认值为1。

由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。

.flatShading : Boolean
定义材质是否使用平面着色进行渲染。默认值为false。

.uniforms : Object
如下形式的对象:

{ "uniform1": { value: 1.0 }, "uniform2": { value: 2 } }

指定要传递给shader代码的uniforms;键为uniform的名称,值(value)是如下形式:

{ value: 1.0 }

这里 value 是uniform的值。名称必须匹配 uniform 的name,和GLSL代码中的定义一样。 注意,uniforms逐帧被刷新,所以更新uniform值将立即更新GLSL代码中的相应值。

.uniformsNeedUpdate : Boolean
可用于在 Object3D.onBeforeRender() 中更改制服时强制进行制服更新。默认为假。

.vertexColors : Boolean
定义是否使用顶点着色。默认为假。

.vertexShader : String
顶点着色器的GLSL代码。这是shader程序的实际代码。 在上面的例子中,vertexShader 和 fragmentShader 代码是从DOM(HTML文档)中获取的; 它也可以作为一个字符串直接传递或者通过AJAX加载。

.wireframe : Boolean
将几何体渲染为线框(通过GL_LINES而不是GL_TRIANGLES)。默认值为false(即渲染为平面多边形)。

.wireframeLinewidth : Float
控制线框宽度。默认值为1。

由于OpenGL Core Profile与大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。

1.1.4 ☘️方法

共有方法请参见其基类Material

.clone () : ShaderMaterial this : ShaderMaterial
创建该材质的一个浅拷贝。需要注意的是,vertexShader和fragmentShader使用引用拷贝; attributes的定义也是如此; 这意味着,克隆的材质将共享相同的编译WebGLProgram; 但是,uniforms 是 值拷贝,这样对不同的材质我们可以有不同的uniforms变量。

二、🍀打造虹彩编织球体

1. ☘️实现思路

使用 ParametricGeometry 和 GLSL 着色器渲染折射光线的虹彩编织球体;单击或点击以触发围绕其盘旋的发光能量轨道。具体代码参考代码样例,可以直接运行。

2. ☘️代码样例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>虹彩编织球体</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background: radial-gradient(ellipse at center, #1b2735 0%, #090a0f 100%);
            color: white;
            font-family: 'Inter', sans-serif;
        }
        canvas {
            display: block;
        }
        .info {
            position: absolute;
            top: 10px;
            left: 10px;
            padding: 10px;
            background: rgba(0,0,0,0.5);
            border-radius: 8px;
            font-size: 14px;
        }
    </style>
    <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/"
      }
    }
    </script>
</head>
<body>
    <div class="info">Drag to rotate. Click/Tap the shape for energy orbits.</div>
</body>
<script type="module">
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
  import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
  import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

  let scene, camera, renderer, controls, composer;
  let mainObject, reflectionObject, originalMaterial, reflectionMaterial;
  let godRaysRenderTarget, godRaysMaterial, godraysPass;
  const clock = new THREE.Clock();

  let particles = [];
  let particleSystem, particleGeometry, particleMaterial;
  let particlePositions, particleColors;
  const maxParticles = 2000;
  let isEmitting = false;
  let emitStartTime = 0;
  let emitDuration = 3;
  const orbitCount = 4;
  const numNewPerOrbit = 3;
  const startY = -3.5;
  const heightRange = 7;
  const speed = heightRange / 3;
  const numTurns = 3.0;


  const vertexShader = `
        uniform float uTime;
        varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vViewDirection;
        varying vec3 vWorldPosition;
        void main() {
            vUv = uv;
            vec3 pos = position;
            vec4 worldPosition = modelMatrix * vec4(pos, 1.0);
            vWorldPosition = worldPosition.xyz;
            vNormal = normalize(normalMatrix * normal);
            vViewDirection = normalize(cameraPosition - worldPosition.xyz);
            gl_Position = projectionMatrix * viewMatrix * worldPosition;
        }
    `;

  const fragmentShader = `
        uniform float uTime;
        uniform float uReflection;
        uniform float uIOR;

        varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vViewDirection;
        varying vec3 vWorldPosition;

        vec3 hsv2rgb(vec3 c) {
            vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
            vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
            return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
        }

        vec3 getEnvColor(vec3 dir) {
            float h = dir.y * 0.5 + 0.5;
            vec3 color1 = hsv2rgb(vec3(0.6, 1.0, 1.0));
            vec3 color2 = hsv2rgb(vec3(0.9, 1.0, 1.0));
            vec3 color3 = hsv2rgb(vec3(0.1, 1.0, 1.0));

            vec3 finalColor = mix(color1, color2, h);
            finalColor = mix(finalColor, color3, smoothstep(0.7, 1.0, h));

            return finalColor;
        }

        void main() {
            vec3 normal = normalize(vNormal);
            vec3 viewDir = normalize(vViewDirection);

            vec3 refractDirR = refract(-viewDir, normal, 1.0 / (uIOR + 0.05));
            vec3 refractDirG = refract(-viewDir, normal, 1.0 / uIOR);
            vec3 refractDirB = refract(-viewDir, normal, 1.0 / (uIOR - 0.05));

            vec3 refractedColor;
            refractedColor.r = getEnvColor(refractDirR).r;
            refractedColor.g = getEnvColor(refractDirG).g;
            refractedColor.b = getEnvColor(refractDirB).b;

            vec3 reflectDir = reflect(-viewDir, normal);
            vec3 reflectedColor = getEnvColor(reflectDir);

            float fresnel = 0.15 + 0.85 * pow(1.0 + dot(viewDir, normal), 3.0);

            vec3 baseColor = mix(refractedColor, reflectedColor, fresnel);

            float hue = vUv.x * 3.0 + uTime * 0.3;
            vec3 iridescentColor = hsv2rgb(vec3(mod(hue, 1.0), 0.8, 1.0));

            float lineFrequency = 40.0;
            float lineSharpness = 60.0;
            float line = pow(abs(sin(vUv.y * lineFrequency)), lineSharpness);

            vec3 glow = iridescentColor * line * 2.5;

            vec3 finalColor = baseColor + glow;

            float opacity = 0.5 + fresnel * 0.5;
            opacity = max(opacity, line);

            if (uReflection > 0.5) {
                float fade = smoothstep(-5.0, -3.5, vWorldPosition.y);
                finalColor *= fade;
                opacity *= fade * 0.6;
            }

            gl_FragColor = vec4(finalColor, opacity);
        }
    `;

  const GodRaysShader = {
    uniforms: {
      tDiffuse: { value: null },
      tGodRaysSource: { value: null },
      uLightPosition: { value: new THREE.Vector2(0.5, 0.5) },
      uExposure: { value: 0.15 },
      uDecay: { value: 0.95 },
      uDensity: { value: 0.97 },
      uWeight: { value: 0.6 },
    },
    vertexShader: `
            varying vec2 vUv;
            void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
        `,
    fragmentShader: `
            uniform sampler2D tDiffuse;
            uniform sampler2D tGodRaysSource;
            uniform vec2 uLightPosition;
            uniform float uExposure;
            uniform float uDecay;
            uniform float uDensity;
            uniform float uWeight;
            varying vec2 vUv;
            const int NUM_SAMPLES = 100;

            void main() {
                vec2 texCoord = vUv;
                vec2 delta = uLightPosition - texCoord;
                delta *= 1.0 / float(NUM_SAMPLES) * uDensity;

                float illuminationDecay = 1.0;
                vec4 accumulatedRays = vec4(0.0);

                for (int i = 0; i < NUM_SAMPLES; i++) {
                    texCoord += delta;
                    vec4 sampleColor = texture2D(tGodRaysSource, texCoord);
                    sampleColor *= illuminationDecay;
                    accumulatedRays += sampleColor;
                    illuminationDecay *= uDecay;
                }

                accumulatedRays *= uWeight;

                vec4 originalColor = texture2D(tDiffuse, vUv);

                gl_FragColor = originalColor + (accumulatedRays * uExposure);
            }
        `
  };

  function fract(x) { return x - Math.floor(x); }
  function clamp(x, min, max) { return Math.max(min, Math.min(max, x)); }
  function mix(a, b, t) { return a * (1 - t) + b * t; }
  function hsv2rgb([h, s, v]) {
    const k = [1, 2/3, 1/3, 3];
    const p = [
      Math.abs(fract(h + k[0]) * 6 - k[3]),
      Math.abs(fract(h + k[1]) * 6 - k[3]),
      Math.abs(fract(h + k[2]) * 6 - k[3])
    ];
    return [
      v * mix(1, clamp(p[0] - 1, 0, 1), s),
      v * mix(1, clamp(p[1] - 1, 0, 1), s),
      v * mix(1, clamp(p[2] - 1, 0, 1), s)
    ];
  }


  function init() {
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    document.body.appendChild(renderer.domElement);

    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
    camera.position.set(0, 0, 18);

    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.5;

    godRaysRenderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);

    godRaysMaterial = new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.6 });

    const parametricFunction = (u, v, target) => {
      u *= Math.PI;
      v *= Math.PI * 2;

      const R = 3.0;
      const weaves = 15.0;
      const thickness = 0.3;

      let mod = Math.sin(u * weaves) * Math.cos(v * weaves) * thickness;

      let x = (R + mod) * Math.sin(u) * Math.cos(v);
      let y = (R + mod) * Math.sin(u) * Math.sin(v);
      let z = (R + mod) * Math.cos(u);

      target.set(x, y, z);
    };

    const geometry = new ParametricGeometry(parametricFunction, 256, 128);

    originalMaterial = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms: {
        uTime: { value: 0 },
        uReflection: { value: 0 },
        uIOR: { value: 1.3 }
      },
      transparent: true,
      side: THREE.DoubleSide,
      blending: THREE.NormalBlending,
      depthWrite: false,
    });

    mainObject = new THREE.Mesh(geometry, originalMaterial);
    scene.add(mainObject);

    reflectionMaterial = originalMaterial.clone();
    reflectionMaterial.uniforms.uReflection.value = 1;
    reflectionObject = new THREE.Mesh(geometry, reflectionMaterial);
    reflectionObject.scale.y = -1;
    reflectionObject.position.y = -7.0;
    scene.add(reflectionObject);

    particlePositions = new Float32Array(maxParticles * 3);
    particleColors = new Float32Array(maxParticles * 3);
    particleGeometry = new THREE.BufferGeometry();
    particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
    particleGeometry.setAttribute('color', new THREE.BufferAttribute(particleColors, 3));
    particleGeometry.setDrawRange(0, 0);

    particleMaterial = new THREE.PointsMaterial({
      size: 0.18,
      vertexColors: true,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });

    particleSystem = new THREE.Points(particleGeometry, particleMaterial);
    scene.add(particleSystem);


    composer = new EffectComposer(renderer);
    composer.addPass(new RenderPass(scene, camera));

    godraysPass = new ShaderPass(GodRaysShader);
    godraysPass.renderToScreen = true;

    godraysPass.uniforms.uExposure.value = 0.12;
    godraysPass.uniforms.uWeight.value = 0.5;

    composer.addPass(godraysPass);

    window.addEventListener('resize', onWindowResize, false);
    renderer.domElement.addEventListener('pointerdown', onPointerDown);
  }

  function onPointerDown(event) {
    event.preventDefault();
    const rect = renderer.domElement.getBoundingClientRect();
    const mouse = new THREE.Vector2();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObject(mainObject);
    if (intersects.length > 0) {
      triggerEnergyOrbits();
    }
  }

  function triggerEnergyOrbits() {
    if (!isEmitting) {
      isEmitting = true;
      emitStartTime = clock.getElapsedTime();
      particles = [];
    }
  }

  function emitParticles(time) {
    for (let orb = 0; orb < orbitCount; orb++) {
      for (let j = 0; j < numNewPerOrbit; j++) {
        if (particles.length >= maxParticles) return;
        const p = {
          orbitIndex: orb,
          startTime: time,
          speed: speed + Math.random() * 0.5,
          phase: orb * 2 * Math.PI / orbitCount + Math.random() * 0.2,
          r: 3.8 + Math.random() * 0.2,
          h: (orb / orbitCount) + 0.6 + Math.random() * 0.1
        };
        particles.push(p);
      }
    }
  }

  function updateParticles(time) {
    let activeCount = 0;
    for (let i = 0; i < particles.length; i++) {
      const p = particles[i];
      const age = time - p.startTime;
      const progress = age * p.speed / heightRange;
      if (progress > 1) continue;

      const theta = p.phase + progress * Math.PI * 2 * numTurns;
      const y = startY + progress * heightRange;
      const x = p.r * Math.cos(theta);
      const z = p.r * Math.sin(theta);

      const index = activeCount * 3;
      particlePositions[index] = x;
      particlePositions[index + 1] = y;
      particlePositions[index + 2] = z;

      const v = 1.0 - Math.pow(progress, 3.0);
      const [r, g, b] = hsv2rgb([p.h % 1, 0.8, v]);
      particleColors[index] = r;
      particleColors[index + 1] = g;
      particleColors[index + 2] = b;

      activeCount++;
    }
    particles = particles.filter(p => (time - p.startTime) * p.speed / heightRange <= 1);

    particleGeometry.setDrawRange(0, activeCount);
    if (activeCount > 0) {
      particleGeometry.attributes.position.needsUpdate = true;
      particleGeometry.attributes.color.needsUpdate = true;
    }
  }

  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    godRaysRenderTarget.setSize(window.innerWidth, window.innerHeight);
    composer.setSize(window.innerWidth, window.innerHeight);
  }

  function animate() {
    requestAnimationFrame(animate);

    const time = clock.getElapsedTime();

    originalMaterial.uniforms.uTime.value = time;
    reflectionMaterial.uniforms.uTime.value = time;

    reflectionObject.rotation.copy(mainObject.rotation);

    controls.update();

    if (isEmitting) {
      const emitTime = time - emitStartTime;
      if (emitTime < emitDuration) {
        emitParticles(time);
      } else {
        isEmitting = false;
      }
    }

    updateParticles(time);

    mainObject.material = godRaysMaterial;
    reflectionObject.material = godRaysMaterial;

    renderer.setRenderTarget(godRaysRenderTarget);
    renderer.clear();
    renderer.render(scene, camera);

    mainObject.material = originalMaterial;
    reflectionObject.material = reflectionMaterial;

    godraysPass.uniforms.tGodRaysSource.value = godRaysRenderTarget.texture;

    renderer.setRenderTarget(null);
    composer.render();
  }

  init();
  animate();
</script>
</html>

效果如下:
在这里插入图片描述
源码

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐