学习threejs,打造虹彩编织球体
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
- 一、🍀前言
- 1.1 ☘️THREE.ShaderMaterial
- 1.1.1 ☘️注意事项
- 1.1.2 ☘️构造函数
- 1.1.3 ☘️属性
- 1.1.4 ☘️方法
- 二、🍀打造虹彩编织球体
- 1. ☘️实现思路
- 2. ☘️代码样例
一、🍀前言
本文详细介绍如何基于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 derivativesfragDepth: false, // set to use fragment depth valuesdrawBuffers: false, // set to use draw buffersshaderTextureLOD: 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>
效果如下:

源码
