4

OpenGL实现GPU体渲染 - lisaidy

 1 year ago
source link: https://www.cnblogs.com/lisaidy/p/17185513.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

之前完成了利用OpenGL实现GPU体渲染的实验,现在把完成的工作做一个总结。

本实验demo的完成主要参考了《OpenGL – Build high performance graphics》这本书的体渲染部分和其中的代码,也参考了体绘制光线投射算法这篇博客。关于体渲染的ray-casting光线投射算法原理这里不再介绍,本文主要讲述实现过程。

以下是具体实现过程:

一、三维体数据手动生成并传入三维纹理

1.1三维体数据生成

3117111-20230306170905491-730712608.png

体数据可视化如图所示,产生体数据的代码如下:

点击查看代码

int Dim[3] = { 200,200,200 };//体数据维度大小
int* Data = (int*)malloc(sizeof(int) * Dim[0] * Dim[1] * Dim[2]);
GLubyte CData[200][200][200][4];//存储颜色和不透明度
glm::vec4 smallCubeC = glm::vec4(1.0, 1.0, 0.0, 1.0);//小立方体颜色
glm::vec4 middleSphereC = glm::vec4(1.0, 0.0, 0.0, 1.0);//中间球体颜色
glm::vec4 largeCubeC = glm::vec4(1.0, 1.0, 1.0, 1.0);//大立方体颜色
float smallCubeD = 0.05;//小立方体不透明度
float middleSphereD = 0.015;//中间球体不透明度
float largeCubeD = 0.018;//大立方体不透明度
void GenCube(int x, int y, int z, int side, int density, int* Data, int* Dim)
{
	int max_x = x + side, max_y = y + side, max_z = z + side;
	int Dimxy = Dim[0] * Dim[1];
	for (int k = z; k < max_z; k++)
	{
		for (int j = y; j < max_y; j++)
		{
			for (int i = x; i < max_x; i++)
			{
				Data[k * Dimxy + j * Dim[0] + i] = density;
			}
		}
	}
}


void GenSphere(int x, int y, int z, int radius, int density, int* Data, int* Dim)
{
	int radius2 = radius * radius;
	int Dimxy = Dim[0] * Dim[1];
	for (int k = 0; k < Dim[2]; k++)
	{
		for (int j = 0; j < Dim[1]; j++)
		{
			for (int i = 0; i < Dim[0]; i++)
			{
				if ((i - x) * (i - x) + (j - y) * (j - y) + (k - z) * (k - z) <= radius2)
				{
					Data[k * Dimxy + j * Dim[0] + i] = density;
				}
			}
		}
	}
}

void Classify(GLubyte CData[200][200][200][4], int* Data, int* Dim)//按照所在位置为每个体数据点赋值,颜色和不透明度
{
	int* LinePS = Data;
	for (int k = 0; k < Dim[2]; k++)
	{
		for (int j = 0; j < Dim[1]; j++)
		{
			for (int i = 0; i < Dim[0]; i++)
			{
				if (LinePS[0] <= 100)
				{
					//白色
					CData[i][j][k][0] = 255.0 * largeCubeC[0];
					CData[i][j][k][1] = 255.0 * largeCubeC[1];
					CData[i][j][k][2] = 255.0 * largeCubeC[2];
					CData[i][j][k][3] = largeCubeD*255.0;
				}
				else if (LinePS[0] <= 200)
				{
					//红色
					CData[i][j][k][0] = 255.0 * middleSphereC[0];
					CData[i][j][k][1] = 255.0 * middleSphereC[1];
					CData[i][j][k][2] = 255.0 * middleSphereC[2];
					CData[i][j][k][3] = middleSphereD*255.0;
				}
				else
				{
					//黄色
					CData[i][j][k][0] = 255.0 * smallCubeC[0];
					CData[i][j][k][1] = 255.0 * smallCubeC[1];
					CData[i][j][k][2] = 255.0 * smallCubeC[2];
					CData[i][j][k][3] = smallCubeD*255.0;
				}
				LinePS++;
			}
		}
	}
	//return CDdata[200][200][200][4];
}

void GenerateVolume(int* Data, int* Dim)
{
	GenCube(0, 0, 0, 200, 100, Data, Dim);//大正方体
	GenSphere(100, 100, 100, 80, 200, Data, Dim);//球体
	GenCube(70, 70, 70, 60, 300, Data, Dim);//小正方体
}

手动生成的体数据会更有利于理解光线投射算法体渲染的原理,该体数据本质上就是200x200x200个点,每个点赋予了对应的颜色值和不透明度。

1.2将体数据存入三维纹理

为了在着色器中实现ray-casting光线投射,合成像素值,需要将体数据存入三维纹理中,然后传入到着色器。

点击查看代码

//volume texture ID
GLuint textureID;

