diff --git a/CMakeLists.txt b/CMakeLists.txt index b8b3ddb8f9c7701ec816839d51a761a065fa6bae..29a966663e50c26cf265a44e11d39ad271ffa949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(COMMON_SOURCES common/Linux/MicroGlut.c ) -file(GLOB_RECURSE SRC_SOURCES src/*.cpp src/*.c) +file(GLOB_RECURSE SRC_SOURCES src/*.cpp src/*.c src/*.h) set(SOURCES ${COMMON_SOURCES} ${SRC_SOURCES}) diff --git a/shaders/basic.vert b/shaders/basic.vert index 3774ffde1272d28cb47aa541d97805d61eafe113..f7e6ed93a25b21dc54b66347b068d0d139447ee0 100644 --- a/shaders/basic.vert +++ b/shaders/basic.vert @@ -7,11 +7,17 @@ in vec2 in_TexCoord; uniform mat4 projectionMatrix; uniform mat4 modelToWorldToView; -out float shade; + +out vec3 position; +out vec3 normal; +out vec2 tex_coord; void main(void) { - shade = (mat3(modelToWorldToView)*in_Normal).z; // Fake shading + position = in_Position; + normal = in_Normal; + tex_coord = in_TexCoord; + gl_Position=projectionMatrix*modelToWorldToView*vec4(in_Position, 1.0); } diff --git a/shaders/ground.frag b/shaders/ground.frag index 7d92382799e0b4e4370ba9fc4c86ebcb5e8c3c01..cc59c06dbaf47026028eeac2e8239763cc5d8365 100644 --- a/shaders/ground.frag +++ b/shaders/ground.frag @@ -1,11 +1,30 @@ #version 150 -in float shade; +in vec3 position; +in vec3 normal; +in vec2 tex_coord; + +uniform sampler2D grass; +uniform sampler2D dirt; out vec4 out_Color; +const float tex_scale = 4.0; + +const vec3 sun = normalize(vec3(0, 1, 0)); + +float unlerp(float lo, float hi, float t) +{ + return clamp((t - lo) / (hi - lo), 0.0, 1.0); +} + void main(void) { - out_Color=vec4(shade,shade,shade,1.0); + vec3 nnormal = normalize(normal); + float light = max(0.0, dot(nnormal, sun)); + + vec3 albedo = mix(texture(dirt, tex_coord * tex_scale).rgb, texture(grass, tex_coord * tex_scale).rgb, unlerp(-0.2, 0.2, position.y)); + + out_Color = vec4(albedo * light, 1.0); } diff --git a/shaders/surface.frag b/shaders/surface.frag index 7d92382799e0b4e4370ba9fc4c86ebcb5e8c3c01..e5bbed52d5c3d8572daca35dff3ca6baadc51cd7 100644 --- a/shaders/surface.frag +++ b/shaders/surface.frag @@ -1,11 +1,90 @@ #version 150 -in float shade; +in vec3 world_pos; +in vec3 normal; +in vec2 tex_coord; + +in float test; + +uniform sampler2D sky; +uniform sampler2D ground; +uniform sampler2D ground_depth; +uniform vec3 camera_pos; + +const float PI = 3.1415926535897f; +const float R0 = 0.04; + +const float MIN_T = 0; +const float MAX_T = 10.0; +const float DT = 0.1; out vec4 out_Color; + +vec2 sphere_uv(vec3 direction) +{ + float u = 0.5 + atan(direction.z, direction.x) / (2 * PI); + float v = 0.5 + asin(direction.y) / PI; + + return vec2(u, v); +} + +float fresnel(vec3 normal, vec3 view) { + float cos_theta = dot(normal, -view); + float x = 1.0 - cos_theta; + return mix(R0, 1.0, x * x * x * x * x); +} + +vec2 tex_at(vec3 pos) { + return pos.xz * vec2(0.1f) + vec2(0.5f); +} + +float depth_at(vec3 pos) { + return mix(4.5, -2.0, texture(ground_depth, tex_at(pos)).r); +} + +/** + * Adapted from Inigo Quilez article on raymarching terrain + * https://iquilezles.org/articles/terrainmarching/ + */ +vec3 refraction_ray(vec3 pos, vec3 dir) { + float lh = 0.0f; + float ly = 0.0f; + + for (float t = MIN_T; t < MAX_T; t += DT) { + vec3 cur_pos = pos + t * dir; + + float y_at = depth_at(cur_pos); + if (cur_pos.y < y_at) { + float res_t = t - DT + DT * (lh - ly) / (cur_pos.y - ly - y_at + lh); + + return texture(ground, tex_at(pos + res_t * dir)).rgb; + } + + lh = y_at; + ly = cur_pos.y; + } + + return vec3(1, 0, 1); +} + + void main(void) { - out_Color=vec4(shade,shade,shade,1.0); + vec3 view = normalize(world_pos - camera_pos); + vec3 nnormal = normalize(normal); + vec3 reflected = reflect(view, nnormal); + float R = fresnel(nnormal, view); + // vec3 water_color = vec3(0, 0.05, 0.1); + + vec3 sky_color = texture(sky, sphere_uv(reflected)).rgb; + vec3 refracted = refract(view, nnormal, 0.667); + vec3 water_color = refraction_ray(world_pos, refracted); + + //out_Color = vec4(1,0,1,1); + // out_Color = vec4(refracted * vec3(0.5) + vec3(0.5), 1.0); + out_Color = vec4(mix(water_color, sky_color, R), 1.0); + + // out_Color = texture(ground_depth, tex_coord); } diff --git a/shaders/surface.vert b/shaders/surface.vert index a3adbe90a7a42e871e52704cbed2f275d47f6f1e..69f69d92e4e14c7f0d9fd4a2d91965bd521e400b 100644 --- a/shaders/surface.vert +++ b/shaders/surface.vert @@ -1,17 +1,93 @@ #version 150 -in vec3 in_Position; -in vec3 in_Normal; -in vec2 in_TexCoord; +in vec3 in_Position; +in vec3 in_Normal; +in vec2 in_TexCoord; uniform mat4 projectionMatrix; uniform mat4 modelToWorldToView; +uniform float time; +uniform int in_WavesNum; -out float shade; + +out vec3 world_pos; +out vec3 normal; +out vec2 tex_coord; +out float test; + +const float pi = 3.14159265358979323846; +const float g = 9.82; + +struct Wave { + float L; // Wavelength (distance between waves in world space) + float A; // Amplitude (height from surface to wave crest) + float S; // Speed (distance the crest moves per second) + vec2 D; // Direction (horizontal vector perpendicular to the wave front) +}; + +layout(std140) uniform WaveBuffer { + Wave waves[64]; +}; + +/* Get the offset for a single wave along the y-axis. */ +float get_gerstner_y(Wave wave, float x, float z, float t) { + // float w = 2.0/wave.L; + float w = sqrt(g*(2*pi/wave.L)); + float phase_const = wave.S * w; + + return wave.A * sin(dot(normalize(wave.D) * w, vec2(x, z)) + t * phase_const); +} + +/* Get the offset for a single wave in the xz-plane. */ +float get_gerstner_xz(Wave wave, float dir, float x, float z, float t) { + // float w = 2.0/wave.L; + float w = sqrt(g*(2*pi/wave.L)); + float phase_const = wave.S * w; + float Q = 0.8/(w*wave.A*in_WavesNum); + + return Q * wave.A * dir * cos(dot(normalize(wave.D) * w, vec2(x, z)) + t * phase_const); +} + +/* Get the offset for all the combined waves */ +vec3 get_waves(const vec3 pos, const float t) { + vec3 offset = vec3(0.0, 0.0, 0.0); + + for (int i = 0; i < in_WavesNum; i++) { + offset.y += get_gerstner_y(waves[i], pos.x, pos.z, t); + + offset.x += get_gerstner_xz(waves[i], normalize(waves[i].D).x, pos.x, pos.z, t); + offset.z += get_gerstner_xz(waves[i], normalize(waves[i].D).y, pos.x, pos.z, t); + } + + return offset + vec3(pos.x, 0.0, pos.z); +} + +/* Get the normal for a wave */ +vec3 compute_normal(vec3 pos, float t) { + float delta = 0.001; + + // Calculate the offsets in x and z directions + vec3 offset1 = vec3(pos.x + delta, pos.y, pos.z); + vec3 offset2 = vec3(pos.x - delta, pos.y, pos.z); + float offset_x = (get_waves(offset1, t) - get_waves(offset2, t)).x; + + offset1 = vec3(pos.x, pos.y, pos.z + delta); + offset2 = vec3(pos.x, pos.y, pos.z - delta); + float offset_z = (get_waves(offset1, t) - get_waves(offset2, t)).z; + + // Construct the normal using partial derivatives + return normalize(vec3(-offset_x, 1.0, -offset_z)); +} void main(void) { - shade = (mat3(modelToWorldToView)*in_Normal).z; // Fake shading - gl_Position=projectionMatrix*modelToWorldToView*vec4(in_Position, 1.0); + vec3 offset_pos = get_waves(in_Position, time); + world_pos = offset_pos; + normal = compute_normal(in_Position, time); + tex_coord = in_TexCoord; + + test = in_WavesNum; + + gl_Position = projectionMatrix * modelToWorldToView * vec4(offset_pos, 1.0); } diff --git a/src/hdr.h b/src/hdr.h new file mode 100644 index 0000000000000000000000000000000000000000..a44c4acf114a97ae588b94cdc44f7636b70d3ba4 --- /dev/null +++ b/src/hdr.h @@ -0,0 +1,20 @@ +#pragma once + +#include <GL/gl.h> +#include <array> +#include <string> +#include <vector> + +struct HDRImage { + using Pixel = std::array<float, 3>; + + GLuint width; + GLuint height; + std::vector<Pixel> pixels; + GLuint texId; + + HDRImage(GLuint width, GLuint height, std::vector<Pixel> pixels); + + static HDRImage load_hdr(const std::string& path); +}; + diff --git a/src/main.cpp b/src/main.cpp index 587570f20a54a546e45f2d7df7a20b4ec81d1251..ea289b54f8833bea2228010701348635e42b9e4d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,14 @@ // Revised 2019 with a bit better variable names. // Experimental C++ version 2022. Almost no code changes. -#include <cstdlib> #include "object.h" #include "waterfall.h" +#include "surface.h" #include <GL/gl.h> +#include <cstdlib> +#include <sys/types.h> #include <GL/glext.h> -#include <iostream> -#include <ostream> #define MAIN #include "GL_utilities.h" #include "LittleOBJLoader.h" @@ -18,20 +18,28 @@ // uses framework OpenGL // uses framework Cocoa + + struct Scene { Object ground; - Object surface; + Surface surface; Waterfall waterfall; Object skybox; vec3 sun = vec3(0, 100, 0); GLuint skybox_tex; + FBOstruct *ground_fbo; + GLuint dirt_tex; + GLuint grass_tex; mat4 proj_matrix = perspective(70.0, 1.0, 0.2, 20.0); + mat4 orth_matrix = ortho(-5.0, 5.0, -5.0, 5.0, -2.0, 4.5); + vec3 pos; float yaw; float pitch; mat4 view_matrix = lookAtv(vec3(-5, 4, -5), vec3(0, 0, 0), vec3(0, 1, 0)); + mat4 top_view_matrix = lookAtv(vec3(0, 0, 0), vec3(0, -1, 0), vec3(0, 0, 1)); void init() { Model *ground_model = LoadModel("models/ground.obj"); @@ -50,18 +58,21 @@ struct Scene { loadShaders("shaders/skybox.vert", "shaders/skybox.frag"); printError("compiled shaders"); ground = Object{ground_model, ground_program}; - surface = Object{surface_model, surface_program}; + surface = Surface{surface_model, surface_program}; waterfall = Waterfall{waterfall_model, waterfall_program}; skybox = Object{skybox_model, skybox_program}; + ground_fbo = initFBO2(1024, 1024, GL_LINEAR, true); + LoadTGATextureSimple("textures/sky4k.tga", &skybox_tex); glBindTexture(GL_TEXTURE_2D, skybox_tex); printError("bind texture"); - // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, - // GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, - // GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); printError("tex parameteri"); + + LoadTGATextureSimple("textures/grass.tga", &grass_tex); + LoadTGATextureSimple("textures/dirt.tga", &dirt_tex); + printError("tex parameteri"); } void do_keyboard_input() { @@ -82,14 +93,66 @@ struct Scene { if (glutKeyIsDown('a')) { pos -= speed * right; } - if (glutKeyIsDown('r')) { + if (glutKeyIsDown('r') || glutKeyIsDown('e')) { pos += speed * up; } - if (glutKeyIsDown('f')) { + if (glutKeyIsDown('f') || glutKeyIsDown('q')) { pos -= speed * up; } } + void draw_ground_fbo() + { + useFBO(ground_fbo, nullptr, nullptr); + ground.use(); + GLuint program = ground.program; + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, orth_matrix.m); + glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, top_view_matrix.m); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, grass_tex); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, dirt_tex); + + glUniform1i(glGetUniformLocation(program, "grass"), 0); + glUniform1i(glGetUniformLocation(program, "dirt"), 1); + + ground.draw(); + + useFBO(nullptr, nullptr, nullptr); + } + + void draw_surface() + { + surface.use(); + GLuint program = surface.program; + int elapsed_millis = glutGet(GLUT_ELAPSED_TIME); + float time = elapsed_millis * 0.001f; + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, skybox_tex); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, ground_fbo->texid); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, ground_fbo->depth); + + glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m); + glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m); + glUniform1i(glGetUniformLocation(program, "sky"), 0); + glUniform1i(glGetUniformLocation(program, "ground"), 1); + glUniform1i(glGetUniformLocation(program, "ground_depth"), 2); + glUniform3f(glGetUniformLocation(program, "camera_pos"), pos.x, pos.y, pos.z); + glUniform1f(glGetUniformLocation(program, "time"), time); + + surface.draw(); + } + void update_view_matrix() { vec3 up{0.0, 1.0, 0.0}; mat4 rot = Rx(pitch) * Ry(yaw); @@ -116,28 +179,6 @@ struct Scene { glEnable(GL_CULL_FACE); } - void draw_surface() { - surface.use(); - GLuint program = surface.program; - glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, - GL_TRUE, proj_matrix.m); - glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, - GL_TRUE, view_matrix.m); - - surface.draw(); - } - - void draw_ground() { - ground.use(); - GLuint program = ground.program; - glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, - GL_TRUE, proj_matrix.m); - glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, - GL_TRUE, view_matrix.m); - - ground.draw(); - } - void draw_waterfall() { waterfall.use(); GLuint program = waterfall.program; @@ -154,11 +195,32 @@ struct Scene { waterfall.draw(); waterfall.move_waterfall_balls(); } + void draw_ground() + { + ground.use(); + GLuint program = ground.program; + glUniformMatrix4fv(glGetUniformLocation(program, "projectionMatrix"), 1, GL_TRUE, proj_matrix.m); + glUniformMatrix4fv(glGetUniformLocation(program, "modelToWorldToView"), 1, GL_TRUE, view_matrix.m); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, grass_tex); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, dirt_tex); + + glUniform1i(glGetUniformLocation(program, "grass"), 0); + glUniform1i(glGetUniformLocation(program, "dirt"), 1); + + ground.draw(); + } - void draw() { + + void draw() + { do_keyboard_input(); update_view_matrix(); + draw_ground_fbo(); + draw_skybox(); draw_surface(); draw_ground(); @@ -201,13 +263,13 @@ void display(void) { glutSwapBuffers(); } -void on_mouse_move(int x, int y) { - const float sensitivity = 0.005f; +void on_mouse_move(int x, int y) +{ + const float sensitivity = 0.0025f; if (mouse_y == -1 && mouse_x == -1) { - mouse_x = x; - mouse_y = y; - - return; + mouse_x = x; + mouse_y = y; + return; } int dx = x - mouse_x; @@ -215,9 +277,8 @@ void on_mouse_move(int x, int y) { mouse_x = x; mouse_y = y; - scene.pitch -= dy * sensitivity; + scene.pitch += dy * sensitivity; scene.yaw += dx * sensitivity; - scene.pitch = std::max(-1.5f, std::min(1.5f, scene.pitch)); } diff --git a/src/surface.cpp b/src/surface.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5592da146889d09ce8bd925958482866b5f9855d --- /dev/null +++ b/src/surface.cpp @@ -0,0 +1,79 @@ +#include "surface.h" +#include <iostream> +#include <ostream> +#include <random> + + +Surface::Wave Surface::get_wave() const { + std::random_device rd; + std::mt19937 gen(rd()); + + std::uniform_real_distribution<float> rd_wavelength( + 0.5 * MEDIAN_WAVELENGTH, 2 * MEDIAN_WAVELENGTH + ); + + std::uniform_real_distribution<float> rd_amplitude( + 0.5 * MEDIAN_AMPLITUDE, 2 * MEDIAN_AMPLITUDE + ); + + std::uniform_real_distribution<float> rd_speed( + 0.5 * MEDIAN_SPEED, 2 * MEDIAN_SPEED + ); + + std::uniform_real_distribution<float> rd_dir( + wind_dir - MEDIAN_DIR, wind_dir + MEDIAN_DIR + ); + + float const dir {rd_dir(rd)}; + return { + rd_wavelength(rd), + rd_amplitude(rd), + rd_speed(rd), + {}, + vec2(cos(dir), sin(dir)), + }; +} + +Surface::Surface(Model* model, GLuint program) + : Object{model, program} { + use(); + + // Create a uniform buffer object, used to send waves. + glGenBuffers(1, &ubo); + glBindBuffer(GL_UNIFORM_BUFFER, ubo); + + // Allocate memory in the buffer. + glBufferData( + GL_UNIFORM_BUFFER, sizeof(Wave) * MAX_WAVES, nullptr, GL_STATIC_DRAW + ); + + block_index = glGetUniformBlockIndex(program, "WaveBuffer"); + + // Bind the buffer to a binding point. + glBindBufferBase(GL_UNIFORM_BUFFER, BINDING_POINT, ubo); + + // // Link the UBO to the shader’s uniform block + glUniformBlockBinding(program, block_index, BINDING_POINT); + + for (int i = 0; i < MAX_WAVES; i++) { + waves.push_back(get_wave()); + } +} + +void Surface::draw() const +{ + if (waves.size() > MAX_WAVES) { + std::cerr << "Error: The max limit of waves has been exceeded." << std::endl; + return; + } + + // Bind the buffer before updating it. + glBindBuffer(GL_UNIFORM_BUFFER, ubo); + glBufferSubData(GL_UNIFORM_BUFFER, 0, waves.size() * sizeof(Wave), waves.data()); + + // Upload the number of waves. + glUniform1i(glGetUniformLocation(program, "in_WavesNum"), waves.size()); + + DrawModel(model, program, "in_Position", "in_Normal", "in_TexCoord"); +} + diff --git a/src/surface.h b/src/surface.h new file mode 100644 index 0000000000000000000000000000000000000000..1792d1b273c3befb01db16465ca5a4e719fb31c9 --- /dev/null +++ b/src/surface.h @@ -0,0 +1,39 @@ +#pragma once + +#include "object.h" +#include <vector> + + +class Surface : public Object { +public: + + Surface() = default; + Surface(Model* model, GLuint program); + void draw() const; + +private: + static GLuint const BINDING_POINT {1}; + char const* UNIFORM_BLOCK = "WaveBuffer"; + static int const MAX_WAVES {16}; + static constexpr float const MEDIAN_WAVELENGTH {1.0}; + static constexpr float const MEDIAN_AMPLITUDE {0.003}; + static constexpr float const MEDIAN_SPEED {0.2}; + static constexpr float const MEDIAN_DIR {3.14/2.0}; + + struct Wave { + GLfloat L; // Wavelength (distance between waves in world space) + GLfloat A; // Amplitude (height from surface to wave crest) + GLfloat S; // Speed (distance the crest moves per second) + char padding1[4]; + vec2 D; // Direction (horizontal vector perpendicular to the wave front) + char padding2[8]; + }; + + GLuint ubo; + GLuint block_index; + std::vector<Wave> waves {}; + float wind_dir {0.0}; // Direction of the wind [0-2π] + + Wave get_wave() const; +}; + diff --git a/textures/autumn_field_puresky_4k.hdr b/textures/autumn_field_puresky_4k.hdr new file mode 100644 index 0000000000000000000000000000000000000000..494be719db2d0523f390fa20d9c2575938455cb0 Binary files /dev/null and b/textures/autumn_field_puresky_4k.hdr differ diff --git a/textures/dirt.tga b/textures/dirt.tga new file mode 100644 index 0000000000000000000000000000000000000000..0b87bfe1c249e6b235d4ef3afc6380db70e2f260 Binary files /dev/null and b/textures/dirt.tga differ diff --git a/textures/grass.tga b/textures/grass.tga new file mode 100644 index 0000000000000000000000000000000000000000..841a4ecb5fa5ce8a3aa616491637eb655f5251b6 Binary files /dev/null and b/textures/grass.tga differ