✠OpenGL-14-其他技术

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✠OpenGL-14-其他技术相关的知识,希望对你有一定的参考价值。

模拟雾的方法有很多种,从非常简单的模型到包含光散射效应的复杂模型。即使非常简单的方法也是有效的。有一种方法是基于物体距眼睛的距离将实际像素颜色与另一种颜色(“雾”的颜色通常是灰色或蓝灰色——也用于背景颜色)混合。

下图说明了这个概念。眼睛(相机)显示在左侧,两个红色物体放置在视锥体中。圆柱体更靠近眼睛,所以它主要是原始颜色(红色);立方体远离眼睛,所以它主要是雾色。对于这个简单的实现,几乎所有的计算都可以在片段着色器中执行。

下面程序显示了一个非常简单的雾算法的相关代码,该算法按照从相机到像素的距离,使用从对象颜色到雾颜色的线性混合。具体来说,此示例将雾添加到以前程序“✠OpenGL-10-增强表面细节”中的高度贴图示例。

// 顶点着色器
#version 430
...
out vec3 vertEyeSpacePos;
...
// 在视觉空间中不考虑透视计算顶点位置,并将它发送给片段着色器
// 变量 p 是高度贴图后的顶点,正如“✠OpenGL-10-增强表面细节”中所述
vertEyeSpacePos = (mv_matrix * p).xyz;

// 片段着色器
#version 430
...
in vec3 vertEyeSpacePos;
out vec4 fragColor;
...
void main() {
	vec4 fogColor = vec4(0.7, 0.8, 0.9, 1.0);// 蓝灰色
	float fogStart = 0.2;
	float fogEnd = 0.8;
	// 在视觉空间中从摄像机到顶点的距离就是到这个顶点的向量的长度,
	// 因为摄像机在视觉空间中的(0,0,0)位置
	float dist = length(vertEyeSpacePos.xyz);
	float fogFactor = clamp((fogEnd-dist)/(fogEnd-fogStart), 0.0, 1.0);
	fragColor = mix(fogColor, texture(t, tc), fogFactor);
}

GLSL 的 clamp()函数用于将此比率限制在值 0.0 和 1.0 之间。然后, GLSL 的 mix()函数
根据 fogFactor 的值返回雾颜色和对象颜色的加权平均值。

genFType clamp(genFType x, genFType minVal, genFType maxVal)
返回:min(max(x, minVal), maxVal)。即,最终的结果∈[minVal,maxVal]
如果minVal > maxVal,结果是未知的。

genFType mix(genFType x, genFType y, genFType a)
返回:x与y的线性混合,例如 x·(1-a)+y·a

令dist=0.1,则(0.8-0.1)/(0.8-0.2)=1.17>1,所以fogFactor=1,fragColor=texture(t,tc),即像素不在雾中,像素颜色就是纹理的颜色。
令dist=0.7,则(0.8-0.7)/(0.8-0.2)=0.17,所以fogFactor=0.17,fragColor=0.83×fogColor+0.17×texture(t,tc),即像素颜色接近雾的颜色。
令dist>=0.8,则fogFactor=0,fragColor=fogColor,即像素颜色就是雾颜色,看不到像素了。

复合、混合、透明度

回忆一下“✠OpenGL-2-图像管线”,像素操作利用 Z 缓冲区,当发现另一个对象在该像素的位置更近时,通过替换现有的像素颜色来实现隐藏面消除。我们实际上可以更好地控制这个过程——可以选择混合两个像素。

当渲染一个像素时,它被称为“源”像素。已经在帧缓冲器中的像素(可能是从先前的对象渲染得来)被称为“目标”像素。 OpenGL 提供了许多选项,用于决定最终将两个像素中的哪一个或者它们的组合,放置在帧缓冲区中。请注意,像素操作步骤不是可编程阶段——因此用于配置所需合成的 OpenGL 工具可在 C++应用程序中(而不是在着色器中)找到。

用于控制合成的两个 OpenGL 函数是 glBlendEquation(mode)和 glBlendFunc(srcFactor, destFactor)。下图显示了合成过程的概述。

合成过程的工作过程如下:
(1)源像素和目标像素分别乘以源因子和目标因子。源和目标因子在 blendFunc()函数调用中指定。
(2)然后使用指定的 blendEquation 来组合修改后的源像素和目标像素以生成新的目标颜色。混合方程在 glBlendEquation()调用中指定。

那些用到“blendColor”的选项需要额外调glBlendColor()来指定将用于计算混合函数结果的常量颜色。还有一些其他混合函数未在表中显示。

glBlendFunc()默认设置 srcFactor 为 GL_ONE(1.0),destFactor 为 GL_ZERO(0.0)。glBlendEquation()的默认值为 GL_FUNC_ADD。 因此,在默认情况下,源像素不变(乘以 1),目标像素被按比例缩小到 0,并且两者相加意味着源像素变为帧缓冲区的颜色。

