Three.js纹理投影简明教程

news2024/10/5 19:14:34

纹理投影是一种将纹理映射到 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux 入门教程||Linux 简介||Linux 安装

Linux 简介 Linux内核最初只是由芬兰人李纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;在赫尔辛基大学上学时出于个人爱好而编写的。 Linux是一套免费使用和自由传播的类Unix操作系统&#xff0c;是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。…

pdf文件怎么压缩?pdf文件变小的简单方法

工作中&#xff0c;pdf文件的使用是非常广泛的&#xff0c;一些特殊的场景下对于pdf文件的大小是有着严格规定的&#xff0c;所以pdf文件压缩成了必备的一项技能&#xff0c;那么怎么将pdf压缩&#xff08;https://www.yasuotu.com/pdfyasuo&#xff09;呢&#xff1f;下面介绍…

一个完整的渗透学习路线是怎样的?如何成为安全渗透工程师?

前言 1/我是如何学习黑客和渗透&#xff1f; 我是如何学习黑客和渗透测试的&#xff0c;在这里&#xff0c;我就把我的学习路线写一下&#xff0c;让新手和小白们不再迷茫&#xff0c;少走弯路&#xff0c;拒绝时间上的浪费&#xff01; 2/学习常见渗透工具的使用 注意&…

2023年江苏建筑安全员精选真题题库及答案

百分百题库提供建筑安全员考试试题、安全员证考试真题、安全员证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 250.施工升降机防坠安全器在装机使用时,应按吊笼额定载重量进行坠落试验,以后至少()个月应进行一次额定载重量的坠落试验 …

辨析Web Service, SOAP, REST, OData之间的关系与区别

最近发现&#xff0c;对于刚刚接触HTTP服务的同学&#xff0c;在一些基础概念上容易混乱。很多同学搞不清楚Web Service&#xff0c;SOAP&#xff0c;REST以及OData这些技术之间的关系与区别。文本会尽量用最简洁的方式&#xff0c;解释这几个概念&#xff0c;并附上一些资料的…

第一章:在Mac OS上安装Go语言开发包

各位朋友们大家好&#xff01; 本节主要为大家讲解如何在Mac OS上安装Go语言开发包&#xff0c;大家可以在Go语言官网下载对应版本的的安装包&#xff0c;如下图所示。 安装Go语言开发包 Mac OS 的Go语言开发包是 .pkg 格式的&#xff0c;双击我们下载的安装包即可开始安装。…

I.MX6ULL内核开发1:内核模块实验

目录 一、实验环境 二、编译4.19.35版内核 1、下载linux内核源码 2、安装必要的环境工具库 3、一键编译内核 4、获取编译出来的内核相关文件&#xff08;与makefile文件一致&#xff09; 三、内核模块代码分析 1、内核模块头文件 2、内核模块打印函数 3、文中语法分析…

filter滤镜实现网页置灰(纪念日)效果

目录 前言关键代码兼容ie的做法定位错乱的原因 前言 一些特殊纪念日的时候&#xff0c;很多网站的首页进行置灰处理。这种效果实际上是用滤镜filter实现的&#xff0c;几行css就可以实现。 在实现整个页面置灰的过程中&#xff0c;要注意页面中有定位的元素&#xff0c;就需…

java中 == 和 equels

1、 和 equals的区别 是操作符 操作符专门用来比较变量的值是否相同。对于基本类型变量来说&#xff0c;只能使用 &#xff0c;因为基本类型的变量没有方法。使用比较是值比较。对于引用类型的变量来说&#xff0c;比较的两个引用对象的地址是否相等。 equals 是方法 equals方…

Linux kdump配置步骤和注意事项(基于debian、OpenEuler和自定义编译内核的Linux)

1、kdump简单描述 kdump是Linux中的一个内核转储机制&#xff0c;主要用于当Linux内核发生崩溃时&#xff0c;将该内核相关的信息和崩溃原因通过转储的形式保留下来&#xff0c;在debian系统中&#xff0c;相关信息会存储在dump文件中&#xff0c;在OpenEuler和CentOS等系统中…

utils:crypto-js的基本使用和(加密/解密)功能封装

目录一、基本使用1. 资源下载2. 目录结构3. 代码二、功能封装1. 封装代码2. 使用说明(1) 引入utils工具(2) 使用工具一、基本使用 1. 资源下载 crypto-js 2. 目录结构 3. 代码 <html xmlns"http://www.w3.org/1999/xhtml"> <head> <meta http-equ…

【Linux】磁盘结构/文件系统/软硬链接/动静态库

文章目录前言一、磁盘结构1、磁盘的物理结构2、磁盘的存储结构3、磁盘的逻辑结构二、文件系统1、对 IO 单位的优化2、磁盘分区与分组3、对分组的具体管理方法4、文件操作三、软硬链接1、理解硬链接2、理解软链接3、理解 . 和 ..四、静动态库1、什么是动静态库2、动静态库的制作…

JavaScript 中的字符串

JavaScript 字符串 什么是字符串&#xff1f; 字符串是用来存储和处理文本数据的 比如&#xff1a;一句话、a等 字符串的创建方式 字面量方式进行创建 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta…

DW 2023年1月Free Excel 第十次打卡 Excel看板

第十章Excel看板 数据下载地址与参考链接&#xff1a;https://d9ty988ekq.feishu.cn/docx/Wdqld1mVroyTJmxicTTcrfXYnDd 数据看板作为数据动态展示的一种重要方式&#xff0c;被广泛的应用于各个领域&#xff0c;因此本节根据1个案例讲解使用Excel制作数据看板的过程&#xff…

大厂年薪43w测试开发手把手教你搭建Web自动化测试框架,超详细

测试框架的设计有两种思路&#xff0c;一种是自底向上&#xff0c;从脚本逐步演变完善成框架&#xff0c;这种适合新手了解框架的演变过程。另一种则是自顶向下&#xff0c;直接设计框架结构和选取各种问题的解决方案&#xff0c;这种适合有较多框架事件经验的人。本章和下一张…

使用Python读取和处理安卓传感器数据与CSV读取

多年来&#xff0c;数据一直是世界运作的重要组成部分。这些数据可以从GDP到血样&#xff0c;再到世界的各个方面。随着我们数据的增长&#xff0c;统计学找到了从它们中提取更多意义的方法。 这些方法之一被称为方差分析&#xff08;ANOVA&#xff09;。方差分析是一套统计模…

python进阶——人工智能视觉识别

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

最简单的k8s安装记录(包含dashboard)

使用sealos一键安装k8s集群 sealos-githubsealos使用文档 wget https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_amd64.tar.gz && \tar -zxvf sealos_4.1.4_linux_amd64.tar.gz sealos && chmod x sealos && mv se…

编译原理学习笔记15——属性文法与语法制导翻译2

编译原理学习笔记15——属性文法与语法制导翻译215.1 S-属性文法15.2 L-属性文法15.3 翻译模式15.4 递归下降翻译器的设计15.1 S-属性文法 S-属性文法的自下而上计算 S-属性文法的自下而上计算 在分析栈中增加附加域存放综合属性值假设产生式A→XYZ对应的语义规则为a:f(X.x…

OAK深度相机操作温度范围说明

编辑&#xff1a;OAK中国 首发&#xff1a;Luxonis 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手君。…