Three.js教程:Face3对象定义Geometry的三角形面

Posted 3D建模

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Three.js教程:Face3对象定义Geometry的三角形面相关的知识,希望对你有一定的参考价值。

推荐:将NSDT场景编辑器加入你的3D工具链
其他系列工具:NSDT简石数字孪生

Face3对象定义Geometry的三角形面

几何体Geometry的三角面属性geometry.faces和缓冲类型几何体BufferGeometry顶点索引属性BufferGeometry.index类似都是顶点位置数据的索引值,用来组织网格模型三角形的绘制。

学习本节课最好对照2.4 顶点索引复用顶点数据学习。

下面代码自定义了一个由两个三角形构成的几何体,两个三角形有两个顶点坐标位置是重合的。

var geometry = new THREE.Geometry(); //声明一个几何体对象Geometry

var p1 = new THREE.Vector3(0, 0, 0); //顶点1坐标
var p2 = new THREE.Vector3(0, 100, 0); //顶点2坐标
var p3 = new THREE.Vector3(50, 0, 0); //顶点3坐标
var p4 = new THREE.Vector3(0, 0, 100); //顶点4坐标
//顶点坐标添加到geometry对象
geometry.vertices.push(p1, p2, p3,p4);

// Face3构造函数创建一个三角面
var face1 = new THREE.Face3(0, 1, 2);
//三角面每个顶点的法向量
var n1 = new THREE.Vector3(0, 0, -1); //三角面Face1顶点1的法向量
var n2 = new THREE.Vector3(0, 0, -1); //三角面2Face2顶点2的法向量
var n3 = new THREE.Vector3(0, 0, -1); //三角面3Face3顶点3的法向量
// 设置三角面Face3三个顶点的法向量
face1.vertexNormals.push(n1,n2,n3);

// 三角面2
var face2 = new THREE.Face3(0, 2, 3);
// 设置三角面法向量
face2.normal=new THREE.Vector3(0, -1, 0);

//三角面face1、face2添加到几何体中
geometry.faces.push(face1,face2);

设置四个顶点

两个三角形有6个顶点,但是两个顶点位置重合的,可以设置4个顶点即可。

var p1 = new THREE.Vector3(0, 0, 0); //顶点1坐标
var p2 = new THREE.Vector3(0, 100, 0); //顶点2坐标
var p3 = new THREE.Vector3(50, 0, 0); //顶点3坐标
var p4 = new THREE.Vector3(0, 0, 100); //顶点4坐标
//顶点坐标添加到geometry对象
geometry.vertices.push(p1, p2, p3,p4);

Face3构建三角形

threejs提供了Face3对象构建三角形,通过Face3构建一个三角形,不要设置顶点位置坐标数据,只需要通过数组索引值从geometry.vertices数组中获得顶点位置坐标数据。

geometry.vertices数组索引0, 1, 2对应的顶点位置坐标数据表示三角形1的三个顶点坐标,索引0, 2, 3对应的顶点位置坐标数据表示三角形2的三个顶点坐标。

// Face3构造函数创建一个三角面
var face1 = new THREE.Face3(0, 1, 2);
// 三角面2
var face2 = new THREE.Face3(0, 2, 3);

三角形法线设置

前面课程将结果网格模型Mesh的几何体Geometry本质上都是一个一个三角形拼接而成,所以可以通过设置三角形的法线方向向量来表示几何体表面各个位置的法线方向向量。

设置三角形法线方向向量有两种方式,一种是直接定义三角形面的法线方向,另一个是定义三角形三个顶点的法线方向数据来表示三角形面法线方向。

使用三维向量THREE.Vector3表示三角形法线方向数值,然后赋值给三角形对象Face3的法线属性Face3.normal

// 三角面2
var face2 = new THREE.Face3(0, 2, 3);
// 设置三角面法向量
face2.normal=new THREE.Vector3(0, -1, 0);

换另一种方式,通过三角形面Face3Face3.vertexNormals属性给三角形的三个顶点分别设置一个顶点法线方向数据。