还有命令 glEnable(GL_BLEND)和 glDisable(GL_BLEND),它们可用于告诉 OpenGL 应用指定的混合,或忽略它。

我们不会在这里说明所有选项的效果,但我们将介绍一些说明性示例。假设我们在C++/OpenGL 应用程序中指定以下设置:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_ADD);

合成将如下进行:
(1)源像素按其 Alpha 值缩放。
(2)目标像素按 1−srcAlpha(源透明度)缩放。
(3)像素值加在一起。

例如,如果源像素为红色,具有 75%不透明度,即[1,0,0,0.75],并且目标像素包含完全
不透明的绿色,即[0,1,0,1],则结果放在帧缓冲区将是:

srcPixel * srcAlpha = [0.75, 0, 0, 0.5625]
destPixel * (1-srcAlpha) = [0, 0.25, 0, 0.25]
resulting pixel = [0.75, 0.25, 0, 0.8125]

也就是说,主要是红色,有些是绿色的,而且基本上是实色。这个设置的总体效果是让目标像素以与源像素的透明度相对应的量显示。在此示例中,帧缓冲区中的像素为绿色,输入像素为红色,透明度为 25%(不透明度为 75%)。因此允许一些绿色通过红色显示。

事实证明,混合函数和混合方程的这些设置在许多情况下都能很好地工作。我们将它们应用到包含两个 3D 模型的场景中的实际示例中去:一个环面和环面前的金字塔。下图显示了这样一个场景,左边是一个不透明的金字塔,右边是金字塔的 Alpha 值设置为 0.8。光照已经添加。

上面效果存在相当明显的不足之处。尽管金字塔模型现在实际上是透明的,但实际透明的金字塔不仅应该显示其背后的对象,还应该显示其自身的背面。

实际上,金字塔的背面没有出现的原因是因为我们启用了背面剔除。一个合理的想法可能是在绘制金字塔时禁用背面剔除。但是,这通常会产生其他伪影,如下图所示。简单地禁用背面剔除的问题在于混合的效果取决于渲染表面的顺序(因为这决定了源像素和目标像素),并且我们不总是能够控制渲染顺序。通常有利的是首先渲染不透明对象,以及在后面的对象(例如环面),最后再渲染透明对象。这也适用于金字塔的表面,并且在这种情况下,包括金字塔底部的两个三角形看起来不同的原因是它们中的一个在金字塔的前面之前被渲染而一个在之后被渲染。诸如此类的伪影有时被称为“顺序”伪影,并且它们可以在透明模型中显示,因为我们不总是能预测其三角形将被渲染的顺序。

我们可以通过从背面开始分别渲染正面和背面来解决金字塔示例中的问题。下面程序显示了执行此操作的代码。我们通过统一变量来指定金字塔的 Alpha 值并传递给着色器程序,然后通过将指定的 Alpha 替换为计算的输出颜色将其应用于片段着色器中。

请注意,要使光照正常工作,我们必须在渲染背面时翻转法向量。我们通过向顶点着色器发送一个标志来完成此操作,然后我们在其中翻转法向量。

void display() {
	...
	aLoc = glGetUniformLocation(renderingProgram, "alpha");
	fLoc = glGetUniformLocation(renderingProgram, "flipNormal");
	...
	glEnable(GL_CULL_FACE);
	...
	glEnable(GL_BLEND);// 配置混合设置
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glBlendEquation(GL_FUNC_ADD);

	glCullFace(GL_FRONT);// 剔除前面,即先渲染金字塔的背面
	glProgramUniform1f(renderingProgram, aLoc, 0.3f);// 背面非常透明
	glProgramUniform1f(renderingProgram, fLoc, -1.0f);// 翻转背面的法向量
	glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);
	
	glCullFace(GL_BACK);// 然后渲染金字塔的正面
	glProgramUniform1f(renderingProgram, aLoc, 0.7f);// 正面略微透明
	glProgramUniform1f(renderingProgram, fLoc, 1.0f);// 正面不需要翻转法向量
	glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);

	glDisable(GL_BLEND);
}
// 顶点着色器
#version 430
...
if (flipNormal < 0)
	varyingNormal = -varyingNormal;
...

// 片段着色器
#version 430
...
fragColor = globalAmbient*material.ambient + ... etc.// 和Blinn-Phone光照一样
fragColor = vec4(fragColor.xyz, alpha);// 使用统一变量发送的alpha值替换

这种“两遍校正”解决方案的结果如下图:

