NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包

我受到了创造弹性材料的启发,并想分享如何复制它。 了解 Javascript、Three.js 和一些关于着色器的概念将有助于继续学习!

演示可以在这里查看,源代码可以在这里找到。

1、创建3D场景

这是我们的出发点。 带有相机、渲染器和红色平面的基本 Three.js 场景。

2、设置着色器材质

让我们用 Three.js 的 ShaderMaterial 替换场景中的材质。 这个类为我们提供了大部分必要的常量和属性(位置、uv、modelViewProjectionMatrices 等)。

创建两个新文件,fragment.glsl 和 vertex.glsl。

//fragment.glsl
void main() {
  gl_FragColor = vec4(1. 0., 0., 1.);
}
//vertex.glsl
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}

将glsl文件导入我们的脚本并实例化材质。

import fragment from "./fragment.glsl";
import vertex from "./vertex.glsl";

...

setMaterial() {
  this.material = new THREE.ShaderMaterial({
    vertexShader: vertex,
    fragmentShader: fragment
  });
}

我们应该再次看到我们的红色平面。🥳

3、使用光线投射器

接下来,让我们解决交互性问题。

我们将添加一个 Three.js的 Raycaster 以确定用户是否试图拖动红色平面。

const raycaster = new THREE.Raycaster();

window.addEventListener("mousedown", (event) => {
  const x = 2 * (event.clientX / window.innerWidth) - 1;
  const y = -2 * (event.clientX / window.innerHeight) + 1;
  raycaster.setFromCamera({x, y}, this.camera);
  const intersect = raycaster.intersectObject(this.mesh);
  if (intersect.length) {
    const target = intersect[0];
  };
};);

在事件侦听器的回调中,我们调用 setFromCamera 方法,该方法从相机位置向屏幕上的指定点发射光线。 该方法期望点位置被格式化为标准化设备坐标 (NDC),即我们的视口尺寸,映射到范围 -1 => 1。

intersectObject 方法检查光线和我们作为参数传递给它的对象之间的交集。 如果我们命中,它会在一个数组中返回一个 Javascript 对象,其中包含交点的位置、它的 uv 位置、到相机的距离和其他信息。 我们稍后将通过 uniforms 变量将此信息传递给我们的着色器。

4、实现拖动控制

我们需要三个事件侦听器来设置我们的控件, mousedownmousemove 和  mouseup

onMouseDown - 鼠标按下时:

  • 使用光线投射器检查用户是否点击了我们的平面。
  • 如果是,则将位置作为 uniforms 传递给我们的材质,并设置布尔值以显示用户当前正在拖动。

onMouseMove - 鼠标移动时:

  • 如果用户正在拖动,使用光线投射器获取交点并将其位置传递给我们的材质。 由于我们的平面没有覆盖屏幕的整个视口,因此我们需要创建一个横跨屏幕整个视口的虚拟平面。

onMouseUp - 鼠标释放时:

  • 如果用户正在拖动平面,重置我们的布尔值以显示用户已释放平面。
 ...
  
  setTouchTarget() {
    this.touchTarget = new THREE.Mesh(
      new THREE.PlaneGeometry(2000, 2000),
      new THREE.MeshBasicMaterial()
    );
  }
  
  onMouseDown(event) {
    const x = 2 * (event.clientX / this.width) - 1;
    const y = -2 * (event.clientY / this.height) + 1;
    this.raycaster.setFromCamera({ x, y }, this.camera);
    const intersect = this.raycaster.intersectObject(this.mesh);
    if (intersect.length) {
      this.isDragging = true;
      const startPosition = intersect[0].point;
      this.material.uniforms.uDragStart.value.copy(startPosition);
      this.material.uniforms.uDragTarget.value.copy(startPosition);
    }
  }

  onMouseMove(event) {
    if (!this.isDragging) return;
    const x = 2 * (event.clientX / this.width) - 1;
    const y = -2 * (event.clientY / this.height) + 1;
    this.raycaster.setFromCamera({ x, y }, this.camera);
    const intersect = this.raycaster.intersectObject(this.touchTarget);
    if (intersect.length) {
      const target = intersect[0].point;
      this.material.uniforms.uDragTarget.value.copy(target);
    }
  }

  onMouseUp() {
    if (!this.isDragging) return;
    this.isDragging = false;
  }

...

5、扭曲我们的平面

回到着色器。 我们将修改我们的顶点着色器来扭曲我们的平面。 首先,增加几何体中高度和宽度分段的数量。

this.geometry = new THREE.PlaneGeometry(1,1,100,100);

使用我们的拖动控件的开始和目标位置,我们将创建一个影响范围来确定我们需要在什么位置应用多少扭曲。 这应该,a) 以起始位置为中心,b) 在其中心最强,c) 逐渐向其边缘下降,d) 随着起始位置和目标位置之间的距离而增大。

uniform vec3 uDragStart;
uniform vec3 uDragTarget;

void main() {
    float startToTarget = distance(uDragTarget, uDragStart);
    float distanceToStart = distance(position, uDragStart);
    float influence = distanceToStart / (0. + 0.3 * startToTarget);
    float distortion = exp(influence * -3.2);

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}

我们可以将变形传递给我们的片元着色器以将其可视化。

//fragment.glsl
varying float vDistortion;

void main() {
	gl_FragColor = vec4(vDistortion, 0., 0., 1.);
}
//vertext.glsl
...
varying float vDistortion;

void main() {
    ...
    vDistortion = distortion;
}

最后,使用失真创建一个偏移向量并将其添加到我们的位置属性:

uniform vec3 uDragStart;
uniform vec3 uDragTarget;

varying float vDistortion;

void main() {
    ...

    vec3 stretch = (uDragTarget - uDragStart) * distortion;

    vec3 pos = position;

    newPosition += stretch;
    newPosition.z += distanceToStart * distortion;

    gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.);

    vDistortion = distortion;
}

6、释放

让我们添加一个动画来在用户释放平面时恢复平面的形状。 我们将需要两个额外的 uniforms,一个告诉我们控件已被释放的变量, uDragRelease,以及自释放以来的时间 uDragReleaseTime。 我们还需要一个统一的全局时间,我们将在渲染循环中更新它。

onMouseDown(event) {
  ...
  if (intersect.length) {
    ...
    this.material.uniforms.uDragRelease.value = false;
  }
}

onMouseUp() {
  if (!this.isDragging) return;
  this.isDragging = false;
  this.material.uniforms.uDragReleaseTime.value = this.time;
  this.material.uniforms.uDragRelease.value = true;
}

render() {
  ...
  this.time += 0.01633;
  this.material.uniforms.uTime.value = this.time;
}

修改顶点着色器以抑制失真。

...
if (uDragRelease > 0.) {
  float timeSinceRelease = uTime - uDragReleaseTime;
  distortion *= exp(-3. * timeSinceRelease);
}
vec3 stretch = (uDragTarget - uDragStart) * distortion;

添加一个转换,使它看起来更“有弹性”。 sin 函数运行良好。

if (uDragRelease > 0.) {
  float timeSinceRelease = uTime - uDragRelease;
  distortion *= exp(-3. * timeSinceRelease);
  distortion *= sin(timeSinceRelease * 50.);
}

瞧……


原文链接:Create an Elastic Material with Three.js

BimAnt翻译整理,转载请标明出处