// Face3构造函数创建一个三角面
var face1 = new THREE.Face3(0, 1, 2);
//三角面每个顶点的法向量
var n1 = new THREE.Vector3(0, 0, -1); //三角面Face1顶点1的法向量
var n2 = new THREE.Vector3(0, 0, -1); //三角面2Face2顶点2的法向量
var n3 = new THREE.Vector3(0, 0, -1); //三角面3Face3顶点3的法向量
// 设置三角面Face3三个顶点的法向量
face1.vertexNormals.push(n1,n2,n3);

三角形颜色设置

三角形颜色设置和三角形法线方向设置类型,可以直接设置三角形颜色,也可以设置三角形三个顶点的颜色。

// 三角形1颜色
face1.color = new THREE.Color(0xffff00);
// 设置三角面face1三个顶点的颜色
face1.color = new THREE.Color(0xff00ff);

通过三角形面Face3.vertexColors属性设置三角形三个顶点颜色。

三个顶点颜色不同三角形面渲染的时候会进行颜色插值计算,测到一个颜色渐变效果。

face1.vertexColors = [
  new THREE.Color(0xffff00),
  new THREE.Color(0xff00ff),
  new THREE.Color(0x00ffff),
]

使用顶点颜色数据的时候,注意设置材质的属性vertexColors属性值为THREE.VertexColors

注意设置三角形Face3的颜色对threejs网格模型Mesh有效,对于点模型Points、线模型Line是无效果,如果想设置点、线模型对应的几何体Geometry的顶点颜色,可以通过Geometry的顶点颜色属性geometry.colors实现。

上一篇:Three.js教程:设置Geometry顶点位置、顶点颜色数据 (mvrlink.com)

下一篇:Three.js教程:访问几何体对象的数据 (mvrlink.com)

Three.js纹理投影简明教程

纹理投影是一种将纹理映射到 3D 对象并使其看起来像是从单个点投影的方法。 把它想象成投射到云上的蝙蝠侠符号,云是我们的对象,蝙蝠侠符号是我们的纹理。 它用于游戏和视觉效果,以及创意世界的更多部分。

工具:使用 NSDT场景编辑器 快速搭建 数字孪生3D场景。

1、纹理投影MVP

首先,让我们设置场景。 每个Three.js项目的场景搭建代码都是一样的,这里就不赘述了。 如果你以前没有做过,你可以熟悉下官方指南。 我个人使用 threejs-modern-app 中的一些实用程序,所以我不需要担心样板代码。

首先我们需要一个相机来投影纹理。

const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 3)
camera.position.set(-1, 1.2, 1.5)
camera.lookAt(0, 0, 0)

然后,我们需要在其上投影纹理的对象。 为了进行投影映射,我们将编写一些自定义着色器代码,因此让我们创建一个新的 ShaderMaterial:

// create the mesh with the projected material
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.ShaderMaterial(
  uniforms:  
    texture:  value: assets.get(textureKey) ,
  ,
  vertexShader: '',
  fragShader: '',
)
const box = new THREE.Mesh(geometry, material)

但是,由于我们可能需要多次使用我们投影的材质,我们可以将它单独放在一个组件中,然后像这样使用它:

class ProjectedMaterial extends THREE.ShaderMaterial 
  constructor( camera, texture ) 
    // ...
  


const material = new ProjectedMaterial(
  camera,
  texture: assets.get(textureKey),
)

现在让我们写着色器!

在着色器代码中,我们基本上会对纹理进行采样,就好像它是从相机投射出来的一样。 不幸的是,这涉及到一些矩阵乘法。 但不要害怕! 我将以一种简单易懂的方式对其进行解释。 如果你想更深入地研究这个主题,可以查看这篇关于矩阵运算的非常好的文章。

在顶点着色器中,我们必须将每个顶点视为从投影相机中查看,所以我们只使用投影相机的 projectionMatrix 和 viewMatrix 而不是场景相机中的那些。 我们使用可变变量将这个转换后的位置传递给片段着色器。

vTexCoords = projectionMatrixCamera * viewMatrixCamera * modelMatrix * vec4(position, 1.0);

在片元着色器中,我们必须将位置从世界空间转换到剪切空间。 我们通过将向量除以它的 .w 分量来做到这一点。 GLSL 内置函数 texture2DProj(或更新的 textureProj)也在内部执行此操作。

在同一行中,我们还将剪切空间范围 [-1, 1] 转换为 uv 查找范围 [0, 1]。 我们使用这个变量稍后从纹理中采样。

