一篇搞懂OpenGL中的渲染器(Renderer)、纹理(Texture)和混合(Blending)

目录

一、什么是渲染器

二、抽象渲染器类

三、什么是OpenGL的纹理

四、怎么利用OpenGL接口操作纹理

五、Blending混合


话不多说,我把我看的视频链接贴出来,下面的笔记是由视频学习和自己的补充而来。这次是(16-18)的笔记

跟着这个小哥的教学视频学的(YouTube原视频,科学上网AI字幕) ►               http://bit.ly/2lt7ccM
这个是哔哩哔哩网站有人搬运的 ►https://www.bilibili.com/video/BV1MJ411u7Bc/?share_source=copy_web&vd_source=80ce9fa9cc5a33fdc2b9a467859dd047

一、什么是渲染器

在OpenGL中,术语“渲染器”通常是指执行图形渲染的软件或硬件组件,其任务是将三维场景的描述转换为最终的二维图像。渲染器是图形渲染管线的一部分,负责处理各种图形操作,包括几何变换、光照、着色、裁剪等,最终生成屏幕上的像素。

渲染器的主要任务包括:

  1. 几何变换: 将三维对象的顶点坐标从世界坐标系变换到相机坐标系,然后投影到裁剪空间。

  2. 裁剪: 在裁剪空间中,对超出视锥体的几何图元进行裁剪。

  3. 透视除法: 将裁剪空间中的坐标进行透视除法,将坐标映射到标准化设备坐标系(Normalized Device Coordinates,NDC)。

  4. 视口变换: 将标准化设备坐标映射到屏幕坐标,根据窗口大小和位置进行视口变换。

  5. 光照和着色: 计算光照效果,为每个像素分配颜色。

  6. 深度测试和遮挡检测: 根据深度信息判断哪些像素应该被渲染,哪些应该被遮挡。

  7. 帧缓冲写入: 将最终的颜色写入帧缓冲,形成最终的图像。

在OpenGL中,渲染器的实现通常取决于图形硬件或软件框架。OpenGL提供了一些API和函数,用于配置和控制渲染器的行为,例如着色器程序的编写和使用,以及状态的设置和管理。在现代OpenGL中,通常使用着色器语言(如GLSL)编写顶点着色器和片段着色器,以自定义渲染过程。

二、抽象渲染器类

抽象类Renderer渲染器,其主要就是统筹我们一整套渲染流程
现阶段我们就暂时把vao绑定,ib绑定,draw调用整合,并且clear函数封装,现在就差uniform变量需要我们在外面设置了,(这里其实有一个概念就是材质材质就是把着色器和一个着色器所有的uniform变量做一个规划,让在加载着色器的时候,把所有的uniform变量也能初始化赋值一遍,这样就相当于给物体上了一个材质,这个功能相当于动态的设置uniform,有兴趣的小伙伴可以试一下。)

#pragma once
#include <GL/glew.h>
#include <iostream>

#include "VertexArray.h"
#include "IndexBuffer.h"
#include "Shader.h"

class Renderer
{
public:
	void Clear() const;
	void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const;
};





#include "Renderer.h"

void Renderer::Clear() const
{
	GLCall(glClear(GL_COLOR_BUFFER_BIT));
}

void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) const
{
	//绑定着色器程序
	shader.Bind();
	//当引入了VAO之后,就不需要再绑定Buffer和指定属性了,直接绑定VAO就行了
	GLCall(va.Bind());
	//
	GLCall(ib.Bind());
	GLCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr));
}

当我们封装好这么个渲染器类的时候,我们就只需要在OpenGL的上下文创建这么一个渲染器对象,绑定其中的所有渲染的准备,加上调用draw函数就可以渲染了。其他的事情,像什么纹理啊,混合啊,只不过是渲染这条路上的不同属性,让这个3D数据怎么画到2D屏幕上,怎么好看,怎么合理,怎么真实做的各种努力罢了。

		//创建一个渲染器实例
		Renderer renderer;

		renderer.Clear();

	    renderer.Draw(va, ib, shader);

