学习threejs,打造交互式花卉生成器

👨‍⚕️ 主页: gis分享者

👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!

👨‍⚕️ 收录于专栏:threejs 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>

效果如下:

源码

相关推荐
猫头虎1 天前
OpenAI发布构建AI智能体的实践指南:实用框架、设计模式与最佳实践解析
人工智能·设计模式·开源·aigc·交互·pip·ai-native
Cloud Traveler2 天前
【征文计划】Rokid CXR-M SDK全解析:从设备连接到语音交互的AR协同开发指南
ar·交互
@PHARAOH2 天前
WHAT - 前端性能指标(交互和响应性能指标)
前端·交互
蓝天智能2 天前
QT QML交互原理:信号与槽机制
开发语言·qt·交互
三掌柜6663 天前
突破AR视觉交互边界:Unity赋能Rokid AR眼镜实现高精度图像识别与实时跟踪
unity·ar·交互
猫头虎3 天前
Paper2Agent:将科研论文转化为可交互的AI智能体工具项目
人工智能·prompt·aigc·交互·pip·agi·ai-native
北城以北88883 天前
Vue-- Axios 交互(二)
javascript·vue.js·交互
MARS_AI_4 天前
云蝠智能VoiceAgent 9月升级概览:从功能交互到用户体验
人工智能·自然语言处理·交互·信息与通信·agi
Goona_4 天前
PyQt批量年龄计算工具:从身份证到指定日期的周岁处理
python·小程序·交互·pyqt