学习threejs,打造交互式花卉生成器
👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
- 一、🍀前言
- 1.1 ☘️THREE.ShaderMaterial
- 1.1.1 ☘️注意事项
- 1.1.2 ☘️构造函数
- 1.1.3 ☘️属性
- 1.1.4 ☘️方法
- 二、🍀打造交互式花卉生成器
- 1. ☘️实现思路
- 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 的显式值,并且可以在预处理器语句中使用。
#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. ☘️实现思路
一个交互式的 GPU 着色器视觉特效 Demo。用户点击/触摸屏幕时,会在光标(指针)位置产生花朵/茎的绘制效果(通过 WebGL shader 实现);还有一个按钮可以“清屏”(clean),让画面重置或淡出。视觉风格比较抽象,依赖噪声函数(noise)来创造自然感、边缘扰动、花瓣变形等。具体代码参考下面代码样例。
2. ☘️代码样例
<!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.14159265359uniform 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 noisevec3 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 noiseflower_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 coordinalefloat 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 cursorcursor_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 pointfloat 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 donestem_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 previewwindow.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>
效果如下:
源码