物理模拟-更加真实的动态水面
书接上回
在上一篇中,我们讨论了简单的动态水面,并使用了正弦波去模拟了水面,但是,显然我们还有东西被遗漏了。
那就是,我们实际上没有计算水面的法向量。看过代码的读者应该知道,我直接使用了网格坐标来代替法线,虽然效果看起
好像还行,但终究是错误的。
本节,我们将讨论如何计算动态水面的法向量,切向量,次法向量
在那之前,我们首先需要用到高等数学中的计算公式,这里我们不展开讲解,具体细节请翻阅高等数学-多元函数微积分的几何应用
计算法向量
隐式曲面方程: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))
求的法线后,将法线传入顶点着色器即可
额外提示:由于加入了法线,顶点数组需要进行更改。具体见源代码,这里不在展开解释
最终修改后的水面计算代码
注意,更加真实的水波,应该是波峰更加陡峭,波谷更加平缓,于是这里我进一步使用了这个公式
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;
}
|