Three.js中的3D文字效果

news2024/9/24 3:27:35

对于一些设计网页中经常会出现一些3D的文字效果,本文将利用Three.js实现各种动画WebGL文本输入效果。

示例效果
原文章

文本采样

通常情况下,文本网格是2D的平面形状,我们所要实现的3D文本形状则是要在2D的平面下,再生成z值形成一个立体的效果。

首先,我们创建一个canvas元素,对其应用一些与字体相关的样式,并确保其大小canvas足以容纳文本。

// 字体样式设置
const fontName = 'Verdana';
const textureFontSize = 100;

// 显示内容
let string = 'Some text' + '\n' + 'to sample' + '\n' + 'with Canvas';

// 创建canvas的2D上下文
const textCanvas = document.createElement('canvas');
const textCtx = textCanvas.getContext('2d');
document.body.appendChild(textCanvas);

设置样式后,我们在canvas上绘制文本。

function sampleCoordinates() {

    // 解析文本
    const lines = string.split(`\n`);
    const linesMaxLength = [...lines].sort((a, b) => b.length - a.length)[0].length;
    const wTexture = textureFontSize * .7 * linesMaxLength;
    const hTexture = lines.length * textureFontSize;

    // 绘制文本
    const linesNumber = lines.length;
    textCanvas.width = wTexture;
    textCanvas.height = hTexture;
    textCtx.font = '100 ' + textureFontSize + 'px ' + fontName;
    textCtx.fillStyle = '#2a9d8f';
    textCtx.clearRect(0, 0, textCanvas.width, textCanvas.height);
    for (let i = 0; i < linesNumber; i++) {
        textCtx.fillText(lines[i], 0, (i + .8) * hTexture / linesNumber);
    }

    // ...
}

接下来我们可以从中获取ImageData,该ImageData对象包含一个一维数组,其中包含每个像素的RGBA数据。通过canvas的大小,我们可以遍历该数组并检查已有像素颜色位置(即文本位置)。

    // 采样坐标
    textureCoordinates = [];
    const samplingStep = 4;
    if (wTexture > 0) {
        const imageData = textCtx.getImageData(0, 0, textCanvas.width, textCanvas.height);
        for (let i = 0; i < textCanvas.height; i += samplingStep) {
            for (let j = 0; j < textCanvas.width; j += samplingStep) {
                // 因为背景的RGBA是(0,0,0,0),所以可以通过判断r通道颜色来区分是否是文字
                if (imageData.data[(j + i * textCanvas.width) * 4] > 0) {
                    textureCoordinates.push({
                        x: j,
                        y: i
                    })
                }
            }
        }
    }

在采样步骤其实可以做一些比如添加随机性、对文本运用轮廓描边等工作。把点采样出来之后,可以在2d canvas上再绘制一遍检验效果。

在Three.js的场景中绘制

设置一个基本的Three.js场景,创建一个Plane对象,我们可以使用上一步中的文本采样画布作为Plane的材质。

生成粒子

我们可以使用相同的采样函数来对文字生成3D坐标。X、Y坐标是从画布中采集的,对于Z坐标,我们可以取一个随机数。我们可以用THREE.Points来展示这些例子

function createParticles() {
    const geometry = new THREE.BufferGeometry();
    const material = new THREE.PointsMaterial({
        color: 0xff0000,
        size: 2
    });
    const vertices = [];
    for (let i = 0; i < textureCoordinates.length; i ++) {
        vertices.push(textureCoordinates[i].x, textureCoordinates[i].y, 5 * Math.random());
    }
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    const particles = new THREE.Points(geometry, material);
    scene.add(particles);
}

因为纹理坐标和三维场景坐标y轴的不同,所以生成的粒子会发现上下颠倒。所以我们需要翻转每个粒子的Y值,为此我们需要计算文本的边界框。作为一种临时解决方案,我们可以将最大X、Y值作为文本的最大宽高值。

function refreshText() {
    sampleCoordinates();
    
    const maxX = textureCoordinates.map(v => v.x).sort((a, b) => (b - a))[0];
    const maxY = textureCoordinates.map(v => v.y).sort((a, b) => (b - a))[0];
    stringBox.wScene = maxX;
    stringBox.hScene = maxY;

    createParticles();
}