vec2 uv = (vTexCoords.xy / vTexCoords.w) * 0.5 + 0.5;

结果如下:

请注意,我们编写了一些代码以仅将纹理投影到立方体面向相机的面上。 默认情况下,每个面都会得到纹理投影,因此我们通过查看法线和相机方向的点积来检查脸是否真的面向相机。 这种技术在照明中确实很常见,如果你想阅读更多有关此主题的信息,请阅读这篇文章。

// this makes sure we don't render the texture also on the back of the object
vec3 projectorDirection = normalize(projPosition - vWorldPosition.xyz);
float dotProduct = dot(vNormal, projectorDirection);
if (dotProduct < 0.0) 
  outColor = vec4(color, 1.0);

第一部分,我们现在想让它看起来像贴在物体上的纹理。

我们只需在开始时保存对象的位置,然后使用它而不是更新后的对象位置来计算投影,这样即使对象之后移动,投影也不会改变。

我们可以将对象初始模型矩阵存储在统一的 savedModelMatrix 中,因此我们的计算变为:

vTexCoords = projectionMatrixCamera * viewMatrixCamera * savedModelMatrix * vec4(position, 1.0);

我们可以公开一个 project() 函数,它将 savedModelMatrix 设置为对象的当前 modelMatrix。

export function project(mesh) 
  // make sure the matrix is updated
  mesh.updateMatrixWorld()

  // we save the object model matrix so it's projected relative
  // to that position, like a snapshot
  mesh.material.uniforms.savedModelMatrix.value.copy(mesh.matrixWorld)

这是我们的最终结果:

就是这样! 现在立方体看起来像是贴上了纹理! 这可以扩展到任何类型的 3D 模型,所以让我们举一个更有趣的例子。

2、纹理投影的有趣案例

对于前面的示例,我们创建了一个用于投影的新相机,但是如果我们使用渲染场景的相同相机进行投影呢? 这样我们就可以准确地看到 2D 图像! 这是因为投影点与视点重合。

另外,让我们尝试投影到多个对象上:

看起来很有趣! 然而,正如你从示例中看到的那样,图像看起来有点扭曲,这是因为纹理被拉伸以填充相机平截头体。 但是如果我们想保留图像的原始比例和尺寸怎么办?

此外,我们根本没有考虑照明。 片段着色器中需要一些代码来说明我们在场景中放置的灯光如何照亮表面。

此外,如果我们想投影到更多的对象上怎么办? 性能会迅速下降。 这就是 GPU 实例化提供帮助的地方! 实例化将繁重的工作转移到 GPU 上,Three.js 最近为其实现了一个易于使用的 API。 唯一的要求是所有实例化对象必须具有相同的几何体和材质。 幸运的是,这就是我们的情况! 所有对象都具有相同的几何形状和材质,唯一的区别是 savedModelMatrix,因为每个对象在投影时都有不同的位置。 但是我们可以将它作为统一传递给每个实例,就像在这个 Three.js 示例中一样。

事情开始变得复杂,但别担心! 我已经对这些东西进行了编码并将其放入three-projected-material库中,因此使用起来更容易,而且你不必每次都重写相同的东西! 如果你对我如何克服剩下的挑战感兴趣,可以去看看。

从现在开始,我们将使用该库。

3、纹理投影进阶

现在我们可以投影到许多对象上并为其设置动画,让我们尝试从中制作一些真正有用的东西。

例如,让我们尝试将其集成到幻灯片中,将图像投影到大量 3D 对象上,然后以有趣的方式对对象进行动画处理。

对于第一个例子,灵感来自 Refik Anadol。 他做了一些非常棒的事情。 但是,我们不能像他那样对速度和力进行全面的模拟,我们需要控制物体的运动; 我们需要它在正确的时间到达正确的地方。

我们通过将对象放置在一些轨迹上来实现这一点:我们定义对象必须遵循的路径,然后在该路径上为对象设置动画。 这是一个 Stack Overflow 答案,解释了实现的原理。

为了进行投影,我们

  • 将元素移动到中间点
  • 执行调用 project() 的纹理投影
  • 将元素放回起点

这是同步发生的,所以用户不会看到任何东西。

现在我们可以自由地以任何我们想要的方式对这些路径进行建模!

