一起学 WebGL:绘制图片

news/2024/5/5 14:35:57/文章来源:https://blog.csdn.net/fe_watermelon/article/details/130517457

大家好,我是前端西瓜哥。之前讲解了如何用 WebGL 绘制红色三角形,今天西瓜哥带大家来学习如何将图片绘制到画布上的技术:纹理映射(texture mapping)。

本文为系列文章中的一篇,请先阅读:

《一起学 WebGL:绘制三角形》

纹理映射会根据纹理图像,将光栅化后的每个片元(像素点)设置对应颜色值。这些像素也称为 纹素(texels, texture elements)。

纹理坐标

纹理图像的坐标系统是二维的,为和世界坐标的 x、y 区分,WebGL 对应使用 s、t 来表示。

目前纹理坐标更常用的命名是 uv。因为历史原因,WebGL 还是用的 st。

和世界坐标系类似,宽高使用的是一个比例值,即真实像素位置除以宽高后得到的比例。

着色器

const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {gl_Position = a_Position;v_TexCoord = a_TexCoord;
}
`;const fragmentShaderSrc = `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;

相比绘制单色三角形,我们在顶点着色器加了 a_TexCoord,记录顶点对应的纹理坐标,因为纹理坐标只有两个维度,所以用的 vec2 属性。

并命名一个 v_TexCoord 的 varying 变量,用于将 a_TexCoord 的值传递给片元着色器。

片元着色器,声明了一个接收同名 v_TexCoord 变量 接收传过来的纹理坐标。

u_Sampler 是 sampler2D 类型,是一个二维纹理采样器,指定着色器提取颜色的纹理对象。texture2D(u_Sampler, v_TexCoord) 表示从 u_Sampler 纹理采样器中的某个位置中取出颜色。

传入顶点数据

将顶点位置和纹理位置对应好,放在一个缓冲区中,并设置读取规则。

先读第一个点的位置,然后是第一个点对应的纹理坐标。然后第二个点…

// 顶点坐标,纹理坐标
const verticesTexCoords = new Float32Array([// 左上点。左边两个是顶点,右边两个是纹理-0.5, 0.5, 0.0, 1.0,// 左下-0.5, -0.5, 0.0, 0.0,// 右上0.5, 0.5, 1.0, 1.0,// 右下0.5, -0.5, 1.0, 0.0,
]);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;// 创建缓存对象
const verticesTexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, verticesTexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);// 传入纹理坐标位置信息
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);

纹理对象与图片绑定