三、什么是OpenGL的纹理

在OpenGL中,纹理(Texture)是一种图像,通常被应用到3D模型的表面,以增强场景的真实感。纹理映射(Texture Mapping)是一种技术,将图像映射到模型的表面上,以模拟物体的外观和细节。纹理是一种二维图像,但在3D图形中通常被应用到三角形或其他多边形的表面上。

下面是一些关于OpenGL纹理的基本概念:

纹理坐标: 纹理坐标是一个二维坐标,通常用 (s, t) 表示,它定义了纹理上的一个点。这些坐标用来确定纹理图像在模型表面的位置。

纹理映射: 纹理映射是指将纹理坐标映射到模型的表面上。通过在模型的每个顶点上指定纹理坐标,OpenGL会在这些点之间进行插值,从而将纹理映射到整个模型表面上。

纹理对象: 在OpenGL中,纹理是通过纹理对象(Texture Object)来表示的。每个纹理对象都有一个唯一的标识符,用于在OpenGL中引用这个纹理。

纹理单元: 纹理单元是OpenGL中的一个概念,它表示图形硬件中用于存储和处理纹理的单元。在一个渲染过程中,可以使用多个纹理单元,每个纹理单元可以存储不同的纹理。

纹理过滤: 纹理过滤是指在纹理被映射到模型表面上时,如何处理纹理坐标与纹理像素之间的关系。常见的纹理过滤方式包括最近邻过滤和线性过滤。

Mipmap: Mipmap是一组包含了原始纹理图像不同分辨率版本的图像集合。使用Mipmap可以提高纹理映射的性能和效果,特别是在远处观察模型时。

在OpenGL中,使用纹理可以为模型赋予更加生动和细致的外观,常见的应用包括游戏图形、虚拟现实(VR)和计算机辅助设计(CAD)等领域。

四、怎么利用OpenGL接口操作纹理

现在我们要在我们画的图像上贴图,贴上一个纹理,而这个纹理可能不是一张图,而是经过数学计算来表现得图像,比如光照,我们要计算到底有多亮,哪里亮等等,这就是我理解的纹理。现在呢我们就把一张图片(纹理),简单的渲染到平面的矩形中去。

我们还需要先了解一下什么是纹理插槽
实际上gpu上有许多的纹理插槽,可以一次性绑定多个纹理插槽

在OpenGL中,纹理插槽(Texture Slot)是指一组被用于存储和处理纹理的单元或槽位。这些插槽允许开发者在一个渲染过程中使用多个不同的纹理。OpenGL中,纹理插槽的概念通常涉及到纹理单元(Texture Unit)。

每个纹理单元都可以存储一张纹理,并在着色器中被引用。在渲染时,可以在不同的纹理单元中存储不同的纹理,然后在着色器中通过纹理插槽的索引来选择使用哪个纹理。这种机制允许在一个渲染过程中同时使用多个纹理,比如混合不同的纹理或在一个对象上同时应用多个纹理。

以下是使用纹理插槽的基本步骤:

激活纹理单元: 在渲染前,需要通过 glActiveTexture 函数激活一个或多个纹理单元,指定激活的纹理单元的编号,通常使用 GL_TEXTURE0、GL_TEXTURE1 等常量。例如:

glActiveTexture(GL_TEXTURE0);

绑定纹理到纹理单元: 使用 glBindTexture 函数将纹理对象绑定到激活的纹理单元上。例如:

glBindTexture(GL_TEXTURE_2D, textureID);

设置纹理插槽索引: 在着色器中,通过 sampler2D 类型的 uniform 变量表示纹理插槽。通过 glUniform1i 函数将纹理插槽的索引值传递给 uniform 变量。例如:

glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture"), 0); // 0 表示使用纹理单元 GL_TEXTURE0