虽然它在这里运行良好,但上述程序中显示的两遍解决方案并不总是足够的。例如,一些更复杂的模型可能具有面向前方的隐藏表面,并且如果这样的对象变得透明,我们的算法将无法渲染模型的那些隐藏的前向部分。 Alec Jacobson 描述了一个适用于大量案例的五遍序列[A. Jacobson, “Cheap Tricks for OpenGL Transparency,” 2012, accessed October 2018.]。

用户定义剪裁平面

OpenGL 不仅可以应用于视锥体,还包括了指定剪裁平面的功能。用户定义的剪裁平面的一个用途是对模型切片。这样就可以通过从简单的模型开始并从中切片来创建复杂的形状。
剪裁平面使用平面的标准数学定义来定义:ax + by + cz + d = 0
其中 a、b、c 和 d 是用来定义有 X、 Y 和 Z 轴的 3D 空间中特定平面的参数。参数表示垂直于平面的向量(a,b,c),以及从原点到平面的距离 d
可以使用 vec4 在顶点着色器中指定这样的平面,如下所示:
vec4 clip_plane = vec4(0.0, 0.0, -1.0, 0.2);
这对应于平面:(0.0) x + (0.0) y + (-1.0) z + 0.2 = 0
然后,通过使用内置的 GLSL 变量 gl_ClipDistance[ ],可以在顶点着色器中实现裁剪,如下例所示:
gl_ClipDistance[0] = dot(clip_plane.xyz, vertPos) + clip_plane.w;
其中,vertPos 指的是在顶点属性(例如来自 VBO)中进入顶点着色器的顶点位置, clip_plane 定义如上。然后我们计算从裁剪平面到传入顶点的带符号距离,如果顶点在平面上,则为 0,或者取决于顶点在平面的哪一侧而为负或正。gl_ClipDistance 数组的下标允许定义多个裁剪距离(即多个平面)。可以定义的最大用户裁剪平面数量取决于图形卡的 OpenGL 实现。

然后必须在 C++/OpenGL 应用程序中启用用户定义的裁剪。内置 OpenGL 标识符GL_CLIP_DISTANCE0、 GL_CLIP_DISTANCE1 等, 对应于每个 gl_ClipDistance[ ]数组元素。
例如,启用第 0 个用户定义剪裁平面,如下所示。
glEnable(GL_CLIP_DISTANCE0);

将前面的步骤应用到我们的发光环面会产生如下图所示的输出, 其中环面的前半部分已经被剪裁了(还应用了旋转以提供更清晰的视图)。
可能看起来好像环面的底部也被修剪了,但这是因为环面的内表面没有被渲染。当裁剪会显示形状的内部表面时,也就需要渲染它们,否则模型将显示得不完整。

渲染内表面需要再次调用 gl_DrawArrays(),并颠倒缠绕顺序。此外,在渲染背向三角形时, 必须反转曲面法向量。

void display() {
	...
	flipLoc = glGetUniformLocation(renderingProgram, "flipNormal");
	...
	glEnable(GL_CLIP_DISTANCE0);// 启用第 0 个用户定义剪裁平面
	// 正常绘制外表面
	glUniform1i(flipLoc, 0);
	glFrontFace(GL_CCW);
	glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
	// 渲染背面,法向量反转
	glUniform1i(flipLoc, 1);
	glFrontFace(GL_CW);
	glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
}
// 顶点着色器
#version 430
...
vec4 clip_plane = vec4(0.0, 0.0, -1.0, 0.5);
uniform int flipNormal;// 反转法向量的标志
...
void main() {
	...
	if (flipNormal == 1)
		varyingNormal = -varyingNormal;
	...
	// 计算从裁剪平面到传入顶点的带符号距离
	gl_ClipDistance[0] = dot(clip_plane.xyz, vertPos) + clip_plane.w;
	...
}

3D纹理

2D 纹理包含由两个变量索引的图像数据,而 3D 纹理包含相同类型的图像数据,但是处在由 3 个变量索引的 3D 结构中。前两个维度仍然代表纹理贴图中的宽度和高度,第三个维度代表深度。

建议将 3D 纹理视为一种物质,我们将其浸没(或“浸入”)被纹理化的对象,从而使对象的表面点从纹理中的相应位置获得颜色。 或者可以想象这个物体被从 3D 纹理“立方体” 中“雕刻”出来,就像雕塑家用一块坚固的大理石雕刻出一个人物一样。

3D 纹理通常是在程序上生成的。根据纹理中的颜色,我们可以构建包含这些颜色的三维数组。 如果纹理包含可以与各种颜色一起使用的“图案”,我们可能会建立一个保存图案的数组,例如 0 和 1。

void generate3Dpattern() {
	for (int x = 0; x < texWidth; x++) {
		for (int y = 0; y < texHeight; y++) {
			for (int z = 0; z < texDepth; z++) {
				if ((y / 10) % 2 == 0)
					tex3Dpattern[x][y][z] = 0.0;
				else
					tex3Dpattern[x][y][z] = 1.0;
			}
		}
	}
}

