👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️THREE.ShaderMaterial](#1.1 ☘️THREE.ShaderMaterial)
-
- [1.1.1 ☘️注意事项](#1.1.1 ☘️注意事项)
- [1.1.2 ☘️构造函数](#1.1.2 ☘️构造函数)
- [1.1.3 ☘️属性](#1.1.3 ☘️属性)
- [1.1.4 ☘️方法](#1.1.4 ☘️方法)
- 二、🍀打造交互式花卉生成器
-
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#2. ☘️代码样例)
一、🍀前言
本文详细介绍如何基于threejs在三维场景中打造交互式花卉生成器,亲测可用。希望能帮助到您。一起学习,加油!加油!
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 的显式值,并且可以在预处理器语句中使用。
javascript
#pragma unroll_loop_start
for ( int i = 0; i < 10; i ++ ) {
// ...
}
#pragma unroll_loop_end
代码示例
javascript
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。这可以避免在缓冲区数据丢失时出错。
javascript
this.defaultAttributeValues = {
'color': [ 1, 1, 1 ],
'uv': [ 0, 0 ],
'uv2': [ 0, 0 ]
};
.defines : Object
使用 #define 指令在GLSL代码为顶点着色器和片段着色器定义自定义常量;每个键/值对产生一行定义语句:
javascript
defines: {
FOO: 15,
BAR: true
}
这将在GLSL代码中产生如下定义语句:
javascript
#define FOO 15
#define BAR true
.extensions : Object
一个有如下属性的对象:
javascript
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
如下形式的对象:
javascript
{ "uniform1": { value: 1.0 }, "uniform2": { value: 2 } }
指定要传递给shader代码的uniforms;键为uniform的名称,值(value)是如下形式:
javascript
{ 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. ☘️实现思路
一个交互式的 GPU 着色器视觉特效 Demo。用户点击/触摸屏幕时,会在光标(指针)位置产生花朵/茎的绘制效果(通过 WebGL shader 实现);还有一个按钮可以"清屏"(clean),让画面重置或淡出。视觉风格比较抽象,依赖噪声函数(noise)来创造自然感、边缘扰动、花瓣变形等。具体代码参考下面代码样例。
2. ☘️代码样例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>花卉生成器</title>
<style>
html, body {
overflow: hidden;
padding: 0;
margin: 0;
}
.container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column-reverse;
align-items: start;
}
.clean-btn {
z-index: 1;
font-family: sans-serif;
font-size: 15px;
color: white;
text-shadow: 0 0 10px #000000;
user-select: none;
padding: 0 0 15px 25px;
cursor: pointer;
text-decoration: underline;
opacity: .5;
}
canvas {
position: absolute;
top: 0;
left: 0;
display: block;
}
.name {
position: fixed;
top: 50%;
left: 50%;
width: 100%;
transform: translate(-50%, -50%);
color: white;
text-align: center;
font-size: 4vw;
text-shadow: 0 0 5px #000000;
user-select: none;
pointer-events: none;
}
@media all and (min-width: 640px) {
.name {
font-size: 45px
}
}
</style>
</head>
<body>
<div class="container">
<canvas id="canvas"></canvas>
<div class="clean-btn">
clean the screen
</div>
</div>
<div class="name">
Click To Add Flowers
</div>
</body>
<script type="x-shader/x-fragment" id="fragmentShader">
#define PI 3.14159265359
uniform float u_ratio;
uniform vec2 u_cursor;
uniform float u_stop_time;
uniform float u_clean;
uniform vec2 u_stop_randomizer;
uniform sampler2D u_texture;
varying vec2 vUv;
// --------------------------------
// 2D noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m = m*m;
m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
float get_flower_shape(vec2 _p, float _pet_n, float _angle, float _outline) {
_angle *= 3.;
_p = vec2(_p.x * cos(_angle) - _p.y * sin(_angle),
_p.x * sin(_angle) + _p.y * cos(_angle));
float a = atan(_p.y, _p.x);
float flower_sectoral_shape = pow(abs(sin(a * _pet_n)), .4) + .25;
vec2 flower_size_range = vec2(.03, .1);
float size = flower_size_range[0] + u_stop_randomizer[0] * flower_size_range[1];
float flower_radial_shape = pow(length(_p) / size, 2.);
flower_radial_shape -= .1 * sin(8. * a); // add noise
flower_radial_shape = max(.1, flower_radial_shape);
flower_radial_shape += smoothstep(0., 0.03, -_p.y + .2 * abs(_p.x));
float grow_time = step(.25, u_stop_time) * pow(u_stop_time, .3);
float flower_shape = 1. - smoothstep(0., flower_sectoral_shape, _outline * flower_radial_shape / grow_time);
flower_shape *= (1. - step(1., grow_time));
return flower_shape;
}
float get_stem_shape(vec2 _p, vec2 _uv, float _w, float _angle) {
_w = max(.004, _w);
float x_offset = _p.y * sin(_angle);
x_offset *= pow(3. * _uv.y, 2.);
_p.x -= x_offset;
// add horizontal noise to the cursor coordinale
float noise_power = .5;
float cursor_horizontal_noise = noise_power * snoise(2. * _uv * u_stop_randomizer[0]);
cursor_horizontal_noise *= pow(dot(_p.y, _p.y), .6);// moise to be zero at cursor
cursor_horizontal_noise *= pow(dot(_uv.y, _uv.y), .3);// moise to be zero at bottom
_p.x += cursor_horizontal_noise;
// vertical line through the cursor point (_p.x)
float left = smoothstep(-_w, 0., _p.x);
float right = 1. - smoothstep(0., _w, _p.x);
float stem_shape = left * right;
// make it grow + don't go up to the cursor point
float grow_time = 1. - smoothstep(0., .2, u_stop_time);
float stem_top_mask = smoothstep(0., pow(grow_time, .5), .03 -_p.y);
stem_shape *= stem_top_mask;
// stop drawing once done
stem_shape *= (1. - step(.17, u_stop_time));
return stem_shape;
}
void main() {
vec3 base = texture2D(u_texture, vUv).xyz;
vec2 uv = vUv;
uv.x *= u_ratio;
vec2 cursor = vUv - u_cursor.xy;
cursor.x *= u_ratio;
vec3 stem_color = vec3(.1 + u_stop_randomizer[0] * .6, .6, .2);
vec3 flower_color = vec3(.6 + .5 * u_stop_randomizer[1], .1, .9 - .5 * u_stop_randomizer[1]);
float angle = .5 * (u_stop_randomizer[0] - .5);
float stem_shape = get_stem_shape(cursor, uv, .003, angle);
stem_shape += get_stem_shape(cursor + vec2(0., .2 + .5 * u_stop_randomizer[0]), uv, .003, angle);
float stem_mask = 1. - get_stem_shape(cursor, uv, .004, angle);
stem_mask -= get_stem_shape(cursor + vec2(0., .2 + .5 * u_stop_randomizer[0]), uv, .004, angle);
float petals_back_number = 1. + floor(u_stop_randomizer[0] * 2.);
float angle_offset = -(2. * step(0., angle) - 1.) * .1 * u_stop_time;
float flower_back_shape = get_flower_shape(cursor, petals_back_number, angle + angle_offset, 1.5);
float flower_back_mask = 1. - get_flower_shape(cursor, petals_back_number, angle + angle_offset, 1.6);
float petals_front_number = 2. + floor(u_stop_randomizer[1] * 2.);
float flower_front_shape = get_flower_shape(cursor, petals_front_number, angle, 1.);
float flower_front_mask = 1. - get_flower_shape(cursor, petals_front_number, angle, .95);
vec3 color = base;
color *= stem_mask;
color *= flower_back_mask;
color *= flower_front_mask;
color += (stem_shape * stem_color);
color += (flower_back_shape * (flower_color + vec3(0., .8 * u_stop_time, 0.)));
color += (flower_front_shape * flower_color);
color.r *= 1. - (.5 * flower_back_shape * flower_front_shape);
color.b *= 1. - (flower_back_shape * flower_front_shape);
color *= u_clean;
gl_FragColor = vec4(color, 1.);
}
</script>
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.);
}
</script>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.133.1/build/three.module";
const canvasEl = document.querySelector("#canvas");
const cleanBtn = document.querySelector(".clean-btn");
const pointer = {
x: 0.66,
y: 0.3,
clicked: true
};
// for codepen preview
window.setTimeout(() => {
pointer.x = 0.75;
pointer.y = 0.5;
pointer.clicked = true;
}, 700);
let basicMaterial, shaderMaterial;
let renderer = new THREE.WebGLRenderer({
canvas: canvasEl,
alpha: true
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
let sceneShader = new THREE.Scene();
let sceneBasic = new THREE.Scene();
let camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);
let clock = new THREE.Clock();
let renderTargets = [
new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight),
new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight)
];
createPlane();
updateSize();
window.addEventListener("resize", () => {
updateSize();
cleanCanvas();
});
render();
let isTouchScreen = false;
window.addEventListener("click", (e) => {
if (!isTouchScreen) {
pointer.x = e.pageX / window.innerWidth;
pointer.y = e.pageY / window.innerHeight;
pointer.clicked = true;
}
});
window.addEventListener("touchstart", (e) => {
isTouchScreen = true;
pointer.x = e.targetTouches[0].pageX / window.innerWidth;
pointer.y = e.targetTouches[0].pageY / window.innerHeight;
pointer.clicked = true;
});
cleanBtn.addEventListener("click", cleanCanvas);
function cleanCanvas() {
pointer.vanishCanvas = true;
setTimeout(() => {
pointer.vanishCanvas = false;
}, 50);
}
function createPlane() {
shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
u_stop_time: { type: "f", value: 0 },
u_stop_randomizer: {
type: "v2",
value: new THREE.Vector2(Math.random(), Math.random())
},
u_cursor: { type: "v2", value: new THREE.Vector2(pointer.x, pointer.y) },
u_ratio: { type: "f", value: window.innerWidth / window.innerHeight },
u_texture: { type: "t", value: null },
u_clean: { type: "f", value: 1 }
},
vertexShader: document.getElementById("vertexShader").textContent,
fragmentShader: document.getElementById("fragmentShader").textContent
});
basicMaterial = new THREE.MeshBasicMaterial();
const planeGeometry = new THREE.PlaneGeometry(2, 2);
const planeBasic = new THREE.Mesh(planeGeometry, basicMaterial);
const planeShader = new THREE.Mesh(planeGeometry, shaderMaterial);
sceneBasic.add(planeBasic);
sceneShader.add(planeShader);
}
function render() {
shaderMaterial.uniforms.u_clean.value = pointer.vanishCanvas ? 0 : 1;
shaderMaterial.uniforms.u_texture.value = renderTargets[0].texture;
if (pointer.clicked) {
shaderMaterial.uniforms.u_cursor.value = new THREE.Vector2(
pointer.x,
1 - pointer.y
);
shaderMaterial.uniforms.u_stop_randomizer.value = new THREE.Vector2(
Math.random(),
Math.random()
);
shaderMaterial.uniforms.u_stop_time.value = 0;
pointer.clicked = false;
}
shaderMaterial.uniforms.u_stop_time.value += clock.getDelta();
renderer.setRenderTarget(renderTargets[1]);
renderer.render(sceneShader, camera);
basicMaterial.map = renderTargets[1].texture;
renderer.setRenderTarget(null);
renderer.render(sceneBasic, camera);
let tmp = renderTargets[0];
renderTargets[0] = renderTargets[1];
renderTargets[1] = tmp;
requestAnimationFrame(render);
}
function updateSize() {
shaderMaterial.uniforms.u_ratio.value = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</html>
效果如下:
源码