在上面的例子中,ourTexture 是在着色器中声明的 sampler2D 类型的 uniform 变量。在着色器中使用纹理: 在片元着色器中,可以使用 texture 函数获取纹理的颜色值,例如:

#version 330 core
...
uniform sampler2D ourTexture; // 纹理插槽

in vec2 TexCoords; // 从顶点着色器传递过来的纹理坐标
out vec4 FragColor;

void main()
{
    FragColor = texture(ourTexture, TexCoords);
}

上述步骤中,纹理插槽的索引通常由 GL_TEXTURE0、GL_TEXTURE1 等常量加上纹理单元的编号组成。这种机制允许同时使用多个纹理,每个纹理使用一个不同的纹理单元。

然后我们就进入代码时间了,我们来看看我们应该使用纹理的整体步骤,不知道怎么来的可以看看我之前的文章,有从头来过的代码:

1.引入png读取库(https://github.com/nothings/stb/blob/master/stb_image.h

    #include "stb_image/stb_image.h"

2.我们读取图片到cpu

	//这个函数是否翻转,是因为PNG在处理图片时常常以左上角为初始坐标,而OPenGL中的纹理则使用左下角为初始坐标
	stbi_set_flip_vertically_on_load(1);
	//stbi_load五个参数分别是图片路径,宽,高,通道,通道种类数,这里种类为agba,所以为4
	m_LocalBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP, 4);

3.然后在OpenGL创建一个纹理

    //创建一个纹理
	GLCall(glGenTextures(1, &m_RendererID));
	//绑定纹理为2D图像纹理
	GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));

4.将着色器绑定到纹理槽

	//激活纹理管道,相当于激活GPU中纹理管道中的一个,可以选择GL_TEXTURE1,GL_TEXTURE2等等
	GLCall(glActiveTexture(GL_TEXTURE0 + slot));
	GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));

5.在着色器中对纹理采样(就是如何取到这些纹理的值,然后利用片段着色器渲染)

void Shader::SetUniform1i(const std::string& name, int value)
{
	GLCall(glUniform1i(GetUniformLocation(name), value));
}


shader.SetUniform1i("u_Texture", 0);

6.然后渲染

封装纹理类的纹理类如下:

#pragma once

#include "Renderer.h"

class Texture
{
	private:
		unsigned int m_RendererID;
		std::string m_FilePath;
		unsigned char* m_LocalBuffer;
		int m_Width, m_Height, m_BPP;
	public:
		Texture(const std::string& path);
		~Texture();

		void Bind(unsigned int slot = 0) const;
		void UnBind() const;

		inline int GetWidth() const { return m_Width; }
		inline int GetHeight() const { return m_Height; }
		inline int GetBPP() const { return m_BPP; }
};







#include "Texture.h"

#include "stb_image/stb_image.h"