以上程序生成存储在tex3Dpattern数组中的图案如下:(0呈蓝色,1呈黄色)

y=[0-9]或[20-29]或[40-49]…时,tex3Dpattern=0;
y=[10-19]或[30-39]或[50-59]…时,tex3Dpattern=1;
从而生成只与y轴相关的蓝黄相间的3D图案。
同样,如果生成3D棋盘纹理,算法如下:

void generate3Dpattern() {
	int xStep, yStep, zStep, sumSteps;
	for (int x = 0; x < texWidth; x++) {
		for (int y = 0; y < texHeight; y++) {
			for (int z = 0; z < texDepth; z++) {
				xStep = (x / 10) % 2;
				yStep = (y / 10) % 2;
				zStep = (z / 10) % 2;
				sumSteps = xStep + yStep + zStep;
				if (sumSteps % 2 == 0)
					tex3Dpattern[x][y][z] = 0.0;
				else
					tex3Dpattern[x][y][z] = 1.0;
			}
		}
	}
}


使用条纹图案对对象进行纹理处理,需要执行以下步骤:
(1) 生成如上图所示的图案;
(2) 使用图案填充所需颜色的字节数组;
(3) 将字节数组加载到纹理对象中;
(4) 确定对象顶点的适当3D纹理坐标;
(5) 在片段着色器中使用适当的采样器来纹理化对象。

3D纹理的纹理坐标范围是[0…1],与2D纹理的方式相同。

在大多数情况下,我们希望对象反映纹理图案,就像它被“雕刻”出来一样(或浸入其中)。所以顶点位置本身就是纹理坐标!通常所需的只是应用一些简单的缩放以确保对象的顶点的位置坐标映射到 3D 纹理坐标的范围[0,1]。

. . .
const int texHeight= 200;
const int texWidth = 200;
const int texDepth = 200;
double tex3Dpattern[texWidth][texHeight][texDepth];
. . .
// 按照由 generate3Dpattern()构建的图案,用蓝色、黄色的 RGB 值来填充字节数组
void fillDataArray(GLubyte data[ ]) {
	for (int i=0; i<texWidth; i++) {
		for (int j=0; j<texHeight; j++) {
			for (int k=0; k<texDepth; k++) {
				if (tex3Dpattern[i][j][k] == 1.0) {
// 黄色
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+0]=(GLubyte) 255;// red
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+1]=(GLubyte) 255;// green
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+2]=(GLubyte) 0;// blue
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+3]=(GLubyte) 255;// alpha
				} else {
// 蓝色
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+0]=(GLubyte) 0;// red
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+1]=(GLubyte) 0;// green
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+2]=(GLubyte) 255;// blue
data[i*(texWidth*texHeight*4) + j*(texHeight*4)+ k*4+3]=(GLubyte) 255;// alpha
} } } }
// 构建条纹的 3D 图案
void generate3Dpattern() {
	for (int x=0; x<texWidth; x++) {
		for (int y=0; y<texHeight; y++) {
			for (int z=0; z<texDepth; z++) {
				if ((y/10)%2 == 0)
					tex3Dpattern[x][y][z] = 0.0;
				else
					tex3Dpattern[x][y][z] = 1.0;
} } } }
// 将顺序字节数据数组加载进纹理对象
int load3DTexture() {
	GLuint textureID;
	GLubyte* data = new GLubyte[texWidth*texHeight*texDepth*4];
	fillDataArray(data);
	glGenTextures(1, &textureID);
	glBindTexture(GL_TEXTURE_3D, textureID);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, texWidth, texHeight, texDepth);
	glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, texWidth, texHeight, texDepth,
GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
	return textureID;
}
void init(GLFWwindow* window) {
	. . .
	generate3Dpattern(); // 3D 图案和纹理只加载一次,所以在 init()里作
	stripeTexture = load3DTexture(); // 为 3D 纹理保存整型图案 ID
}
void display(GLFWwindow* window, double currentTime) {
	. . .
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_3D, stripeTexture);
	glDrawArrays(GL_TRIANGLES, 0, numObjVertices);
}
########################################################
// 顶点着色器
. . .
out vec3 originalPosition;// 原始模型顶点将被用于纹理坐标
. . .
void main(void) {
	originalPosition = position;// 将原始模型坐标传递,用作 3D 纹理坐标
	gl_Position = proj_matrix * mv_matrix * 仅在一个片段中隐藏状态栏并在其他片段中显示

最全最详细publiccms其他常用代码片段(内容站点)

Android,从其他片段返回的空列表视图

从其他片段添加新的 RecyclerView 项

如何使用来自其他活动android的片段打开一个活动

在Android Studio片段之间切换时地图片段不隐藏