但首先,我们必须确保在中间点,元素将正确覆盖图像区域。 为此,我使用了泊松盘采样算法,该算法将点更均匀地分布在表面上,而不是随机定位它们。

this.points = poissonSampling([this.width, this.height], 7.73, 9.66) // innerradius and outerradius

// here is what this.points looks like,
// the z component is 0 for every one of them
// [
//   [
//     2.4135735314978937, --> x
//     0.18438944023363374 --> y
//   ],
//   [
//     2.4783704056100464,
//     0.24572635574719284
//   ],
//   ...

下面我们来看看第一个demo中路径是如何生成的。 在此演示中,大量使用了 perlin 噪声(或者更确切地说是它的开源对应物,open simple noise)。 还要注意 mapRange() 函数(处理中的 map()),它基本上将一个数字从一个区间映射到另一个区间。 另一个执行此操作的库是 d3-scale 及其 d3.scaleLinear()。 还使用了一些缓动函数。

const segments = 51 // must be odds so we have the middle frame
const halfIndex = (segments - 1) / 2
for (let i = 0; i < segments; i++) 
  const offsetX = mapRange(i, 0, segments - 1, startX, endX)

  const noiseAmount = mapRangeTriple(i, 0, halfIndex, segments - 1, 1, 0, 1)
  const frequency = 0.25
  const noiseAmplitude = 0.6
  const noiseY = noise(offsetX * frequency) * noiseAmplitude * eases.quartOut(noiseAmount)
  const scaleY = mapRange(eases.quartIn(1 - noiseAmount), 0, 1, 0.2, 1)

  const offsetZ = mapRangeTriple(i, 0, halfIndex, segments - 1, startZ, 0, endZ)

  // offsetX goes from left to right
  // scaleY shrinks the y before and after the center
  // noiseY is some perlin noise on the y axis
  // offsetZ makes them enter from behind a little bit
  points.push(new THREE.Vector3(x + offsetX, y * scaleY + noiseY, z + offsetZ))

我们可以处理的另一件事是每个元素到达的延迟。 我们在这里也使用了 Perlin 噪音,这使得它们看起来像是“成群结队”地到达。

const frequency = 0.5
const delay = (noise(x * frequency, y * frequency) * 0.5 + 0.5) * delayFactor

我们还在波浪效果中使用了柏林噪声,它修改了曲线的每个点,使其具有“旗帜波浪”效果。

const  frequency, speed, amplitude  = this.webgl.controls.turbulence
const z = noise(x * frequency - time * speed, y * frequency) * amplitude
point.z = targetPoint.z + z

对于鼠标交互,我们检查路径的点是否比某个半径更近,如果是,我们计算从鼠标点到路径点的向量。 然后我们沿着该向量的方向稍微移动路径点。 为此,我们使用 lerp() 函数,它以特定百分比返回指定范围内的插值。 例如 0.2 表示 20%。

// displace the curve points
if (point.distanceTo(this.mousePoint) < displacement) 
  const direction = point.clone().sub(this.mousePoint)
  const displacementAmount = displacement - direction.length()
  direction.setLength(displacementAmount)
  direction.add(point)

  point.lerp(direction, 0.2) // magic number


// and move them back to their original position
if (point.distanceTo(targetPoint) > 0.01) 
  point.lerp(targetPoint, 0.27) // magic number

剩下的代码处理幻灯片样式的动画,有兴趣的可以去看看源码!

在另外两个演示中,我使用了一些不同的函数来塑造元素移动的路径,但总体而言,代码非常相似。

4、纹理投影结束语

我希望这篇文章简单易懂,足以让你深入了解纹理投影技术。 可以查看 GitHub 上的代码并下载它! 我确保以易于理解的方式编写代码并提供大量注释。


原文链接:Three.js纹理投影 — BimAnt

以上是关于Three.js教程:Face3对象定义Geometry的三角形面的主要内容,如果未能解决你的问题,请参考以下文章

Three.js纹理投影简明教程

Three.js 对象的“中心”是啥?

Three.js建模基础

无法将从 Blender 导出的对象加载到 three.js 中?

THREE.JS : 带有光线投射器和透视相机的点击事件

Three.js案例从0到1对象跟随鼠标动起来