学习threejs,打造虹彩编织球体
本文详细介绍如何基于threejs在三维场景中使用ShaderMaterial自定义着色器材质,打造虹彩编织球体,亲测可用。希望能帮助到您。一起学习,加油!加油!
👨⚕️ 主页: 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>
效果如下:
源码
更多推荐



所有评论(0)