Texture::Texture(const std::string& path)
	:m_FilePath(path), m_LocalBuffer(nullptr), m_Width(0), m_Height(0), m_BPP(0)
{
	//这个函数是否翻转,是因为PNG在处理图片时常常以左上角为初始坐标,而OPenGL中的纹理则使用左下角为初始坐标
	stbi_set_flip_vertically_on_load(1);
	//stbi_load五个参数分别是图片路径,宽,高,通道,通道种类数,这里种类为agba,所以为4
	m_LocalBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP, 4);

	//创建一个纹理
	GLCall(glGenTextures(1, &m_RendererID));
	//绑定纹理为2D图像纹理
	GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));

	//这里我们需要设置纹理的参数的一些属性
	//1.target:指定函数的纹理绑定到的目标。必须是GL_TEXTURE_1D、GL_TEXTURE_1D_ARRAY、GL_TEXTURE_2D、
	// GL_TEXTURE_2D_ARRAY、GL_TEXTURE_2D_MULTISAMPLE、GL_TEXTURE_2D_MULTISAMPLE_ARRAY、
	// GL_TEXTURE_3D、GL_TEXTURE_CUBE_MAP、GL_TEXTURE_CUBE_MAP_ARRAY、GL_TEXTURE_RECTANGLE或glTexParameter之一。
	//2.texture:glTextureParameter指定函数的纹理对象名称。
	//3.pname:指定单值纹理参数的符号名称。pname 可以是下列值之一:GL_DEPTH_STENCIL_TEXTURE_MODE、
	// GL_TEXTURE_BASE_LEVEL、GL_TEXTURE_COMPARE_FUNC、GL_TEXTURE_COMPARE_MODE、
	// GL_TEXTURE_LOD_BIASGL_TEXTURE_MIN_FILTER、GL_TEXTURE_MAG_FILTER、
	// GL_TEXTURE_MIN_LOD、GL_TEXTURE_MAX_LOD、GL_TEXTURE_MAX_LEVEL、
	// GL_TEXTURE_SWIZZLE_R、GL_TEXTURE_SWIZZLE_G、GL_TEXTURE_SWIZZLE_B、
	// GL_TEXTURE_SWIZZLE_A、GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_R
	// 对于向量命令 (),pname 也可以是 或 之一。glTexParameter* vGL_TEXTURE_BORDER_COLORGL_TEXTURE_SWIZZLE_RGBA
	//4.param
	//	对于标量命令,指定 pname 的值。
	//	params
	//	对于 vector 命令,指定指向存储 pname 值的数组的指针。
	GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
	GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
	GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
	GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));

	GLCall(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer));

	//解除绑定纹理,像这种创建过程,绑定完之后,就要解绑,
	//这样下一次生产另一个纹理的时候,就可以继续绑定对应的rendererID了 
	GLCall(glBindTexture(GL_TEXTURE_2D, 0));

	//如果m_LocalBuffer有值则释放
	if (m_LocalBuffer)
		stbi_image_free(m_LocalBuffer);
}

Texture::~Texture()
{
	GLCall(glDeleteTextures(1, &m_RendererID));
}

void Texture::Bind(unsigned int slot /*= 0*/) const
{
	//激活纹理管道,相当于激活GPU中纹理管道中的一个,可以选择GL_TEXTURE1,GL_TEXTURE2等等
	GLCall(glActiveTexture(GL_TEXTURE0 + slot));
	GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));
}

void Texture::UnBind() const
{
	//解除绑定纹理
	GLCall(glBindTexture(GL_TEXTURE_2D, 0));
}

当然我们在采样中需要引入纹理坐标,这个纹理坐标就是告诉着色器,我们的图像需要的纹理应该对应关系,比如我一个矩形的左下角-0.5f, -0.5f,对应纹理的左下角, 0.0f, -0.0f,因为纹理是0,0坐标原点。所以我们要把顶点数组中先加两个值,如下,前两个就是layout(location = 0) in vec4 position;后两个layout(location = 1) in vec2 texCoord;这样在传给纹理编辑器就知道渲染什么样的纹理了。 vec4 textColor = texture(u_Texture, v_TexCoord);

 float positions[] = {
 -0.5f, -0.5f, //0
  0.5f, -0.5f, //1
  0.5f,  0.5f, //2
 -0.5f,  0.5f//3
};
由上面的顶点数组变成现在这种带有纹理坐标的数组
float positions[] = {
 -0.5f, -0.5f, 0.0f, -0.0f,//0
  0.5f, -0.5f, 1.0f, -0.0f,//1
  0.5f,  0.5f, 1.0f,  1.0f,//2
 -0.5f,  0.5f, 0.0f,  1.0f//3
};





#着色器代码
#shader vertex
#version 330 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec2 texCoord;

out vec2 v_TexCoord;

//模型视觉投影矩阵
uniform mat4 u_MVP;

void main()
{
    //利用正交矩阵把图形的顶点变形到想要的位置上去
    gl_Position = u_MVP * position;
    v_TexCoord = texCoord;
};

