OpenGL全景图两种实现方式
1. 球形全景图
球形全景图的实现方式又叫environment mapping,它通过将场景中的物体映射到球形表面来实现。在OpenGL中,可以使用纹理映射来实现球形全景图。首先,需要创建一个球形模型,并将场景中的物体映射到球形模型的面上。然后,将球形模型的纹理映射到球形全景图的纹理上,最后,使用纹理映射的球形全景图来渲染场景。
在这方面,我们可以参考Lib Env - Shader API这篇文章说的hdri纹理解析方式
以及全景图shader这篇文章的shader设置
本质上,球形全景图的渲染就是将场景中的物体映射到球形模型的面上,然后使用球形模型的纹理来渲染场景。我们需要在shader中计算每个像素点在球形模型上的位置以及其向量方向,它使用了envIrradiance这个函数来计算给定方向的辐射度(irradiance)。它基于环境的球谐光照(spherical harmonics projection)来计算。这个函数返回一个RGB颜色值,表示在特定方向上的环境光照强度,同样也乘以了环境曝光。
在shader中,我们使用envIrradiance函数来计算每个像素点在球形模型上的位置以及其向量方向,然后使用球形模型的纹理来渲染场景。
在此我将提供一个shader的实现,它基于球形全景图的实现方式。
const GLchar *fragmentShaderSource3D = R"glsl(
#version 100
precision highp float;
uniform vec2 iResolution; //分辨率
uniform vec2 iMouse; //鼠标移动坐标
uniform float iFov; // 视场角uniform变量
uniform sampler2D iChannel1; // HDR环境贴图
// 旋转变换
vec3 rotateX(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
return vec3(p.x, ca*p.y - sa*p.z, sa*p.y + ca*p.z);
}
vec3 rotateY(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
return vec3(ca*p.x + sa*p.z, p.y, -sa*p.x + ca*p.z);
}
// 从HDR环境贴图采样
vec3 envSampleLOD(vec3 dir, float lod)
{
float theta = acos(dir.y);
float phi = atan(dir.z, dir.x);
vec2 uv = vec2(phi / (2.0 * 3.14159265) + 0.5, theta / 3.14159265);
return texture2D(iChannel1, uv, lod).rgb;
}
// 背景
vec3 background(vec3 rd)
{
return envSampleLOD(rd, 0.0);
}
void main()
{
vec2 pixel = (gl_FragCoord.xy / iResolution.xy)*2.0-1.0;
// 计算光线方向
float asp = iResolution.x / iResolution.y;
float fov = radians(iFov);
float fx = tan(fov / 2.0);
float fy = fx / asp;
vec3 rd = normalize(vec3(pixel.x * fx, pixel.y * fy, -1.0));
vec2 mouse = iMouse.xy / iResolution.xy;
float roty = 0.0;
float rotx = 0.0;
rotx = (mouse.y-0.5)*3.0;
roty = -(mouse.x-0.5)*6.0;
rd = rotateX(rd, rotx);
rd = rotateY(rd, roty);
vec3 rgb;
rgb = background(rd);
gl_FragColor = vec4(rgb, 1.0);
}
)glsl";
这里使用了一个名为iChannel1
的纹理输入,该纹理包含了一个HDR环境贴图,并且使用texture2D
函数来采样该贴图;传入窗口大小为其分辨率大小,并且传入鼠标坐标和视场角来控制全景图展示过程中的移动和缩放。
然后由两个函数rotateX和rotateY来控制相机的旋转,这两个函数接收一个向量和一个角度作为输入,并返回旋转后的向量。
最后,使用gl_FragColor
将最终的颜色输出到帧缓冲中。
渲染结果
2. 立方体全景图
立方体全景图的实现方式又叫cubemap,它通过将场景中的物体映射到立方体表面来实现。在OpenGL中,可以使用纹理映射来实现立方体全景图。首先,需要创建一个立方体模型,并将场景中的物体映射到立方体模型的面上。然后,将立方体模型的纹理映射到立方体全景图的纹理上,最后,使用纹理映射的立方体全景图来渲染场景。
在这方面,可以参照Cubemaps的渲染方式,由于在游戏里,天空盒被广泛使用,因此其着色器和demo文件在网上层出不穷,随便找就能找到一大把,在此,我依然使用渲染hdri图类似的方式创建shader。(此时还是基于全景图shader这篇文章的shader设置
,或者说这篇文章就是为cubemap实现定做的)
const GLchar *fragmentShaderSource3D = R"glsl(
#version 100
precision highp float;
uniform vec2 iResolution;
uniform vec2 iMouse;
uniform float iFov; // 视场角uniform变量
uniform sampler2D iChannel1; // HDR环境贴图
// 旋转变换
vec3 rotateX(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
return vec3(p.x, ca*p.y - sa*p.z, sa*p.y + ca*p.z);
}
vec3 rotateY(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
return vec3(ca*p.x + sa*p.z, p.y, -sa*p.x + ca*p.z);
}
// 从HDR环境贴图采样
vec3 envSampleLOD(vec3 dir, float lod)
{
float theta = acos(dir.y);
float phi = atan(dir.z, dir.x);
vec2 uv = vec2(phi / (2.0 * 3.14159265) + 0.5, theta / 3.14159265);
return texture2D(iChannel1, uv, lod).rgb;
}
// 背景
vec3 background(vec3 rd)
{
return envSampleLOD(rd, 0.0);
}
void main()
{
vec2 pixel = (gl_FragCoord.xy / iResolution.xy)*2.0-1.0;
// 计算光线方向
float asp = iResolution.x / iResolution.y;
float fov = radians(iFov);
float fx = tan(fov / 2.0);
float fy = fx / asp;
vec3 rd = normalize(vec3(pixel.x * fx, pixel.y * fy, -1.0));
vec2 mouse = iMouse.xy / iResolution.xy;
float roty = 0.0;
float rotx = 0.0;
rotx = (mouse.y-0.5)*3.0;
roty = -(mouse.x-0.5)*6.0;
rd = rotateX(rd, rotx);
rd = rotateY(rd, roty);
vec3 rgb;
rgb = background(rd);
gl_FragColor = vec4(rgb, 1.0);
}
)glsl";
可以看出,最大的区别在于它们如何从环境贴图中采样颜色,球形贴图使用 2D 纹理 iChannel1 作为 HDR 环境贴图,并且在 envSampleLOD 函数中,根据给定的方向 dir 计算出纹理坐标 uv,其他方面传入参数几乎于解析hdri图相同。
计算过程如下:
- 首先,通过 acos 和 atan 函数将方向向量转换为球面坐标(theta 和 phi)。
- 然后,将球面坐标映射到 2D 纹理坐标 uv。
- 最后使用 texture2D 函数从 2D 纹理 iChannel1 中采样颜色,并指定 LOD(细节级别)参数。
而 CubeMap 则使用 iChannel0 作为立方体贴图,使用 envSampleCube 函数进行采样。
它的特点是:
- textureCube 函数接受一个方向向量 rd 作为参数,用于在 cubemap 的六个面之间进行插值采样。
- 不需要手动计算纹理坐标,因为 textureCube 函数会根据方向向量自动确定采样的纹理面和坐标。
总的来说使用 cubemap 纹理通常更加方便和高效,因为它可以无缝地在不同的方向上进行采样,而无需额外的纹理坐标计算。此外,cubemap 纹理在表示环境光照和反射时更加自然和真实。
但是cubemap需要额外的内存来存储6个面,而球形贴图只需要一个2D纹理。
在此我们模拟提取一个cubemap的6个面,然后将其作为球形贴图的输入的代码。
// 加载 cubemap 纹理
std::vector<std::string> cubemapFaces = {
"path/to/right.jpg",
"path/to/left.jpg",
"path/to/top.jpg",
"path/to/bottom.jpg",
"path/to/front.jpg",
"path/to/back.jpg"
};
glGenTextures(1, &m_CubemapTexture);
glBindTexture(GL_TEXTURE_CUBE_MAP, m_CubemapTexture);
int width, height, channels;
for (unsigned int i = 0; i < cubemapFaces.size(); i++)
{
unsigned char *data = stbi_load(cubemapFaces[i].c_str(), &width, &height, &channels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
}
else
{
std::cout << "Cubemap texture failed to load at path: " << cubemapFaces[i] << std::endl;
stbi_image_free(data);
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);