bool LoadVolume() {
	GenerateVolume(Data, Dim);//生成原始体数据
	Classify(CData, Data, Dim);//对体数据分类赋予对应颜色值和不透明度

	//generate OpenGL texture
	glGenTextures(1, &textureID);
	glBindTexture(GL_TEXTURE_3D, textureID);
	// set the texture parameters
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	//set the mipmap levels (base and max)
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
	glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 4);
	glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, 200, 200, 200, 0, GL_RGBA, GL_UNSIGNED_BYTE, CData);//将体数据存入3D纹理
	GL_CHECK_ERRORS

	//generate mipmaps
	glGenerateMipmap(GL_TEXTURE_3D);
	return true;
}

将体数据存入3D纹理后就可以在着色器中接收此3D纹理,然后利用坐标采样获得采样点的rgb和不透明度。

二、利用坐标获取片元坐标position对应的3D纹理

2.1坐标映射

接下来如果想要在着色器中实现光线投射算法,就需要能够根据位置坐标获得3D纹理对应坐标的颜色值和不透明度。一个不错的解决办法是构造一个1x1x1的单位立方体包围盒,将3D纹理坐标(范围0~1)映射与立方体坐标对应上,也就是映射。这样就可以通过着色器的内置变量片元坐标position,获取到对应位置坐标的3d纹理颜色和不透明度,然后沿着光线投射方向步进采样合成颜色值。

以下是相关的代码,省略了绘制立方体的部分。由于这里我绘制的立方体是xyz值范围(-0.5~0.5)的立方体,所以要与3D纹理坐标映射,需要做一个变换,即用于采样3D纹理的坐标vUV需要由片元位置坐标加上0.5得到,下面是完成这个映射过程的顶点着色器代码。

点击查看代码

#version 330 core
layout(location = 0) in vec3 vVertex; //object space vertex position
//uniform
uniform mat4 MVP;   
smooth out vec3 vUV; 
void main()
{  
	gl_Position = MVP*vec4(vVertex.xyz,1);
	vUV = vVertex + vec3(0.5);
}

2.2采样纹理,在片元着色器中实现ray-casting

下面是片元着色器的代码,实现了光线投射算法。需要注意的是步进起始点,当视点位于体渲染外的时候,起始点是着色器内部获取的立方体表面的坐标,这是正确的,而当视点移动到体渲染对象的内部观察时,这个起始点就不对了,这时的起始点不应该在立方体表面,而应该以视点作为起始点往视线方向采样,如下图所示。所以需要加一个判断条件,判断视点是否在立方体包围盒内部。

3117111-20230306205135707-1939439714.png

下面是完整的片元着色器代码

点击查看代码

#version 330 core
layout(location = 0) out vec4 vFragColor;	//fragment shader output
smooth in vec3 vUV;				//用于采样3D纹理的坐标
								
uniform sampler3D	volume;		//体数据纹理
uniform vec3		camPos;		//相机位置
uniform vec3		step_size;	//采样步长
//constants
const int MAX_SAMPLES = 300;	
const vec3 texMin = vec3(0);	//最小纹理坐标
const vec3 texMax = vec3(1);	//最大纹理坐标

void main()
{ 
	vec3 dataPos = vUV;    //光线投射起始点坐标
	vec3 geomDir;          //光线步进方向
	if(abs(camPos.x)<=0.5&&abs(camPos.y)<=0.5&&abs(camPos.z)<=0.5)//当相机也就是视点位于体渲染对象内部时,起始点应该改为相机视点的位置坐标作为起始点
	{
		dataPos=camPos+vec3(0.5);
	}
	geomDir = normalize((vUV-vec3(0.5)) - camPos); //由视点坐标和起始点坐标相减得到沿视线方向步进的方向的向量
	vec3 dirStep = geomDir * step_size; 
	bool stop = false; 
	vec4 cumc=vec4(0);
	//沿射线方向采样累积颜色和不透明度
	for (int i = 0; i < MAX_SAMPLES; i++) {
		dataPos = dataPos + dirStep;		
		stop = dot(sign(dataPos-texMin),sign(texMax-dataPos)) < 3.0;
		if (stop) 
			break;
		vec4 samplec=texture(volume, dataPos).rgba;//获取采样点颜色值和不透明度
		cumc[0]+=samplec.r*samplec[3]*(1-cumc[3]);
		cumc[1]+=samplec.g*samplec[3]*(1-cumc[3]);
		cumc[2]+=samplec.b*samplec[3]*(1-cumc[3]);
		cumc[3]+=samplec.a*(1-cumc[3]);
		if( cumc[3]>0.99)
			break;
	} 
	vFragColor=cumc.rgba;
}

三、最终demo效果

最终实现的效果如图所示,为了方便调试,利用imgui添加了一个简单的GUI界面

3117111-20230306211139970-2012416179.gif

这个案例应该会对理解体渲染和GPU实现体渲染有所帮助。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK