物理模拟-更加真实的动态水面

物理模拟-更加真实的动态水面

书接上回

在上一篇中,我们讨论了简单的动态水面,并使用了正弦波去模拟了水面,但是,显然我们还有东西被遗漏了。

那就是,我们实际上没有计算水面的法向量。看过代码的读者应该知道,我直接使用了网格坐标来代替法线,虽然效果看起

好像还行,但终究是错误的。

本节,我们将讨论如何计算动态水面的法向量,切向量,次法向量

在那之前,我们首先需要用到高等数学中的计算公式,这里我们不展开讲解,具体细节请翻阅高等数学-多元函数微积分的几何应用

计算法向量

隐式曲面方程F(x,y,z) = 0

在点M(x0,y0,z0)的法向量公式:n = (Fx(x0,y0,z0),Fy(x0,y0,z0),Fz(x0,y0,z0))

曲线的参数方程x = x(t),y = y(t),z = z(t)

在点M(x0,y0,z0)切向量公式:T = (x'(t),y'(t),z'(t))

我们可以将曲线参数方程写成下面这样:

x = f(x) = x
y = f(x,z) = Asin(w(x,z)+Φ)
z = f(z) = z

现在,我要计算曲线上某一点的切向量与次法向量,通过二者的叉乘可以计算出法向量

切向量的计算公式如下:

对z轴进行求导

B(x,z) = (fx(x),fx(x,z),fx(z)),fx(x,z)表示方程对x求偏导数
B(x,z) = (1,fx(z),0)

次法向量的计算公式如下:

对x轴进行求导

T(x,y) = (fz(x),fz(x,z),fz(z)),fx(x,z)表示方程对x求偏导数
T(x,y) = (0,fz(z),1)

最终法向量

N(x,y) = T(x,y) x(叉乘) B(x,y)
N(x,y) = (-fx(x,y),1,-fz(x,y))

求的法线后,将法线传入顶点着色器即可

额外提示:由于加入了法线,顶点数组需要进行更改。具体见源代码,这里不在展开解释

最终修改后的水面计算代码

注意,更加真实的水波,应该是波峰更加陡峭,波谷更加平缓,于是这里我进一步使用了这个公式

W i ( x , y , t ) = 2 A i × ( s i n ( D i ⋅ ( x , y ) × w i + t × φ i ) + 1 2 ) k W_i(x,y,t)=2A_i×(\frac{sin(D_i·(x,y)×w_i+t×φ_i)+1}{2})^k Wi(x,y,t)=2Ai×(2sin(Di(x,y)×wi+t×φi)+1)k

int waveNumbers=20;
float A=0.2f;//0.2
float w=PI/2;
int k=3;
void WaterSin(float *vertices, int n)	//水面正弦波数组的计算
{
    for (int i = 0; i < n*n; i=i+1) {
        //环形波
        float x=vertices[i * 6];
        float z=vertices[i * 6 + 2];
        float d = sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength));
        //中心波
        float d1 = sqrt((x)*(x) + (z)*(z));//计算X与Z的二范数
        //平行波
        float d2 = vertices[i*3];
        float sum = 0;
        for(int i=0;i<waveNumbers;i++){
            sum+=2*A/waveNumbers*pow(((sin(w*d+right_time*PI/75)+1)/2),k);
        }
        float ax=1-A*cos(w*d+right_time*PI/75)*(w*(2*x-2*waterLength)/(sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength))));
        float az=1-A*cos(w*d+right_time*PI/75)*(w*(2*z-2*waterLength)/(sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength))));

        vertices[i * 6 + 1] = sum+1.15f;
//        glm::vec3 T=glm::vec3(0.0,ax,1.0);
//        glm::vec3 B=glm::vec3(1.0,az,0.0);
//        glm::vec3 N=glm::cross(T,B);
        vertices[i * 6 + 3]=-ax;
        vertices[i * 6 + 4]=1;
        vertices[i * 6 + 5]=-az;
    }
}

最终效果

虽然效果看起来距离真实的水面还差很远,不过比之前要好很多了!

在这里插入图片描述

完整代码

// Std. Includes
#include <iostream>
#include <string>
#include <cmath>

// GLEW
#define GLEW_STATIC
#include <glad/glad.h>

// GLFW
#include <GLFW/glfw3.h>

#include <myShader2.h>
#include <myCamera.h>
#include <myModel.h>

// GLM Mathemtics
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#define PI 3.1415926


// Function prototypes
unsigned int loadTexture(char const * path);
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;

// Camera
Camera  camera(glm::vec3(0.0f, 0.0f, 3.0f));
GLfloat lastX = WIDTH / 2.0;
GLfloat lastY = HEIGHT / 2.0;
bool    keys[1024];

// Light attributes
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

// Deltatime
GLfloat deltaTime = 0.0f;	// Time between current frame and last frame
float waterTime=0.0f;
GLfloat lastFrame = 0.0f;  	// Time of last frame

//自定义的相关参数
const float waterLength=6.0f;
const int n = 30;	//水面区域日后将会被划分为n行n列,以方便进行动态渲染
float right_time = glfwGetTime(); //控制水面正弦波的初始波动位置的常量

float vertices[n*n*6];	//水面顶点数组
int indices[(n-1)*(n-1)*6];	//水面索引数组

void VertexIndex(float *vertices, int *indices, int n);	//水面顶点数组及索引计算
void WaterSin(float *vertices, int n);	//水面正弦波数组的计算

// The MAIN function, from here we start the application and run the game loop

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_SAMPLES, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    // Create a GLFWwindow object that we can use for GLFW's functions
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    glfwMakeContextCurrent(window);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    // Set the required callback functions
    glfwSetKeyCallback(window, key_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // GLFW Options
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // Define the viewport dimensions
    glViewport(0, 0, WIDTH*2, HEIGHT*2);

    // OpenGL options
    glEnable(GL_DEPTH_TEST);	//开启深度测试
    glEnable(GL_POINT_SMOOTH);	//启用抗锯齿,对点和线
    glEnable(GL_LINE_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);  //GL_NICEST代表图形显示质量优先
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_BLEND);	//启用混合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);	//源的每一个像素的alpha都等于自己的alpha,目标的每一个像素的alpha等于1减去该位置源像素的alpha。 因此不论叠加多少次,亮度不发生变化,规范的讲,指的是把渲染的图像融合到目标区域。


    // 水池与水面模型着色器,共用着色器可以有效减少代码冗余
    Shader lightingShader("shader/lighting_maps.vs", "shader/lighting_maps.fs");

    //加载pool.obj水池模型
    Model ourModel("resources/model/pool.obj");

    // 加载贴图
    GLuint diffuseMap, diffuseMap2;
    glGenTextures(1, &diffuseMap);
    glGenTextures(1, &diffuseMap2);
    int width, height;
    // 漫反射贴图,其实就相当于给我们的水池外边贴上一幅图片,使其看起来像是“木质”的
    //水池贴图
    unsigned int image = loadTexture("resources/textures/pool_map.png");

    //水面贴图
    unsigned int image2 =loadTexture("resources/textures/water.png");

    // 将图片设置为纹理
    lightingShader.use();
    lightingShader.setInt("material.diffuse",0);

    //在动态绘制之前调用函数计算顶点数组值,提高程序效率
    VertexIndex(vertices, indices, n);
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
//    glBindVertexArray(VAO);
//    glBindBuffer(GL_ARRAY_BUFFER, VBO);
//    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, GL_STATIC_DRAW);
//    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices, GL_STATIC_DRAW);
//    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float ), (void*)0);
//    glEnableVertexAttribArray(0);
//    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
//    glEnableVertexAttribArray(1);
//    glBindBuffer(GL_ARRAY_BUFFER, 0);
//    glBindVertexArray(0);

    // Game loop
    while (!glfwWindowShouldClose(window))
    {
        // Calculate deltatime of current frame
        GLfloat currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // Check if any events have been activiated (key pressed, mouse moved etc.) and call corresponding response functions
        glfwPollEvents();
        processInput(window);
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f); //将背景设置为浅灰色
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        //把相应的摄像机位置坐标传给片段着色器,也即是使用使用摄像机对象的位置坐标代替观察者的位置
        lightingShader.use();
        lightingShader.setVec3("light.position",lightPos.x, lightPos.y, lightPos.z-8);
        lightingShader.setVec3("viewPos",camera.Position.x, camera.Position.y, camera.Position.z);

        // 设置光线属性
        lightingShader.setVec3("light.ambient",0.2f, 0.2f, 0.2f);
        lightingShader.setVec3("light.diffuse",0.5f, 0.5f, 0.5f);
        lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
        lightingShader.setFloat("material.shininess",64.0f);

        //摄像机转换矩阵
        glm::mat4 view;
        view = camera.GetViewMatrix();
        glm::mat4 projection = glm::perspective(camera.Zoom, (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 100.0f);
        //得到统一位置
        lightingShader.setMat4("model",glm::mat4(1.0f));
        lightingShader.setMat4("view",view);
        lightingShader.setMat4("projection",projection);

        // 混合环境贴图到纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, image);

        //绘制已加载的模型
        glm::mat4 model;
        model = glm::translate(model, glm::vec3(0.0f, -1.75f, -2.0f)); // 转换它(模型坐标)使得其(模型)可以同时(在程序初始运行时)被我们观察到正面和顶部
        model = glm::scale(model, glm::vec3(0.4f, 0.4f, 0.4f));	// 缩小模型规模使其在场景中显得更加真实
        lightingShader.setMat4("model",model);
//        ourModel.Draw(lightingShader);

        //以下为水面部分
        //time是作为全局变量直接传递到WaterSin函数当中的
        right_time = glfwGetTime()*100;
        WaterSin(vertices, n);


        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices[0], GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(0 * sizeof(float)));
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(1);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);

        //线框模式用于检查三角形索引法绘制是否正确
//        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glBindVertexArray(0);


        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, image2);
        lightingShader.use();

        lightingShader.setMat4("model",model);
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, (n-1)*(n-1)*6, GL_UNSIGNED_INT, 0);
        glfwSwapBuffers(window);	//向屏幕绘制一次,即交换一次缓冲区
    }

    // Terminate GLFW, clearing any resources allocated by GLFW.
    glfwTerminate();
    return 0;

}

// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
    if (key >= 0 && key < 1024)
    {
        if (action == GLFW_PRESS)
            keys[key] = true;
        else if (action == GLFW_RELEASE)
            keys[key] = false;
    }
}

void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);

}

bool firstMouse = true;
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
    float xpos = static_cast<float>(xposIn);
    float ypos = static_cast<float>(yposIn);

    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);

}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

void VertexIndex(float *vertices, int *indices, int n)	//水面顶点数组及索引计算//,glm::vec3 normal
{
    //坐标范围是X轴(-l,l),Z轴(-l,l)
    float valueX = -waterLength;
    float valueZ = waterLength;	//以上两个参数是为了顶点数组从水池左上角开始计算水面顶点数组,这样比较符合日常直觉
    int index = 0;
    float number = 2 * waterLength / (n - 1);	//得到单行/单列的格网数
    //顶点数组计算 注意valueX与valueZ的方向与二维笛卡尔坐标的X和Y方向一致
    for (int i = 0; i < n*n; i++)	//计算的顺序是先保持Z值不变从左到右,然后再移动到下一行进行重复(即优先从左到右,再从上到下)
    {
        vertices[index++] = valueX;
        vertices[index++] = 0;	//该部分的值后续是要动态计算的,因而此处赋值多少都可以
        vertices[index++] = valueZ;
        index+=3;

        valueX += number;	//移动到下一列
        if ((i+1)%n==0)	//代表一行已经计算完毕,故而要将valueX(行首的X值)恢复到3,而对Z进行一次减法,使之移动到下一行
        {
            valueX = -waterLength;
            valueZ -= number;
        }
    }
    index = 0;
    for(int i=0;i<n-1;i++){
        for(int j=0;j<n-1;j++){
            indices[index++] = (j+i*n)*2;//0
            indices[index++] = (j+1+i*n)*2;//2
            indices[index++] = (j+n+i*n)*2;//20
            //三角形B
            indices[index++] = (j + 1 + i*n)*2;//2
            indices[index++] = (j + n + i*n)*2;
            indices[index++] = (j + n + 1 + i*n)*2;
        }
    }
    cout << index << endl;
}

int waveNumbers=20;
float A=0.2f;//0.2
float w=PI/2;
float Q=1.0f;
int k=3;
void WaterSin(float *vertices, int n)	//水面正弦波数组的计算
{
    for (int i = 0; i < n*n; i=i+1) {
        //环形波
        float x=vertices[i * 6];
        float z=vertices[i * 6 + 2];
        float d = sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength));//计算X与Z的二范数
        //中心波
        float d1 = sqrt((x)*(x) + (z)*(z));//计算X与Z的二范数
        //平行波
        float d2 = vertices[i*3];
        //注意,由于1.5是真实的最高坐标,因而我们最后要让正弦波叠加后的最大值比1.5稍低一些从而使得水面看起来不会“溢出去”
        float sum = 0;
        for(int i=0;i<waveNumbers;i++){
            sum+=2*A/waveNumbers*pow(((sin(w*d+right_time*PI/75)+1)/2),k);
        }
        float ax=1-A*cos(w*d+right_time*PI/75)*(w*(2*x-2*waterLength)/(sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength))));
        float az=1-A*cos(w*d+right_time*PI/75)*(w*(2*z-2*waterLength)/(sqrt((x - waterLength)*(x - waterLength) + (z - waterLength)*(z - waterLength))));

        vertices[i * 6 + 1] = sum+1.15f;
//        glm::vec3 T=glm::vec3(0.0,ax,1.0);
//        glm::vec3 B=glm::vec3(1.0,az,0.0);
//        glm::vec3 N=glm::cross(T,B);
        vertices[i * 6 + 3]=-ax;
        vertices[i * 6 + 4]=1;
        vertices[i * 6 + 5]=-az;
    }
}
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值