OpenGL实现全景图(SkyBox和Hdri双实现方式)

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中,可以使用纹理映射来实现立方体全景图。首先,需要创建一个立方体模型,并将场景中的物体映射到立方体模型的面上。然后,将立方体模型的纹理映射到立方体全景图的纹理上,最后,使用纹理映射的立方体全景图来渲染场景。
cubemap

在这方面,可以参照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);

渲染结果

渲染结果

  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenGL实现鼠标水平方向漫游天空盒的方法如下: 首先,我们需要创建一个窗口和一个OpenGL上下文。然后,我们需要加载天空盒的纹理,并将其绑定到一个立方体的顶点数组对象(VAO)上。 接下来,我们需要设置一个摄像机,使其能够根据鼠标的水平移动来旋转。我们可以使用GLFW的鼠标回调函数来获取鼠标的位置,并计算鼠标的偏移量。然后,我们可以根据鼠标的偏移量来更新摄像机的旋转角度。 最后,我们需要在渲染循环中更新观察矩阵,并将其传递给着色器程序。观察矩阵可以通过glm库的lookAt函数来创建,其中摄像机的位置可以根据鼠标的旋转角度来计算。 下面是一个示例代码,演示了如何使用OpenGL实现鼠标水平方向漫游天空盒: ```cpp #include <GL/glew.h> #include <GLFW/glfw3.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <iostream> // 窗口大小 const GLuint WIDTH = 800, HEIGHT = 600; // 鼠标初始位置 GLfloat lastX = WIDTH / 2.0f; GLfloat lastY = HEIGHT / 2.0f; // 摄像机参数 glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); // 鼠标是否是第一次获取输入 bool firstMouse = true; // 鼠标回调函数 void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if (firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } GLfloat xoffset = xpos - lastX; GLfloat yoffset = lastY - ypos; // 注意这里的坐标系是反的(y轴向上为正) lastX = xpos; lastY = ypos; GLfloat sensitivity = 0.05f; xoffset *= sensitivity; yoffset *= sensitivity; // 根据鼠标的偏移量来更新摄像机的旋转角度 // 这里使用glm库的yaw和pitch函数来实现 // yaw表示绕y轴旋转,pitch表示绕x轴旋转 cameraFront.x = cos(glm::radians(xoffset)) * cameraFront.x - sin(glm::radians(xoffset)) * cameraFront.z; cameraFront.z = sin(glm::radians(xoffset)) * cameraFront.x + cos(glm::radians(xoffset)) * cameraFront.z; cameraFront = glm::normalize(cameraFront); cameraFront.y += yoffset; cameraFront = glm::normalize(cameraFront); } int main() { // 初始化GLFW glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); // 创建窗口 GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "Skybox", nullptr, nullptr); glfwMakeContextCurrent(window); // 初始化GLEW glewExperimental = GL_TRUE; glewInit(); // 设置鼠标回调函数 glfwSetCursorPosCallback(window, mouse_callback); // 设置视口大小 glViewport(0, 0, WIDTH, HEIGHT); // 渲染循环 while (!glfwWindowShouldClose(window)) { // 检查是否有触发事件(例如按下了关闭按钮) glfwPollEvents(); // 清空颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 更新观察矩阵 glm::mat4 view; view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); // 将观察矩阵传递给着色器程序 // ... // 绘制天空盒 // ... // 交换缓冲 glfwSwapBuffers(window); } // 清理资源 glfwTerminate(); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值