#shader fragment
#version 330 core

layout(location = 0) out vec4 color;

in vec2 v_TexCoord;

uniform vec4 u_Color;
uniform sampler2D u_Texture;

void main()
{
    //通过纹理坐标算出真正需要片段着色器渲染的值
    vec4 textColor = texture(u_Texture, v_TexCoord);
    //color = vec4(1.0, 0.0, 0.0, 1.0);
    //color = u_Color;
    color = textColor;
}

五、Blending混合

在计算机图形学和图像处理中,Alpha 值通常表示颜色的透明度或不透明度。Alpha 通道是RGBA颜色模型的一个组成部分,RGBA 分别代表红色(Red)、绿色(Green)、蓝色(Blue)和 Alpha。

在OpenGL中,Blending(混合)是一种用于在绘制新的像素时将其颜色与已存在的像素颜色进行组合的技术。它允许我们通过控制颜色的透明度来创建半透明效果,以及实现一些特殊的图形效果。

Blending的核心概念是将新像素的颜色与已存在的像素颜色进行组合,产生一个混合后的结果。这个过程涉及到两个颜色的加权组合,通常使用如下的混合方程:

Resulting Color=(source color×source factor)+(destination color×destination factor)

这里,源颜色(source color)是你要绘制的新像素的颜色,目标颜色(destination color)是已存在的像素的颜色。Source Factor 和 Destination Factor 是混合时两个颜色的权重,它们决定了混合的方式。

在OpenGL中,你可以使用以下函数启用和配置混合:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

上述代码中,glEnable(GL_BLEND) 启用了混合功能,而 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 设置了混合因子。在这个例子中,使用了典型的源颜色因子 GL_SRC_ALPHA 和目标颜色因子 GL_ONE_MINUS_SRC_ALPHA,它们适用于基于源颜色的 alpha 混合。

具体到 (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA),混合方程可以展开为:

Resulting Color=(source color × source alpha)+(destination color × (1−source alpha))

这里:

GL_SRC_ALPHA 是源颜色的 alpha 值。
GL_ONE_MINUS_SRC_ALPHA 是 1 减去源颜色的 alpha 值。

OpenGL 还提供了多种混合因子,允许开发者根据需要实现不同的混合效果。以下是一些常见的混合因子:

基于源颜色的混合因子:

GL_SRC_ALPHA:源颜色的 alpha 值。
GL_ONE_MINUS_SRC_ALPHA:1 减去源颜色的 alpha 值。
基于目标颜色的混合因子:

GL_DST_ALPHA:目标颜色的 alpha 值。
GL_ONE_MINUS_DST_ALPHA:1 减去目标颜色的 alpha 值。
常用混合因子:

GL_ONE:表示完全使用源或目标颜色,取决于混合方程中的位置。
GL_ZERO:表示不使用源或目标颜色。
加法混合因子:

GL_SRC_COLOR:源颜色。
GL_DST_COLOR:目标颜色。
GL_ONE_MINUS_SRC_COLOR:1 减去源颜色。
GL_ONE_MINUS_DST_COLOR:1 减去目标颜色。
这些混合因子可以组合使用,以实现不同的混合效果。例如,如果需要实现加法混合,可以使用 (GL_SRC_ALPHA, GL_ONE) 或 (GL_ONE, GL_ONE)。

而在学习中,我遇到了透明图片渲染出现背景不透明的情况,所以需要利用blending来使得图片渲染正常,应该是纹理渲染正常,所以使用了如下代码:

		//启用混合和透明功能
		GLCall(glEnable(GL_BLEND));
		//设置混合和透明功能。。。暂不清楚详细情况
		GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

混合因子的选择取决于具体的需求,不同的混合因子可以用于实现不同的透明度效果、颜色效果或特殊的图形效果。在使用混合时,开发者可以根据实际场景和效果调整混合因子来达到期望的渲染效果。

  • 35
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值