对于每个点,Y坐标变为boxTotalHeight-Y。将整个粒子系统移动盒子的一半宽度和一半高度可以解决居中问题。

function createParticles() {
    
    // ...
    for (let i = 0; i < textureCoordinates.length; i ++) {
       // 将Y进行翻转
       vertices.push(textureCoordinates[i].x, stringBox.hScene - textureCoordinates[i].y, 5 * Math.random());
    }
    // ...
    
    // 将文字居中
    particles.position.x = -.5 * stringBox.wScene;
    particles.position.y = -.5 * stringBox.hScene;
}

到目前为止,我们是借助文本canvas直接从3D场景中收集的像素坐标。但是假设我们需要3D文本的高度等于10个单位。如果我们将字体大小设置为10,则canvas分辨率将太低而无法进行适当的采样。为了避免这种情况(并且更灵活地使用粒子密度),我们可以添加一个额外的缩放因子(在3D空间中使用它们之前,我们将与画布坐标相乘的值)。

// ...
const textureFontSize = 30;
const fontScaleFactor = .3;
// ...
function refreshText() {
    // ...
    textureCoordinates = textureCoordinates.map(c => {
        return { x: c.x * fontScaleFactor, y: c.y * fontScaleFactor }
    });
    // ...
}

切换到InstancedMesh

THREE.Points的主要限制是粒子大小。THREE.PointsMaterial是基于WebGL的gl_PointSize,它可以以大约50 到 100 的最大像素大小进行渲染,具体取决于显卡。如果我们想要用3D形状来作为粒子形状的话,只能用THREE.InstancedMesh

首先根据粒子数量创建InstancedMesh,然后添加要用于每个实例的几何体和材质,再创建一个虚拟对象,帮助我们为每个粒子生成一个 4×4 变换矩阵。使用setMatrixAt方法将变换矩阵应用于每个实例

function updateParticlesMatrices() {
    let idx = 0;
    textureCoordinates.forEach(p => {

        // we apply samples coordinates like before + some random rotation
        dummy.rotation.set(2 * Math.random(), 2 * Math.random(), 2 * Math.random());
        dummy.position.set(p.x, stringBox.hScene - p.y, Math.random());

        dummy.updateMatrix();
        instancedMesh.setMatrixAt(idx, dummy.matrix);

        idx ++;
    })
    instancedMesh.instanceMatrix.needsUpdate = true;
}

基本动画

我们可以为文字做一些基本的动画。我们可以添加一个额外的Particle对象数组来存储每个实例的参数,仍然需要textureCoordinates数组来存储以像素为单位的2D坐标,但现在我们将它们重新映射到particles数组。然后粒子变换更新放到主render循环中。

每个Particle对象都包含一个属性列表和一个grow()更新其中一些属性的函数。

首先,我们定义位置、旋转和缩放。每个粒子的位置都是静态的,创建粒子时比例会从零增加到一,并且旋转将始终设置动画。

function Particle([x, y]) {
    this.x = x;
    this.y = y;
    this.z = 0;
    this.rotationX = Math.random() * 2 * Math.PI;
    this.rotationY = Math.random() * 2 * Math.PI;
    this.rotationZ = Math.random() * 2 * Math.PI;
    this.scale = 0;
    this.deltaRotation = .2 * (Math.random() - .5);
    this.deltaScale = .01 + .2 * Math.random();
    this.grow = function () {
        this.rotationX += this.deltaRotation;
        this.rotationY += this.deltaRotation;
        this.rotationZ += this.deltaRotation;
        if (this.scale < 1) {
            this.scale += this.deltaScale;
        }
    }
}
// ...
function updateParticlesMatrices() {
    let idx = 0;
    // textureCoordinates.forEach(p => {
    particles.forEach(p => {
        // update the particles data
        p.grow();
        // dummy.rotation.set(2 * Math.random(), 2 * Math.random(), 2 * Math.random());
        dummy.rotation.set(p.rotationX, p.rotationY, p.rotationZ);
        dummy.scale.set(p.scale, p.scale, p.scale);
        dummy.position.set(p.x, stringBox.hScene - p.y, p.z);
        dummy.updateMatrix();
        instancedMesh.setMatrixAt(idx, dummy.matrix);
        idx ++;
    })
    instancedMesh.instanceMatrix.needsUpdate = true;
}