/***** 纹理对象 *****/
const texture = gl.createTexture(); // 创建纹理对象
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler'); // 获取 u_Sampler 地址const img = new Image();
img.onload = () => {gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转纹理图像的 y 轴gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元gl.bindTexture(gl.TEXTURE_2D, texture); // 将我们的纹理对象绑定到 gl.TEXTURE_2D,类似绑定缓冲区对象// 配置纹理参数// 这里表示在 “绘制范围小于纹理尺寸” 时,使用 “加权平均” 算法缩小gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);// 将纹理图像分配给纹理对象gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);// 使用 0 号纹理单元gl.uniform1i(u_Sampler, 0);/****** 绘制 ******/// 清空画布,并指定颜色gl.clearColor(0, 0, 0, 1);gl.clear(gl.COLOR_BUFFER_BIT);// 绘制矩形,这里提供了 4 个点gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};img.src = './fe_watermelon.jpg';

创建纹理对象,然后在图片加载完之后配置到纹理对象上。

WebGL 下有多个纹理纹理单元(比如 gl.TEXTURE0、gl.TEXTURE5 之类),至少有 8 个。这些单元可以保存多个我们创建好的纹理图片,在需要的时候进行切换。

激活一个纹理单元:

gl.activeTexture(gl.TEXTURE0);

激活后,我们用过 gl.TEXTURE_2D 来访问这个纹理图像,进行纹理绑定和参数配置。

这里我们需要反转纹理图像的 y 轴线,因为图片和纹理坐标系不一样。我实在是蚌不住了。

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转纹理图像的 y 轴

gl.texImage2D 用于将将纹理图像分配给纹理对象。

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

gl.RGB 表示纹素的格式为 RGB,此外还有 gl.RGBA、gl.LUMINANCE(流明) 等。gl.UNSIGNED_BYTE 表示纹理的数据类型。

将纹理单元绑定到 u_Sampler 变量上。

gl.uniform1i(u_Sampler, 0);

最后就是调用 el.drawArrays 方法进行绘制。

图片的注意事项

关于图片,有几点需要注意。

首先是 图片不要跨域,因为安全限制,Canvas 是不能将跨域的图片绘制上去的,会报错。

然后是 图片的尺寸需要是 2 的幂次方,比如 16、32、64、128、256、512。

尺寸不对的图片需要留白补全到 2 的幂次方,然后在设置纹理坐标时指定对应真正宽高比例。

完整源码

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {gl_Position = a_Position;v_TexCoord = a_TexCoord;
}
`;const fragmentShaderSrc = `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;// 顶点坐标,纹理坐标
const verticesTexCoords = new Float32Array([// 左上点。左边两个是顶点,右边两个是纹理-0.5, 0.5, 0.0, 1.0,// 左下-0.5, -0.5, 0.0, 0.0,// 右上0.5, 0.5, 1.0, 1.0,// 右下0.5, -0.5, 1.0, 0.0,
]);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;// 创建缓存对象
const verticesTexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, verticesTexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);// 传入纹理坐标位置信息
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);/***** 纹理对象 *****/
const texture = gl.createTexture(); // 创建纹理对象
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler'); // 获取 u_Sampler 地址// 记载图片
const img = new Image();
img.onload = () => {gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转纹理图像的 y 轴gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元gl.bindTexture(gl.TEXTURE_2D, texture); // 将我们的材质对象绑定上去// 配置纹理参数gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);// 配置纹理图像gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);// 绑定 0 号纹理单元gl.uniform1i(u_Sampler, 0);/****** 绘制 ******/// 清空画布,并指定颜色gl.clearColor(0, 0, 0, 1);gl.clear(gl.COLOR_BUFFER_BIT);// 绘制矩形,这里提供了 4 个点gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};img.src = './fe_watermelon.jpg';

绘制结果:

填充方式

补充一下设置图片的几种方式。

gl.texParameteri(target, pname, param);

上面表示,在 pname 场景下,使用 param 策略。比如

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

表示在 “绘制范围小于纹理尺寸”(gl.TEXTURE_MIN_FILTER) 场景下,使用 “加权平均”(gl.LINEAR) 算法进行缩小。

参数 pname 有下面几个几个值可选择:

  • gl.TEXTURE_MAG_FILTER纹理放大。对应场景为纹理尺寸小于要绘制区域,需要将纹理放大。默认值为 gl.LINEAR
  • gl.TEXTURE_MIN_FILTER纹理缩小。默认值为 gl.NEAREST_MIPMAP_LINEAR
  • gl.TEXTURE_WRAP_S纹理水平填充。默认为 gl.REPEAT
  • gl.TEXTURE_WRAP_T纹理垂直填充。默认为 gl.REPEAT

参数 param 的一些可选值:

  • gl.LINEAR:使用 “加权平均” 缩放;
  • gl.NEAREST:使用 “曼哈顿距离” 缩放;
  • gl.REPEAT:重复平铺;
  • gl.MIRRORED_REPEAT:镜像重复平铺:
  • gl.CLAMP_TO_EDGE:使用纹理图像的边缘进行延伸填充;

看个势力,将绘制区域设置为图片的 2.5 倍,并设置填充方式。

// 顶点坐标,纹理坐标
const verticesTexCoords = new Float32Array([// 左上点。左边两个是顶点,右边两个是纹理-0.5, 0.5, 0.0, 2.5,// 左下-0.5, -0.5, 0.0, 0.0,// 右上0.5, 0.5, 2.5, 2.5,// 右下0.5, -0.5, 2.5, 0.0,
]);// ...
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 边缘像素平铺
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

得到了一个很有意思的结果:

结尾

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 系列 07 - 基于easyocr的ocr识别

OCR,光学文字识别,对文本资料进行扫描,然后对图像文件进行分析处理,获取文字及版面信息的过程。本示例通过easyocr库来演示。easyocr是一个比较流行的库,支持超过80种语言。安装的时候注意会附带安装torch库(一个深度学…

【Python】序列类型③-集合

文章目录 1.集合(set)简介2.集合的定义3.集合的遍历4.集合的常用方法 1.集合(set)简介 集合是一种无序可变的容器对象 集合最大的特点:同一个集合内元素是不允许有重复的,因此集合自带"去重"效果 2.集合的定义 集合的定义有两种方式: 使用{}进行定义,这种方式不能定…

获得 随机验证码(以图片为底层)

1:工具类 Slf4j public class RandomValidateCode {private static String baseNumLetter "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";private static String font "微软雅黑";//绘制验证码图片,返回验证码文本内容pu…

基于海鸥算法改进的随机森林分类算法-附代码

基于海鸥算法改进的随机森林分类算法 文章目录 基于海鸥算法改进的随机森林分类算法1.数据集2.RF模型3.基于海鸥算法优化的RF4.测试结果5.Matlab代码6.Python代码 摘要:为了提高随机森林数据的分类预测准确率,对随机森林中的树木个数和最小叶子点数参数利…

版本控制系统Git - 配置与基本使用

Git 1 Git简介1 Git概述2 Git的作用2.1 项目版本管理2.2 多人协同开发2.3 Git 的结构2.4 Git的工作原理 2 Git安装1 下载Git2 安装Git3 配置环境变量4 测试git是否安装成功5 安装git桌面工具(可以不安装) 3 Git基本操作1 设置Git用户2 新建仓库3 查看仓库状态4 添加到暂存文件5…

【python可视化】常用数据类型

🙋‍ 哈喽大家好,本次是python数据分析、挖掘与可视化专栏第二期 ⭐本期内容:常用数据类型 🏆系列专栏:Python数据分析、挖掘与可视化 👍欢迎大佬指正,一起学习,一起加油&#xff01…

C++入门3(C++新特性 using string auto)

C入门3 C新特性auto推导规则auto 作为函数的形参类型decltype基于范围for循环 typedef与usingC语言定义变量typedef 在C语言中的写法using在C11中的写法using与template的结合 string的简单使用 C新特性 auto推导规则 auto类型推导: auto定义的变量,可以根据初始化…

C语言单链表

本节目标: ①定义单链表结构体 ②初始化单链表 ③单链表增加结点(头插法、尾插法) ④删除指定结点 ⑤打印输出 目录 导入头文件 定义单链表结构体 初始化单链表 头插法 尾插法插入 删除指定结点 打印单链表 全部代码展示 导入头文件 …

中文大模型安全性哪家强?清华团队新发布

当前大型语言模型的火爆程度我们不用再进行赘述了,伴随着百度文心一言打响国内商业大模型第一枪,华为盘古,阿里通义千问,智谱ChatGLM,科大讯飞星火等国内公司纷纷开始布局。 另一方面由于众所周知的政策原因,和如火如荼…

5 分钟教你如何免费用上 GPT-4

今天要分享的就是普通用户,没有 OpenAI 账号,不需要写代码,你依然可以免费体验 GPT-4,当然,会有一些缺点,本篇文章将会手把手教你怎么用上免费版的 GPT-4 以及它的一些限制。 第一步:打开 Stea…

Threejs进阶之十三:CSS3DRenderer与Tween.js实现粒子小球按规律变化

今天我们使用CSS3DRendererTween.js实现Threejs官方示例中的粒子小球按规律变化的效果,先看下最终实现的效果 先来分析下,这个页面的动画效果是由512个小球组合起来的四种不同变化,分别是曲面、立方体、随机和圆球四种变化;下面我…

UDP的报文结构

UDP 报文结构 基本上所有的教科书上都是这样画的图, 但实际上 UDP 报文结构不是这样的, 这样显示应该是容易排版. 正确应该如下图 : 端口号 : 每个端口号在 UDP 报文里占两个字节, 取值范围就是: 0 ~ 65535 源 ip 和源端口描述了数据从哪里来, 目的 ip 和目的端口描述了数据去哪…

文本的清洗和标准化:如何处理混乱的数据?

❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…

操作系统考试复习—第三章 优先级倒置 死锁问题

当前OS广泛采用优先级调度算法和抢占方式,然而在系统中存在着影响进程运行的资源从而可能产生"优先级倒置"现象 具体解释为:在原本的调度算法设计中,高优先级进程可以抢占低优先级的CPU资源,先执行高优先级任务。但是存…

x265码控分析

D和R的关系 高分辨率量化 均匀量化:量化区间 ‘ Δ k y k − y k − 1 ‘ \Delta_ky_k-y_{k-1} ‘Δk​yk​−yk−1​‘,近似为常数;p(x)为信源概率密度函数,且 ‘ Δ k ‘ \Delta_k ‘Δk​‘的大小相对于p(x)的变化率充分小&…

电力NLP:指令票规范识别

文章目录 任务目的想法讲解数据集介绍1电气主语2操作任务判断数据集3操作内容判断数据集4错误词数据集 解法讲解程序、数据集下载链接 任务目的 识别调度指令票(或者其它操作票)是否规范。 想法讲解 按石第2014—16号定值单投入石双西线161开关6区保护…

突发!ChatGPT王炸级更新!支持GPT-4联网 Code Interpreter!

4月30日,OpenAI官方悄悄发布了联网版GPT-3.5。虽然名字变了,但使用体验却是换汤不换药,还是那套。 然而,万万没想到的是,刚过去没几天,昨天5月4日,鱼哥发现自己的Plus账号竟然多了一些能力&…

树莓派硬件介绍及配件选择

目录 树莓派Datasheet下载地址: Raspberry 4B 外观图: 技术规格书: 性能介绍: 树莓派配件选用 电源的选用: 树莓派外壳选用: 内存卡/U盘选用 树莓派Datasheet下载地址: Raspberry Pi …

C++11多线程:std::thread创建线程和std::async创建异步任务的区别,std::async创建异步任务后没有被推迟执行。

系列文章目录 文章目录 系列文章目录前言一、thread和async的区别1.1 新线程和异步任务1.2 std::async和std::thread最明显的不同,就是async有时候并不创建新线程。1.3 std::async和std::thread的区别1.4 std::async不确定性问题的解决 二、使用方法2.1 std::async创…

JVM学习随笔02——虚拟机内存区组成与内存溢出异常

一、Java虚拟机内存区组成图 1、程序计数器: 每个线程独占一个计数器,用来指示该线程下一条要执行的指令的地址。这一部分不会导致内存异常。PS:如果一个线程进入的是一般的Java方法,计数器指示的是下一条指令地址;如果…