NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
我受到了创造弹性材料的启发,并想分享如何复制它。 了解 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、实现拖动控制
我们需要三个事件侦听器来设置我们的控件, mousedown
、 mousemove
和 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翻译整理,转载请标明出处