其他扩展

我们也可以利用上述流程做一些气泡、云朵、花草的文字效果。

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

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

相关文章

Oracle数据库启停命令

在日常工作中&#xff0c;关于数据库的启停&#xff1a;   先关闭上层应用服务 --> 关闭监听 --> 关闭数据库&#xff1b;   启动数据库 --> 启动监听 --> 启动应用(Oracle SQL Developer)。 监听lsnrctl Oracle监听命令。 lsnrctl start [listener-name]&a…

Xml格式化与高亮显示

具体请参考&#xff1a;Xml格式化与高亮显示

分布式系统的数据一致性方案

1、在出现一致性问题时如果系统的并发或不一致情况较少&#xff0c;可以先使用重试来解决 a、同步重试 b、异步重试 c、入库&#xff0c;定时任务重试 2、分布式事务 基于数据库 XA 协议的 2PC、3PC&#xff0c;基于业务层的TCC,基于消息队列消息表的最终一致性方案&#xff0…

七大设计原则之接口隔离原则应用

目录1 接口隔离原则介绍2 接口隔离原则应用1 接口隔离原则介绍 接口隔离原则&#xff08;Interface Segregation Principle, ISP&#xff09;是指用多个专门的接口&#xff0c;而不使用单一的总接口&#xff0c;客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口时…

八、Git远程仓库操作——跨团队成员的协作

前言 前面一篇博文介绍了git团队成员之间的协作&#xff0c;现在在介绍下如果是跨团队成员的话&#xff0c;如何协作&#xff1f; 跨团队成员协作&#xff0c;其实就是你不属于那个项目的成员&#xff0c;你没有权限向那个仓库提交代码。但是github还有另一种 pull request&a…

图形化深度学习开发平台PaddleStudio(代码开源)

目录一、PaddleStudio概述二、环境准备2.1 安装PaddlePaddle2.2 安装依赖库三、基本使用介绍3.1 启动3.2 快速体验3.2.1 下载示例项目3.2.2 训练3.2.3 评估3.2.4 测试3.2.5 静态图导出四、数据集格式4.1 图像分类4.2 目标检测4.3 语义分割4.4 实例分割五、趣味项目实战&#xf…

【前端笔试题二】从一个指定数组中,每次随机取一个数,且不能与上次取数相同,即避免相邻取数重复

前言 本篇文章记录下我在笔试过程中遇到的真实题目&#xff0c;供大家参考。 1、题目 系统给定一个数组&#xff0c;需要我们编写一个函数&#xff0c;该函数每次调用&#xff0c;随机从该数组中获取一个数&#xff0c;且不能与上一次的取数相同。 2、思路解析 数组已经有了…

Java编译过程、JIT编译详解、类加载过程

文章目录Java编译执行过程类加载过程即时编译JITJIT编译优化中的常见技术方法内联逃逸分析 栈上分配 锁消除小总结Java编译执行过程 提到编译,可能大多数人想到的就是将**.java编译成***.class文件,但其实Java代码的编译执行是一个非常复杂的过程,将**.java编译成**.class…

Lesson1:初识编程语言、Python环境搭建

一、什么是编程语言 用来和计算机交流&#xff0c;控制计算机&#xff0c;让计算机按照我们的要求做事情&#xff0c;这样的语言叫做编程语言。 Note&#xff1a;编程语言四个字可以拆成两个部分进行理解——编程语言。 所谓语言&#xff0c;它的作用就是交流&#xff0c;向对…

Redis实战—黑马点评(一) 登录篇

Redis实战 — 黑马点评&#xff08;一&#xff09; 登录篇 来自黑马的redis课程的笔记 【黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目】 目录Redis实战 — 黑马点评&#xff08;一&#xff09; 登录篇1. 项目…

深度学习笔记:使用随机梯度下降法识别mnist数据集

深度学习算法实现流程&#xff1a; 1 从训练数据中随机选出一部分数据&#xff0c;称为mini-batch。我们的目标为减小mini-batch损失函数的值 2 计算损失函数关于权重的梯度。梯度方向即为损失函数值减小最快的方向 3 将权重沿梯度下降方向更新 4 重复以上步骤&#xff0c;在…

【自动驾驶汽车技术 | 车载雷达系统】

本文编辑&#xff1a;调皮哥的小助理 1、摘要 自动驾驶汽车传感器系统一般包括4种雷达&#xff1a;激光雷达(Lidar)、毫米波雷达(mmWave Radar)、超声波雷达(Ultrasonic Radar)和红外雷达(Infrared Radar)。目前激光雷达和毫米波雷达是基本和必要的车载传感器设备&#xff0c;…

I.MX6ULL内核开发8:linux设备驱动模型

目录 一、为什么需要设备驱动模型 二、sysfs概述 驱动模型一 驱动模型二 kobject kset kobj_type 一、为什么需要设备驱动模型 早期内核&#xff08;2.4之前&#xff09;没有统一的设备驱动模型&#xff0c;但是照样可以使用&#xff08;之前的led字符设备驱动&#xff…

2023-2-12刷题情况

字母板上的路径 题目描述 我们从一块字母板上的位置 (0, 0) 出发&#xff0c;该坐标对应的字符为 board[0][0]。 在本题里&#xff0c;字母板为board [“abcde”, “fghij”, “klmno”, “pqrst”, “uvwxy”, “z”]&#xff0c;如下所示。 我们可以按下面的指令规则行动…

合宙Air103|fbd数据库| fskv - 替代fdb库|LuatOS-SOC接口|官方demo|学习(16):类redis的fbd数据库及fskv库

基础资料 基于Air103开发板&#xff1a;&#x1f697; Air103 - LuatOS 文档 上手&#xff1a;开发上手 - LuatOS 文档 探讨重点 对官方社区库接口类redis的fbd数据库及fskv库的调用及示例进行复现及分析&#xff0c;了解两库的基本原理及操作方法。 软件及工具版本 Luat…

肝了几天的Git入门教程,收获满满

1.简介 谈及版本控制系统&#xff0c;或许大多数程序员最开始接触的都是SVN&#xff08;Subversion&#xff09;&#xff0c;它是一个集中式的版本控制系统&#xff0c;使用的时候需要提供一台的服务器来进行部署&#xff0c;所有的更新与同步操作都需要与这台服务器进行交互&…

windows/linux下Qt可执行程序打包,linux桌面双击运行程序sh脚本

1、windows下Qt打包 windows下Qt的可执行文件打包简单的来说就是利用Qt自带依赖的打包工具windeployqt进行打包&#xff0c;该工具存在Qt安装目录下&#xff0c;执行命令为&#xff1a;windeployqt name.exe 打包依赖文件可参考如下链接中1-7步&#xff0c;后面的步骤是打包依…

156、【动态规划】AcWing ——3. 完全背包问题:二维数组+一维滚动数组(C++版本)

题目描述 原题链接&#xff1a;3. 完全背包问题 解题思路 完全背包相对于01背包来说&#xff0c;对同一个物品可以选择多次。而01背包对同一个物品只能选择一次。 递推公式上的区别&#xff1a;01背包是dp[i][j] max(dp[i - 1][j], dp[i - 1][j - v[i]] w[i])&#xff0c;…

14. 最长公共前缀

14. 最长公共前缀 一、题目描述&#xff1a; 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 “”。 示例 1&#xff1a; 输入&#xff1a;strs [“flower”,“flow”,“flight”] 输出&#xff1a;“fl” 示例 2&#xff1a; …

在线支付系列【23】支付宝支付接入指南

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言接入指南1. 创建应用2. 绑定应用3. 配置密钥4. 上线应用5. 开通产品沙箱环境开发前准备&#xff08;沙箱环境&#xff09;1. 获取参数、秘钥、证书2. 下载支付宝客户端3. 案例演示前言 在之…