一起学 WebGL:绘制图片

news2024/9/22 15:46:35

大家好,我是前端西瓜哥。之前讲解了如何用 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.coloradmin.cn/o/494321.html

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

相关文章

25000 字详解 23 种设计模式(多图 + 代码)

25000 字详解 23 种设计模式(多图 代码) 目录 创建型模式结构型模式行为型模式总结 前言 一直想写一篇介绍设计模式的文章,让读者可以很快看完,而且一看就懂,看懂就会用,同时不会将各个模式搞混。 设计…

记录一次docker容器引起的时间相差8h的问题

一、背景 系统打印日志时间小8h,部分插入mysql的日期却大8h,简直诡异。 测试时间是上午10:05 经过排查,mysql设置的时区,链接url设置的时区都是ok的。而且有其他服务时间正常,故排除MySQL的问题。 二、排查 2.1 查…

AIGPT中文版(无需魔法,直接使用)不愧是生活工作的好帮手。

AIGPT AIGPT是一款非常强大的人工智能技术的语言处理工具软件,它具有 AI绘画 功能、AI写作、写论文、写代码、哲学探讨、创作等功能,可以说是生活和工作中的好帮手。 我们都知道使用ChatGPT是需要账号以及使用魔法的,其中的每一项对我们初学…

09-Vue技术栈(TodoList案例)

目录 1、前期准备2、组件化编码流程3、拆分静态组件3.1 app组件3.2 TodoList组件3.2.1 TodoItem组件 3.3 TodoFooter组件 4、实现动态组件5、实现交互5.1 渲染页面5.2 添加功能5.3 勾选or取消勾选一个todo5.4 删除一个todo5.5 渲染TodoFooter底部内容5.6 全选or取消全选5.7清除…

软考算法-排序篇-下

排序篇 一:故事背景二:冒泡排序2.1 概念2.2 画图表示2.3 代码实现2.4 总结提升 三:快速排序3.1 概念3.2 画图表示3.3 代码实现3.4 总结提升 四:归并排序4.1 概念4.2 画图表示4.3 代码实现4.4 总结提升 五:基数排序5.1 …

第二章——进程与线程(上)

上船不思岸上人,下船不提船上事 文章目录 2.1.1 进程的概念,组成,特征知识总览进程的概念进程的组成——PCB程序是如何运行的进程的组成进程的特征知识回顾 2.1.2 进程的状态与转换,进程的组织知识总览创建态,就绪态运…

《花雕学AI》用Edge和chrome浏览器体验GPT-4智能聊天的神奇免费插件,Sider – 聊天机器人的新选择

你有没有想过和人工智能聊天?你有没有想过用浏览器就能和GPT-4这样的先进的聊天机器人对话?如果你有这样的想法,那么你一定要试试Sider这个神奇的免费插件。 Sider(Sider – AI Sidebar)是一款基于ChatGPT的智能侧边栏…

零基础小白学5G网络优化技术,最常陷入的怪圈有哪些?

“赛道”这个词是自媒体最喜欢谈的,因为生活里面处处是赛道。从上小学选择哪个中学,高考选择哪个专业,大学毕业选择哪个行业...... 一开始就选对赛道的人,少之又少,都是需要经历和试错才可以。面对行业和工作这个赛道&…

豪取BAT!超详细暑期实习算法面经(非科班无论文)

面试锦囊之面经分享系列,持续更新中 赶紧后台回复"面试"加入讨论组交流吧 写在前面 本人基本情况:211本硕,本科电子信息工程,硕士通信与信息系统,典型的非科班,无论文,两段实习经历…

UNIX系统调用和库函数(详细讲解)

什么是系统调用? 所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的 UNIX 实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call,见图1-1) Research UNX 系统第7版提供了约5…

复古视觉大闸蟹创意海报设计

一、新建画布1500*2300像素,分辨率72 二、把文案要求拖入新建的画布中,更改文字颜色,然后打组命名为文案 三、拖入一个大闸蟹到画面当中,点击视图,新建一个居中的参考线,750居中 四、给画面添加一个背景&am…

关于WPA3-H2E的技术讲解

序言 H2E是Hash-To-Element的缩写。 问:虽然使用WPA3 SAE解决了offline dictionary破解密钥的问题,但是原先用于生成PMK的算法在计算时间上和密钥有关联性(这也行?),仍然存在所谓被side-channel方式破解。 解:新的算法,使用hash计算一次即可,堵死这个理论缺口[4]。…

盖雅工场发布数字化转型人效实践案例集

近日,盖雅工场重磅发布《聚集人效,重塑组织:典范企业管理实践案例集》(以下简称案例集)。 过去一年,盖雅工场携旗下盖雅学苑访谈了来自制造业、服务业、连锁零售业、汽车产业的几十家企业后,并…

【喜报】通付盾获评苏州市知识产权优势型企业!

近日,苏州市第二批知识产权强企培育工程成长型、优势型、引领型企业名单公示,江苏通付盾科技有限公司获评“苏州市知识产权强企培育工程-优势型企业”。 *名单发布来源:苏州市工业和信息化局 苏州市知识产权优势型企业 获评优势型企业的主要…

【观察】更懂业务的数智平台,才能应对数智化转型的“千变万化”

毫无疑问,随着数智化转型的加速,越来越多的企业正在把数智化战略提升到一个全新的高度,转型的进程也正从“浅层次”的数智化走向“深层次”数智化的阶段。 这也让企业的数智化转型进入到了一个全新的阶段,其面临的挑战也越来越大&…

intel I2C的速率配置

目录 寄存器篇 修改寄存器 intel I2C 驱动结构 lpss-pci文件 lpss文件 驱动结构 Synopsys DesignWare I2C BIOS配置修改 ACPI表的查看 I2C速率 寄存器篇 修改速率很简单,看到手册里面的寄存器说明,然后将其改掉即可。 寄存器偏移量为0&#x…

1、Flutter使用总结(RichText、Container)

1、创建Flutter项目 flutter create DemoName 2、运行项目 flutter run -d ‘iPhone 14 Pro Max’ 注: 当运用Android Studio时、选择安卓模拟器运行项目、如果项目路径有中文名称: 那么运行报错、如果直接在项目路径下,采用终端运行安卓模拟器、可执行如下命令 flutter ru…

博客系统后端设计(二) - 封装数据库操作

文章目录 封装数据库操作1. 创建一个 db.sql 文件2. 封装数据库的连接操作3. 创建实体类4. 封装数据库的增删改查操作4.1 创建 BlogDao 类中的方法4.2 创建 UserDao 类中的方法 封装数据库操作 这个步骤主要是把一些基本的数据库操作封装好,以后备用。 1. 创建一个…

微信小程序——wxs脚本

WXS目录 一、WXS的概述1、什么是wxs2、应用场景:3. wxs 与JavaScript(1)wxs 支持的数据类型:(2)wxs 不支持类似于 ES6 及以上的语法形式(3)wxs 遵循 CommonJS 规范 二 、WXS基础语法1、 内嵌 wxs 脚本2、外联的 wxs 脚本 三、WXS的特点1. 与 JavaScript 不同2. 不能…

【计算机图形学基础教程】MFC基本绘图函数2

MFC基本绘图函数 绘图工具类 CGdiObject类:GDI绘图工具的基类CBitmap类:封装了GDI画刷,可以选作设备上下文的当前画刷,用于填充图形的内部CFont类:封装了GDI字体,可以选作设备上下文的当前字体CPalette类…