chore: initial commit
This commit is contained in:
commit
2db9ca215a
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/rust,linux,git
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,linux,git
|
||||||
|
|
||||||
|
### Git ###
|
||||||
|
# Created by git for backups. To disable backups in Git:
|
||||||
|
# $ git config --global mergetool.keepBackup false
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Created by git when using merge tools for conflicts
|
||||||
|
*.BACKUP.*
|
||||||
|
*.BASE.*
|
||||||
|
*.LOCAL.*
|
||||||
|
*.REMOTE.*
|
||||||
|
*_BACKUP_*.txt
|
||||||
|
*_BASE_*.txt
|
||||||
|
*_LOCAL_*.txt
|
||||||
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### Rust ###
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/rust,linux,git
|
||||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sdl3 = "0.18"
|
||||||
|
glow = "0.17"
|
||||||
|
glam = "0.33"
|
||||||
|
noise = "0.9"
|
||||||
|
bytemuck = "1"
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Engine 3D
|
||||||
|
|
||||||
|
Scaffold de engine 3D em Rust usando SDL3 e OpenGL 3.3+.
|
||||||
|
|
||||||
|
- Terreno procedural com Perlin Noise (FBM multi-oitavas)
|
||||||
|
- Malha contínua triangulada
|
||||||
|
- Iluminação difusa + especular (Blinn-Phong) + névoa atmosférica
|
||||||
|
- Mar com ondas de Gerstner, reflexões Fresnel e transparência por profundidade
|
||||||
|
- Câmera FPS livre com mouse look
|
||||||
|
- Game loop com update (60Hz) e render desacoplados
|
||||||
|
|
||||||
|
## Dependências
|
||||||
|
|
||||||
|
| Crate | Uso |
|
||||||
|
|---|---|
|
||||||
|
| `sdl3` | Janela e contexto OpenGL |
|
||||||
|
| `glow` | Bindings OpenGL em Rust |
|
||||||
|
| `glam` | Álgebra linear 3D (Mat4, Vec3) |
|
||||||
|
| `noise` | Perlin Noise / FBM |
|
||||||
|
| `bytemuck` | Cast seguro de slices para bytes |
|
||||||
|
|
||||||
|
**Requer SDL3 instalado no sistema:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install SDL3-devel
|
||||||
|
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt install libsdl3-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compilar e rodar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controles
|
||||||
|
|
||||||
|
| Tecla | Ação |
|
||||||
|
|---|---|
|
||||||
|
| W / A / S / D | Mover câmera (frente/esq/trás/dir) |
|
||||||
|
| Space / Shift | Subir / Descer |
|
||||||
|
| Mouse | Olhar ao redor (FPS look — inicia capturado) |
|
||||||
|
| Tab | Alternar captura do mouse |
|
||||||
|
| ESC | Soltar mouse / Fechar |
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # Inicialização SDL3 + game loop, 3-pass render
|
||||||
|
├── camera.rs # Câmera FPS (WASD, mouse look, view/projection)
|
||||||
|
├── input.rs # Polling eventos SDL3, estado teclas
|
||||||
|
├── terrain.rs # Heightmap Perlin FBM + geração de malha
|
||||||
|
├── water.rs # Malha do mar (plano no nível do mar, 256x256)
|
||||||
|
├── renderer.rs # VAO/VBO/EBO terreno+água, depth FBO, 3-pass pipeline
|
||||||
|
├── shader.rs # Compilação/link GLSL
|
||||||
|
└── shaders/
|
||||||
|
├── terrain.vert # Vertex shader terreno (MVP, normals)
|
||||||
|
├── terrain.frag # Fragment shader terreno (luz, névoa, gradiente por altura)
|
||||||
|
├── water.vert # Vertex shader água (5 ondas Gerstner, normal perturbada)
|
||||||
|
└── water.frag # Fragment shader água (Fresnel, specular, transparência por depth)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline de renderização
|
||||||
|
|
||||||
|
1. **Terreno** — render opaco (escreve cor + depth)
|
||||||
|
2. **Blit depth** — copia depth buffer → FBO com depth texture
|
||||||
|
3. **Água** — render com alpha blending, depth-test on, depth-write off. Lê depth texture para calcular coluna d'água (raso = transparente, fundo = opaco)
|
||||||
103
src/camera.rs
Normal file
103
src/camera.rs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use sdl3::keyboard::Scancode;
|
||||||
|
|
||||||
|
use crate::input::InputState;
|
||||||
|
|
||||||
|
pub struct Camera {
|
||||||
|
pub position: Vec3,
|
||||||
|
yaw: f32,
|
||||||
|
pitch: f32,
|
||||||
|
front: Vec3,
|
||||||
|
up: Vec3,
|
||||||
|
right: Vec3,
|
||||||
|
world_up: Vec3,
|
||||||
|
aspect: f32,
|
||||||
|
fov: f32,
|
||||||
|
near: f32,
|
||||||
|
far: f32,
|
||||||
|
movement_speed: f32,
|
||||||
|
mouse_sensitivity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn new(position: Vec3, aspect: f32) -> Self {
|
||||||
|
let world_up = Vec3::new(0.0, 1.0, 0.0);
|
||||||
|
let mut camera = Self {
|
||||||
|
position,
|
||||||
|
yaw: -90.0_f32,
|
||||||
|
pitch: 0.0,
|
||||||
|
front: Vec3::new(0.0, 0.0, -1.0),
|
||||||
|
up: Vec3::ZERO,
|
||||||
|
right: Vec3::ZERO,
|
||||||
|
world_up,
|
||||||
|
aspect,
|
||||||
|
fov: 60.0,
|
||||||
|
near: 0.1,
|
||||||
|
far: 500.0,
|
||||||
|
movement_speed: 8.0,
|
||||||
|
mouse_sensitivity: 0.1,
|
||||||
|
};
|
||||||
|
camera.update_vectors();
|
||||||
|
camera
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_matrix(&self) -> Mat4 {
|
||||||
|
Mat4::look_at_rh(self.position, self.position + self.front, self.up)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn projection_matrix(&self) -> Mat4 {
|
||||||
|
Mat4::perspective_rh(self.fov.to_radians(), self.aspect, self.near, self.far)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_keyboard(&mut self, input: &InputState, dt: f32) {
|
||||||
|
let velocity = self.movement_speed * dt;
|
||||||
|
|
||||||
|
if input.is_key_down(Scancode::W) {
|
||||||
|
self.position += self.front * velocity;
|
||||||
|
}
|
||||||
|
if input.is_key_down(Scancode::S) {
|
||||||
|
self.position -= self.front * velocity;
|
||||||
|
}
|
||||||
|
if input.is_key_down(Scancode::A) {
|
||||||
|
self.position -= self.right * velocity;
|
||||||
|
}
|
||||||
|
if input.is_key_down(Scancode::D) {
|
||||||
|
self.position += self.right * velocity;
|
||||||
|
}
|
||||||
|
if input.is_key_down(Scancode::Space) {
|
||||||
|
self.position += self.world_up * velocity;
|
||||||
|
}
|
||||||
|
if input.is_key_down(Scancode::LShift) || input.is_key_down(Scancode::RShift) {
|
||||||
|
self.position -= self.world_up * velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_mouse(&mut self, dx: f32, dy: f32) {
|
||||||
|
self.yaw += dx * self.mouse_sensitivity;
|
||||||
|
self.pitch -= dy * self.mouse_sensitivity;
|
||||||
|
|
||||||
|
if self.pitch > 89.0 {
|
||||||
|
self.pitch = 89.0;
|
||||||
|
}
|
||||||
|
if self.pitch < -89.0 {
|
||||||
|
self.pitch = -89.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_vectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_vectors(&mut self) {
|
||||||
|
let yaw_rad = self.yaw.to_radians();
|
||||||
|
let pitch_rad = self.pitch.to_radians();
|
||||||
|
|
||||||
|
self.front = Vec3::new(
|
||||||
|
yaw_rad.cos() * pitch_rad.cos(),
|
||||||
|
pitch_rad.sin(),
|
||||||
|
yaw_rad.sin() * pitch_rad.cos(),
|
||||||
|
)
|
||||||
|
.normalize();
|
||||||
|
|
||||||
|
self.right = self.front.cross(self.world_up).normalize();
|
||||||
|
self.up = self.right.cross(self.front).normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/input.rs
Normal file
29
src/input.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use sdl3::keyboard::Scancode;
|
||||||
|
|
||||||
|
pub struct InputState {
|
||||||
|
pub keys: HashSet<Scancode>,
|
||||||
|
pub mouse_dx: f32,
|
||||||
|
pub mouse_dy: f32,
|
||||||
|
pub mouse_captured: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
keys: HashSet::new(),
|
||||||
|
mouse_dx: 0.0,
|
||||||
|
mouse_dy: 0.0,
|
||||||
|
mouse_captured: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_key_down(&self, scancode: Scancode) -> bool {
|
||||||
|
self.keys.contains(&scancode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_mouse_delta(&mut self) {
|
||||||
|
self.mouse_dx = 0.0;
|
||||||
|
self.mouse_dy = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
175
src/main.rs
Normal file
175
src/main.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
mod camera;
|
||||||
|
mod input;
|
||||||
|
mod renderer;
|
||||||
|
mod shader;
|
||||||
|
mod terrain;
|
||||||
|
mod water;
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use glow::HasContext;
|
||||||
|
use sdl3::event::Event;
|
||||||
|
use sdl3::keyboard::Scancode;
|
||||||
|
|
||||||
|
use camera::Camera;
|
||||||
|
use input::InputState;
|
||||||
|
use renderer::Renderer;
|
||||||
|
use terrain::{compute_ocean_mask, generate_terrain};
|
||||||
|
use water::generate_water;
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
let sdl = sdl3::init().map_err(|e| e.to_string())?;
|
||||||
|
let video = sdl.video().map_err(|e| e.to_string())?;
|
||||||
|
let mouse = sdl.mouse();
|
||||||
|
|
||||||
|
let gl_attr = video.gl_attr();
|
||||||
|
gl_attr.set_context_version(3, 3);
|
||||||
|
gl_attr.set_context_profile(sdl3::video::GLProfile::Core);
|
||||||
|
gl_attr.set_double_buffer(true);
|
||||||
|
gl_attr.set_depth_size(24);
|
||||||
|
gl_attr.set_stencil_size(8);
|
||||||
|
|
||||||
|
let window = video
|
||||||
|
.window("Engine 3D", 1280, 720)
|
||||||
|
.opengl()
|
||||||
|
.resizable()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let gl_context = window.gl_create_context().map_err(|e| e.to_string())?;
|
||||||
|
let _ = window.gl_make_current(&gl_context);
|
||||||
|
|
||||||
|
let gl = unsafe {
|
||||||
|
glow::Context::from_loader_function(|name| {
|
||||||
|
video
|
||||||
|
.gl_get_proc_address(name)
|
||||||
|
.map(|f| f as *const std::ffi::c_void)
|
||||||
|
.unwrap_or(std::ptr::null())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
gl.enable(glow::DEPTH_TEST);
|
||||||
|
gl.enable(glow::CULL_FACE);
|
||||||
|
gl.cull_face(glow::BACK);
|
||||||
|
gl.clear_color(0.5, 0.7, 0.85, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let terrain_mesh = generate_terrain(200, 100.0, 0.02, 15.0, 42);
|
||||||
|
|
||||||
|
let sea_level = terrain_mesh.min_height
|
||||||
|
+ (terrain_mesh.max_height - terrain_mesh.min_height) * 0.40;
|
||||||
|
let ocean_mask = compute_ocean_mask(
|
||||||
|
&terrain_mesh.heightmap,
|
||||||
|
sea_level,
|
||||||
|
terrain_mesh.grid_resolution,
|
||||||
|
);
|
||||||
|
let mask_res = terrain_mesh.grid_resolution + 1;
|
||||||
|
|
||||||
|
let water_mesh = generate_water(
|
||||||
|
100.0,
|
||||||
|
terrain_mesh.min_height,
|
||||||
|
terrain_mesh.max_height,
|
||||||
|
256,
|
||||||
|
);
|
||||||
|
|
||||||
|
let win_size = window.size();
|
||||||
|
let renderer = Renderer::new(
|
||||||
|
&gl,
|
||||||
|
&terrain_mesh,
|
||||||
|
&water_mesh,
|
||||||
|
&ocean_mask,
|
||||||
|
mask_res,
|
||||||
|
win_size.0 as i32,
|
||||||
|
win_size.1 as i32,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut camera = Camera::new(glam::Vec3::new(0.0, 15.0, 10.0), 1280.0 / 720.0);
|
||||||
|
let mut input = InputState::new();
|
||||||
|
input.mouse_captured = true;
|
||||||
|
mouse.set_relative_mouse_mode(&window, true);
|
||||||
|
|
||||||
|
let mut event_pump = sdl.event_pump().map_err(|e| e.to_string())?;
|
||||||
|
let mut last_frame = Instant::now();
|
||||||
|
let mut running = true;
|
||||||
|
let mut total_time: f32 = 0.0;
|
||||||
|
|
||||||
|
while running {
|
||||||
|
let current = Instant::now();
|
||||||
|
let dt = current.duration_since(last_frame).as_secs_f32();
|
||||||
|
last_frame = current;
|
||||||
|
total_time += dt;
|
||||||
|
|
||||||
|
input.reset_mouse_delta();
|
||||||
|
|
||||||
|
for event in event_pump.poll_iter() {
|
||||||
|
match event {
|
||||||
|
Event::Quit { .. } => running = false,
|
||||||
|
Event::KeyDown {
|
||||||
|
scancode: Some(sc),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
match sc {
|
||||||
|
Scancode::Escape => {
|
||||||
|
if input.mouse_captured {
|
||||||
|
input.mouse_captured = false;
|
||||||
|
mouse.set_relative_mouse_mode(&window, false);
|
||||||
|
} else {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scancode::Tab => {
|
||||||
|
input.mouse_captured = !input.mouse_captured;
|
||||||
|
mouse.set_relative_mouse_mode(&window, input.mouse_captured);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
input.keys.insert(sc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::KeyUp {
|
||||||
|
scancode: Some(sc),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
input.keys.remove(&sc);
|
||||||
|
}
|
||||||
|
Event::MouseMotion { xrel, yrel, .. } if input.mouse_captured => {
|
||||||
|
input.mouse_dx += xrel as f32;
|
||||||
|
input.mouse_dy += yrel as f32;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.process_keyboard(&input, dt);
|
||||||
|
if input.mouse_captured {
|
||||||
|
camera.process_mouse(input.mouse_dx, input.mouse_dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = camera.view_matrix();
|
||||||
|
let proj = camera.projection_matrix();
|
||||||
|
let pos = camera.position;
|
||||||
|
|
||||||
|
renderer.render_terrain(&gl, &view, &proj, pos);
|
||||||
|
renderer.copy_depth_to_texture(&gl);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
gl.enable(glow::BLEND);
|
||||||
|
gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
|
||||||
|
gl.depth_mask(false);
|
||||||
|
}
|
||||||
|
renderer.render_water(&gl, &view, &proj, pos, total_time);
|
||||||
|
unsafe {
|
||||||
|
gl.depth_mask(true);
|
||||||
|
gl.disable(glow::BLEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl_swap_window();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
334
src/renderer.rs
Normal file
334
src/renderer.rs
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use glow::HasContext;
|
||||||
|
|
||||||
|
use crate::shader::ShaderProgram;
|
||||||
|
use crate::terrain::TerrainMesh;
|
||||||
|
use crate::water::WaterMesh;
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
terrain_vao: glow::VertexArray,
|
||||||
|
_terrain_vbo: glow::Buffer,
|
||||||
|
_terrain_ebo: glow::Buffer,
|
||||||
|
terrain_shader: ShaderProgram,
|
||||||
|
terrain_index_count: i32,
|
||||||
|
terrain_min_height: f32,
|
||||||
|
terrain_max_height: f32,
|
||||||
|
|
||||||
|
water_vao: glow::VertexArray,
|
||||||
|
_water_vbo: glow::Buffer,
|
||||||
|
_water_ebo: glow::Buffer,
|
||||||
|
water_shader: ShaderProgram,
|
||||||
|
water_index_count: i32,
|
||||||
|
|
||||||
|
depth_texture: glow::Texture,
|
||||||
|
depth_fbo: glow::Framebuffer,
|
||||||
|
ocean_mask_texture: glow::Texture,
|
||||||
|
terrain_scale: f32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_mesh_vao(
|
||||||
|
gl: &glow::Context,
|
||||||
|
vertices: &[f32],
|
||||||
|
indices: &[u32],
|
||||||
|
) -> Result<(glow::VertexArray, glow::Buffer, glow::Buffer), String> {
|
||||||
|
unsafe {
|
||||||
|
let vao = gl.create_vertex_array().map_err(|e| e.to_string())?;
|
||||||
|
let vbo = gl.create_buffer().map_err(|e| e.to_string())?;
|
||||||
|
let ebo = gl.create_buffer().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
gl.bind_vertex_array(Some(vao));
|
||||||
|
|
||||||
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||||
|
gl.buffer_data_u8_slice(
|
||||||
|
glow::ARRAY_BUFFER,
|
||||||
|
bytemuck::cast_slice(vertices),
|
||||||
|
glow::STATIC_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
|
||||||
|
gl.buffer_data_u8_slice(
|
||||||
|
glow::ELEMENT_ARRAY_BUFFER,
|
||||||
|
bytemuck::cast_slice(indices),
|
||||||
|
glow::STATIC_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
let stride = 8 * std::mem::size_of::<f32>() as i32;
|
||||||
|
|
||||||
|
gl.vertex_attrib_pointer_f32(0, 3, glow::FLOAT, false, stride, 0);
|
||||||
|
gl.enable_vertex_attrib_array(0);
|
||||||
|
|
||||||
|
gl.vertex_attrib_pointer_f32(
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
glow::FLOAT,
|
||||||
|
false,
|
||||||
|
stride,
|
||||||
|
(3 * std::mem::size_of::<f32>()) as i32,
|
||||||
|
);
|
||||||
|
gl.enable_vertex_attrib_array(1);
|
||||||
|
|
||||||
|
gl.vertex_attrib_pointer_f32(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
glow::FLOAT,
|
||||||
|
false,
|
||||||
|
stride,
|
||||||
|
(6 * std::mem::size_of::<f32>()) as i32,
|
||||||
|
);
|
||||||
|
gl.enable_vertex_attrib_array(2);
|
||||||
|
|
||||||
|
gl.bind_vertex_array(None);
|
||||||
|
|
||||||
|
Ok((vao, vbo, ebo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub fn new(
|
||||||
|
gl: &glow::Context,
|
||||||
|
terrain: &TerrainMesh,
|
||||||
|
water: &WaterMesh,
|
||||||
|
ocean_mask: &[u8],
|
||||||
|
mask_resolution: usize,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let terrain_shader = ShaderProgram::new(
|
||||||
|
gl,
|
||||||
|
include_str!("shaders/terrain.vert"),
|
||||||
|
include_str!("shaders/terrain.frag"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let water_shader = ShaderProgram::new(
|
||||||
|
gl,
|
||||||
|
include_str!("shaders/water.vert"),
|
||||||
|
include_str!("shaders/water.frag"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (terrain_vao, terrain_vbo, terrain_ebo) =
|
||||||
|
setup_mesh_vao(gl, &terrain.vertices, &terrain.indices)?;
|
||||||
|
|
||||||
|
let (water_vao, water_vbo, water_ebo) =
|
||||||
|
setup_mesh_vao(gl, &water.vertices, &water.indices)?;
|
||||||
|
|
||||||
|
let (depth_texture, depth_fbo) = unsafe { create_depth_texture(gl, width, height)? };
|
||||||
|
|
||||||
|
let ocean_mask_texture =
|
||||||
|
unsafe { upload_ocean_mask(gl, ocean_mask, mask_resolution)? };
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
terrain_vao,
|
||||||
|
_terrain_vbo: terrain_vbo,
|
||||||
|
_terrain_ebo: terrain_ebo,
|
||||||
|
terrain_shader,
|
||||||
|
terrain_index_count: terrain.index_count,
|
||||||
|
terrain_min_height: terrain.min_height,
|
||||||
|
terrain_max_height: terrain.max_height,
|
||||||
|
|
||||||
|
water_vao,
|
||||||
|
_water_vbo: water_vbo,
|
||||||
|
_water_ebo: water_ebo,
|
||||||
|
water_shader,
|
||||||
|
water_index_count: water.index_count,
|
||||||
|
|
||||||
|
depth_texture,
|
||||||
|
depth_fbo,
|
||||||
|
ocean_mask_texture,
|
||||||
|
terrain_scale: terrain.world_scale,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_terrain(
|
||||||
|
&self,
|
||||||
|
gl: &glow::Context,
|
||||||
|
view: &Mat4,
|
||||||
|
projection: &Mat4,
|
||||||
|
camera_pos: Vec3,
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
self.terrain_shader.bind(gl);
|
||||||
|
|
||||||
|
let model = Mat4::IDENTITY;
|
||||||
|
self.terrain_shader.set_mat4(gl, "model", &model);
|
||||||
|
self.terrain_shader.set_mat4(gl, "view", view);
|
||||||
|
self.terrain_shader.set_mat4(gl, "projection", projection);
|
||||||
|
|
||||||
|
let light_dir = glam::Vec3::new(0.8, 1.0, 0.3).normalize();
|
||||||
|
let light_color = glam::Vec3::new(1.0, 0.95, 0.8);
|
||||||
|
|
||||||
|
self.terrain_shader.set_vec3(gl, "lightDir", light_dir);
|
||||||
|
self.terrain_shader.set_vec3(gl, "lightColor", light_color);
|
||||||
|
self.terrain_shader.set_vec3(gl, "viewPos", camera_pos);
|
||||||
|
|
||||||
|
let fog_color = glam::Vec3::new(0.5, 0.7, 0.85);
|
||||||
|
self.terrain_shader.set_vec3(gl, "fogColor", fog_color);
|
||||||
|
self.terrain_shader.set_float(gl, "fogDensity", 0.003);
|
||||||
|
|
||||||
|
self.terrain_shader
|
||||||
|
.set_float(gl, "minHeight", self.terrain_min_height);
|
||||||
|
self.terrain_shader
|
||||||
|
.set_float(gl, "maxHeight", self.terrain_max_height);
|
||||||
|
|
||||||
|
gl.bind_vertex_array(Some(self.terrain_vao));
|
||||||
|
gl.draw_elements(
|
||||||
|
glow::TRIANGLES,
|
||||||
|
self.terrain_index_count,
|
||||||
|
glow::UNSIGNED_INT,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bind_vertex_array(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_depth_to_texture(&self, gl: &glow::Context) {
|
||||||
|
unsafe {
|
||||||
|
gl.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
|
||||||
|
gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(self.depth_fbo));
|
||||||
|
gl.blit_framebuffer(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
glow::DEPTH_BUFFER_BIT,
|
||||||
|
glow::NEAREST,
|
||||||
|
);
|
||||||
|
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_water(
|
||||||
|
&self,
|
||||||
|
gl: &glow::Context,
|
||||||
|
view: &Mat4,
|
||||||
|
projection: &Mat4,
|
||||||
|
camera_pos: Vec3,
|
||||||
|
time: f32,
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
self.water_shader.bind(gl);
|
||||||
|
|
||||||
|
let model = Mat4::IDENTITY;
|
||||||
|
self.water_shader.set_mat4(gl, "model", &model);
|
||||||
|
self.water_shader.set_mat4(gl, "view", view);
|
||||||
|
self.water_shader.set_mat4(gl, "projection", projection);
|
||||||
|
self.water_shader.set_float(gl, "time", time);
|
||||||
|
|
||||||
|
let light_dir = glam::Vec3::new(0.8, 1.0, 0.3).normalize();
|
||||||
|
let light_color = glam::Vec3::new(1.0, 0.95, 0.8);
|
||||||
|
|
||||||
|
self.water_shader.set_vec3(gl, "lightDir", light_dir);
|
||||||
|
self.water_shader.set_vec3(gl, "lightColor", light_color);
|
||||||
|
self.water_shader.set_vec3(gl, "viewPos", camera_pos);
|
||||||
|
|
||||||
|
self.water_shader
|
||||||
|
.set_vec3(gl, "waterDeep", glam::Vec3::new(0.0, 0.2, 0.4));
|
||||||
|
self.water_shader
|
||||||
|
.set_vec3(gl, "waterShallow", glam::Vec3::new(0.0, 0.5, 0.7));
|
||||||
|
self.water_shader
|
||||||
|
.set_vec3(gl, "skyColor", glam::Vec3::new(0.5, 0.7, 0.85));
|
||||||
|
self.water_shader.set_float(gl, "maxWaterDepth", 5.0);
|
||||||
|
|
||||||
|
let fog_color = glam::Vec3::new(0.5, 0.7, 0.85);
|
||||||
|
self.water_shader.set_vec3(gl, "fogColor", fog_color);
|
||||||
|
self.water_shader.set_float(gl, "fogDensity", 0.003);
|
||||||
|
|
||||||
|
self.water_shader
|
||||||
|
.set_float(gl, "terrainScale", self.terrain_scale);
|
||||||
|
|
||||||
|
gl.active_texture(glow::TEXTURE0);
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, Some(self.depth_texture));
|
||||||
|
|
||||||
|
gl.active_texture(glow::TEXTURE1);
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, Some(self.ocean_mask_texture));
|
||||||
|
self.water_shader.set_int(gl, "oceanMask", 1);
|
||||||
|
|
||||||
|
gl.bind_vertex_array(Some(self.water_vao));
|
||||||
|
gl.draw_elements(
|
||||||
|
glow::TRIANGLES,
|
||||||
|
self.water_index_count,
|
||||||
|
glow::UNSIGNED_INT,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bind_vertex_array(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn create_depth_texture(
|
||||||
|
gl: &glow::Context,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<(glow::Texture, glow::Framebuffer), String> {
|
||||||
|
let texture = gl.create_texture().map_err(|e| e.to_string())?;
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, Some(texture));
|
||||||
|
gl.tex_image_2d(
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
glow::DEPTH_COMPONENT24 as i32,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0,
|
||||||
|
glow::DEPTH_COMPONENT,
|
||||||
|
glow::FLOAT,
|
||||||
|
glow::PixelUnpackData::Slice(None),
|
||||||
|
);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
|
||||||
|
|
||||||
|
let fbo = gl.create_framebuffer().map_err(|e| e.to_string())?;
|
||||||
|
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
|
||||||
|
gl.framebuffer_texture_2d(
|
||||||
|
glow::FRAMEBUFFER,
|
||||||
|
glow::DEPTH_ATTACHMENT,
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
Some(texture),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||||
|
|
||||||
|
Ok((texture, fbo))
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn upload_ocean_mask(
|
||||||
|
gl: &glow::Context,
|
||||||
|
data: &[u8],
|
||||||
|
resolution: usize,
|
||||||
|
) -> Result<glow::Texture, String> {
|
||||||
|
let texture = gl.create_texture().map_err(|e| e.to_string())?;
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, Some(texture));
|
||||||
|
gl.tex_image_2d(
|
||||||
|
glow::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
glow::RGBA as i32,
|
||||||
|
resolution as i32,
|
||||||
|
resolution as i32,
|
||||||
|
0,
|
||||||
|
glow::RGBA,
|
||||||
|
glow::UNSIGNED_BYTE,
|
||||||
|
glow::PixelUnpackData::Slice(Some(data)),
|
||||||
|
);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
|
||||||
|
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
|
||||||
|
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||||
|
|
||||||
|
Ok(texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Renderer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/shader.rs
Normal file
98
src/shader.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use glow::HasContext;
|
||||||
|
|
||||||
|
pub struct ShaderProgram {
|
||||||
|
program: glow::Program,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShaderProgram {
|
||||||
|
pub fn new(gl: &glow::Context, vert_src: &str, frag_src: &str) -> Result<Self, String> {
|
||||||
|
unsafe {
|
||||||
|
let vertex_shader = compile_shader(gl, glow::VERTEX_SHADER, vert_src)?;
|
||||||
|
let fragment_shader = compile_shader(gl, glow::FRAGMENT_SHADER, frag_src)?;
|
||||||
|
|
||||||
|
let program = gl.create_program().map_err(|e| e.to_string())?;
|
||||||
|
gl.attach_shader(program, vertex_shader);
|
||||||
|
gl.attach_shader(program, fragment_shader);
|
||||||
|
gl.link_program(program);
|
||||||
|
|
||||||
|
if !gl.get_program_link_status(program) {
|
||||||
|
let log = gl.get_program_info_log(program);
|
||||||
|
gl.delete_program(program);
|
||||||
|
gl.delete_shader(vertex_shader);
|
||||||
|
gl.delete_shader(fragment_shader);
|
||||||
|
return Err(format!("Program link failed: {log}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.delete_shader(vertex_shader);
|
||||||
|
gl.delete_shader(fragment_shader);
|
||||||
|
|
||||||
|
Ok(Self { program })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind(&self, gl: &glow::Context) {
|
||||||
|
unsafe {
|
||||||
|
gl.use_program(Some(self.program));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mat4(&self, gl: &glow::Context, name: &str, mat: &Mat4) {
|
||||||
|
unsafe {
|
||||||
|
let location = gl.get_uniform_location(self.program, name);
|
||||||
|
if let Some(loc) = location {
|
||||||
|
gl.uniform_matrix_4_f32_slice(Some(&loc), false, mat.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_vec3(&self, gl: &glow::Context, name: &str, vec: Vec3) {
|
||||||
|
unsafe {
|
||||||
|
let location = gl.get_uniform_location(self.program, name);
|
||||||
|
if let Some(loc) = location {
|
||||||
|
gl.uniform_3_f32(Some(&loc), vec.x, vec.y, vec.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_float(&self, gl: &glow::Context, name: &str, value: f32) {
|
||||||
|
unsafe {
|
||||||
|
let location = gl.get_uniform_location(self.program, name);
|
||||||
|
if let Some(loc) = location {
|
||||||
|
gl.uniform_1_f32(Some(&loc), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_int(&self, gl: &glow::Context, name: &str, value: i32) {
|
||||||
|
unsafe {
|
||||||
|
let location = gl.get_uniform_location(self.program, name);
|
||||||
|
if let Some(loc) = location {
|
||||||
|
gl.uniform_1_i32(Some(&loc), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ShaderProgram {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn compile_shader(
|
||||||
|
gl: &glow::Context,
|
||||||
|
shader_type: u32,
|
||||||
|
source: &str,
|
||||||
|
) -> Result<glow::Shader, String> {
|
||||||
|
let shader = gl.create_shader(shader_type).map_err(|e| e.to_string())?;
|
||||||
|
gl.shader_source(shader, source);
|
||||||
|
gl.compile_shader(shader);
|
||||||
|
|
||||||
|
if !gl.get_shader_compile_status(shader) {
|
||||||
|
let log = gl.get_shader_info_log(shader);
|
||||||
|
gl.delete_shader(shader);
|
||||||
|
return Err(format!("Shader compilation failed: {log}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(shader)
|
||||||
|
}
|
||||||
60
src/shaders/terrain.frag
Normal file
60
src/shaders/terrain.frag
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#version 330 core
|
||||||
|
|
||||||
|
in vec3 FragPos;
|
||||||
|
in vec3 Normal;
|
||||||
|
in float Height;
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
uniform vec3 lightDir;
|
||||||
|
uniform vec3 lightColor;
|
||||||
|
uniform vec3 viewPos;
|
||||||
|
|
||||||
|
uniform vec3 fogColor;
|
||||||
|
uniform float fogDensity;
|
||||||
|
|
||||||
|
uniform float minHeight;
|
||||||
|
uniform float maxHeight;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
float ambientStrength = 0.25;
|
||||||
|
vec3 ambient = ambientStrength * lightColor;
|
||||||
|
|
||||||
|
vec3 norm = normalize(Normal);
|
||||||
|
vec3 lightDirection = normalize(lightDir);
|
||||||
|
float diff = max(dot(norm, lightDirection), 0.0);
|
||||||
|
vec3 diffuse = diff * lightColor;
|
||||||
|
|
||||||
|
float specularStrength = 0.5;
|
||||||
|
vec3 viewDirection = normalize(viewPos - FragPos);
|
||||||
|
vec3 halfwayDir = normalize(lightDirection + viewDirection);
|
||||||
|
float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0);
|
||||||
|
vec3 specular = specularStrength * spec * lightColor;
|
||||||
|
|
||||||
|
float t = (Height - minHeight) / (maxHeight - minHeight);
|
||||||
|
t = clamp(t, 0.0, 1.0);
|
||||||
|
|
||||||
|
vec3 green = vec3(0.2, 0.6, 0.1);
|
||||||
|
vec3 brown = vec3(0.55, 0.35, 0.1);
|
||||||
|
vec3 gray = vec3(0.5, 0.5, 0.5);
|
||||||
|
vec3 white = vec3(0.9, 0.9, 0.95);
|
||||||
|
|
||||||
|
vec3 terrainColor;
|
||||||
|
if (t < 0.4)
|
||||||
|
terrainColor = mix(green, brown, t / 0.4);
|
||||||
|
else if (t < 0.7)
|
||||||
|
terrainColor = mix(brown, gray, (t - 0.4) / 0.3);
|
||||||
|
else
|
||||||
|
terrainColor = mix(gray, white, (t - 0.7) / 0.3);
|
||||||
|
|
||||||
|
vec3 objectColor = terrainColor;
|
||||||
|
vec3 result = (ambient + diffuse + specular) * objectColor;
|
||||||
|
|
||||||
|
float distance = length(viewPos - FragPos);
|
||||||
|
float fogFactor = 1.0 - exp(-distance * fogDensity);
|
||||||
|
fogFactor = clamp(fogFactor, 0.0, 1.0);
|
||||||
|
result = mix(result, fogColor, fogFactor);
|
||||||
|
|
||||||
|
FragColor = vec4(result, 1.0);
|
||||||
|
}
|
||||||
21
src/shaders/terrain.vert
Normal file
21
src/shaders/terrain.vert
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#version 330 core
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 aPos;
|
||||||
|
layout (location = 1) in vec3 aNormal;
|
||||||
|
layout (location = 2) in vec2 aTexCoord;
|
||||||
|
|
||||||
|
uniform mat4 model;
|
||||||
|
uniform mat4 view;
|
||||||
|
uniform mat4 projection;
|
||||||
|
|
||||||
|
out vec3 FragPos;
|
||||||
|
out vec3 Normal;
|
||||||
|
out float Height;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
FragPos = vec3(model * vec4(aPos, 1.0));
|
||||||
|
Normal = mat3(transpose(inverse(model))) * aNormal;
|
||||||
|
Height = aPos.y;
|
||||||
|
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||||
|
}
|
||||||
74
src/shaders/water.frag
Normal file
74
src/shaders/water.frag
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
#version 330 core
|
||||||
|
|
||||||
|
in vec3 FragPos;
|
||||||
|
in vec3 Normal;
|
||||||
|
in vec2 TexCoord;
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
uniform vec3 viewPos;
|
||||||
|
uniform vec3 lightDir;
|
||||||
|
uniform vec3 lightColor;
|
||||||
|
uniform vec3 waterDeep;
|
||||||
|
uniform vec3 waterShallow;
|
||||||
|
uniform vec3 skyColor;
|
||||||
|
uniform sampler2D depthTex;
|
||||||
|
uniform float maxWaterDepth;
|
||||||
|
uniform vec3 fogColor;
|
||||||
|
uniform float fogDensity;
|
||||||
|
uniform sampler2D oceanMask;
|
||||||
|
uniform float terrainScale;
|
||||||
|
|
||||||
|
float linearize(float ndc)
|
||||||
|
{
|
||||||
|
float near = 0.1;
|
||||||
|
float far = 500.0;
|
||||||
|
return (2.0 * near * far) / (far + near - (ndc * 2.0 - 1.0) * (far - near));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 maskUV = FragPos.xz / terrainScale + 0.5;
|
||||||
|
float oceanFactor;
|
||||||
|
if (maskUV.x < 0.0 || maskUV.x > 1.0 || maskUV.y < 0.0 || maskUV.y > 1.0) {
|
||||||
|
oceanFactor = 1.0;
|
||||||
|
} else {
|
||||||
|
oceanFactor = texture(oceanMask, maskUV).r;
|
||||||
|
}
|
||||||
|
if (oceanFactor < 0.5)
|
||||||
|
discard;
|
||||||
|
|
||||||
|
vec3 norm = normalize(Normal);
|
||||||
|
vec3 viewDir = normalize(viewPos - FragPos);
|
||||||
|
|
||||||
|
float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0);
|
||||||
|
|
||||||
|
vec3 reflected = skyColor * 1.3;
|
||||||
|
|
||||||
|
vec3 L = normalize(lightDir);
|
||||||
|
vec3 H = normalize(L + viewDir);
|
||||||
|
float spec = pow(max(dot(norm, H), 0.0), 256.0);
|
||||||
|
vec3 sun = spec * lightColor * 1.0;
|
||||||
|
|
||||||
|
float upness = max(dot(norm, vec3(0.0, 1.0, 0.0)), 0.0);
|
||||||
|
vec3 base = mix(waterShallow, waterDeep, upness);
|
||||||
|
|
||||||
|
vec3 color = mix(base, reflected, fresnel * 0.85);
|
||||||
|
color += sun;
|
||||||
|
|
||||||
|
vec2 screenUV = gl_FragCoord.xy / textureSize(depthTex, 0);
|
||||||
|
float terrainD = texture(depthTex, screenUV).r;
|
||||||
|
float waterD = gl_FragCoord.z;
|
||||||
|
|
||||||
|
float tLinear = linearize(terrainD);
|
||||||
|
float wLinear = linearize(waterD);
|
||||||
|
float column = tLinear - wLinear;
|
||||||
|
float alpha = clamp(column / maxWaterDepth, 0.15, 0.9);
|
||||||
|
|
||||||
|
float dist = length(viewPos - FragPos);
|
||||||
|
float fog = 1.0 - exp(-dist * fogDensity);
|
||||||
|
fog = clamp(fog, 0.0, 1.0);
|
||||||
|
color = mix(color, fogColor, fog);
|
||||||
|
|
||||||
|
FragColor = vec4(color, alpha);
|
||||||
|
}
|
||||||
60
src/shaders/water.vert
Normal file
60
src/shaders/water.vert
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#version 330 core
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 aPos;
|
||||||
|
layout (location = 1) in vec3 aNormal;
|
||||||
|
layout (location = 2) in vec2 aTexCoord;
|
||||||
|
|
||||||
|
uniform mat4 model;
|
||||||
|
uniform mat4 view;
|
||||||
|
uniform mat4 projection;
|
||||||
|
uniform float time;
|
||||||
|
|
||||||
|
out vec3 FragPos;
|
||||||
|
out vec3 Normal;
|
||||||
|
out vec2 TexCoord;
|
||||||
|
|
||||||
|
const float PI = 3.14159265359;
|
||||||
|
|
||||||
|
vec3 gerstner(vec2 dir, float freq, float amp, float steep, float phase, vec3 pos, inout vec3 nor)
|
||||||
|
{
|
||||||
|
float w = freq * 2.0 * PI;
|
||||||
|
float Q = steep / (w * amp * 2.5 + 0.01);
|
||||||
|
float WA = w * amp;
|
||||||
|
float fi = w * dot(dir, pos.xz) + time * phase;
|
||||||
|
float C = cos(fi);
|
||||||
|
float S = sin(fi);
|
||||||
|
|
||||||
|
pos.x += Q * amp * dir.x * C;
|
||||||
|
pos.y += amp * S;
|
||||||
|
pos.z += Q * amp * dir.y * C;
|
||||||
|
|
||||||
|
nor.x -= dir.x * WA * C;
|
||||||
|
nor.y += Q * WA * S;
|
||||||
|
nor.z -= dir.y * WA * C;
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec3 pos = aPos;
|
||||||
|
vec3 nor = aNormal;
|
||||||
|
|
||||||
|
// Large slow swell
|
||||||
|
pos = gerstner(normalize(vec2( 0.7, 0.7)), 0.04, 2.0, 0.35, 0.5, pos, nor);
|
||||||
|
// Medium swell crossing
|
||||||
|
pos = gerstner(normalize(vec2(-0.6, 0.8)), 0.08, 1.1, 0.40, 1.2, pos, nor);
|
||||||
|
// Medium swell opposite
|
||||||
|
pos = gerstner(normalize(vec2( 0.9, -0.4)), 0.10, 0.7, 0.35, 0.9, pos, nor);
|
||||||
|
// Choppy surface
|
||||||
|
pos = gerstner(normalize(vec2(-0.3, -0.95)), 0.18, 0.3, 0.55, 1.8, pos, nor);
|
||||||
|
// Fine ripples
|
||||||
|
pos = gerstner(normalize(vec2( 0.5, -0.85)), 0.28, 0.08, 0.35, 2.5, pos, nor);
|
||||||
|
|
||||||
|
nor = normalize(nor);
|
||||||
|
|
||||||
|
FragPos = vec3(model * vec4(pos, 1.0));
|
||||||
|
Normal = mat3(transpose(inverse(model))) * nor;
|
||||||
|
TexCoord = aTexCoord;
|
||||||
|
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||||
|
}
|
||||||
212
src/terrain.rs
Normal file
212
src/terrain.rs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
use noise::{NoiseFn, Perlin, Fbm};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
pub struct TerrainMesh {
|
||||||
|
pub vertices: Vec<f32>,
|
||||||
|
pub indices: Vec<u32>,
|
||||||
|
pub index_count: i32,
|
||||||
|
pub min_height: f32,
|
||||||
|
pub max_height: f32,
|
||||||
|
pub heightmap: Vec<Vec<f32>>,
|
||||||
|
pub grid_resolution: usize,
|
||||||
|
pub world_scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_terrain(
|
||||||
|
grid_resolution: usize,
|
||||||
|
world_scale: f32,
|
||||||
|
noise_freq: f64,
|
||||||
|
height_multiplier: f32,
|
||||||
|
seed: u32,
|
||||||
|
) -> TerrainMesh {
|
||||||
|
let fbm = Fbm::<Perlin>::new(seed);
|
||||||
|
let vertices_per_row = grid_resolution + 1;
|
||||||
|
let total_vertices = vertices_per_row * vertices_per_row;
|
||||||
|
let half = grid_resolution as f32 * 0.5;
|
||||||
|
let cell_size = world_scale / grid_resolution as f32;
|
||||||
|
|
||||||
|
let mut heightmap = vec![vec![0.0f32; vertices_per_row]; vertices_per_row];
|
||||||
|
let mut min_h = f32::MAX;
|
||||||
|
let mut max_h = f32::MIN;
|
||||||
|
|
||||||
|
for j in 0..vertices_per_row {
|
||||||
|
for i in 0..vertices_per_row {
|
||||||
|
let x = i as f64 * noise_freq;
|
||||||
|
let z = j as f64 * noise_freq;
|
||||||
|
let h = fbm.get([x, z]) as f32 * height_multiplier;
|
||||||
|
heightmap[j][i] = h;
|
||||||
|
if h < min_h {
|
||||||
|
min_h = h;
|
||||||
|
}
|
||||||
|
if h > max_h {
|
||||||
|
max_h = h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vertices: Vec<f32> = Vec::with_capacity(total_vertices * 8);
|
||||||
|
|
||||||
|
for j in 0..vertices_per_row {
|
||||||
|
for i in 0..vertices_per_row {
|
||||||
|
let px = (i as f32 - half) * cell_size;
|
||||||
|
let pz = (j as f32 - half) * cell_size;
|
||||||
|
let py = heightmap[j][i];
|
||||||
|
|
||||||
|
let h_left = if i > 0 {
|
||||||
|
heightmap[j][i - 1]
|
||||||
|
} else {
|
||||||
|
py
|
||||||
|
};
|
||||||
|
let h_right = if i < grid_resolution {
|
||||||
|
heightmap[j][i + 1]
|
||||||
|
} else {
|
||||||
|
py
|
||||||
|
};
|
||||||
|
let h_down = if j > 0 {
|
||||||
|
heightmap[j - 1][i]
|
||||||
|
} else {
|
||||||
|
py
|
||||||
|
};
|
||||||
|
let h_up = if j < grid_resolution {
|
||||||
|
heightmap[j + 1][i]
|
||||||
|
} else {
|
||||||
|
py
|
||||||
|
};
|
||||||
|
|
||||||
|
let tangent_x =
|
||||||
|
glam::Vec3::new(2.0 * cell_size, h_right - h_left, 0.0).normalize();
|
||||||
|
let tangent_z =
|
||||||
|
glam::Vec3::new(0.0, h_up - h_down, 2.0 * cell_size).normalize();
|
||||||
|
let normal = tangent_z.cross(tangent_x).normalize();
|
||||||
|
|
||||||
|
let u = i as f32 / grid_resolution as f32;
|
||||||
|
let v = j as f32 / grid_resolution as f32;
|
||||||
|
|
||||||
|
vertices.push(px);
|
||||||
|
vertices.push(py);
|
||||||
|
vertices.push(pz);
|
||||||
|
vertices.push(normal.x);
|
||||||
|
vertices.push(normal.y);
|
||||||
|
vertices.push(normal.z);
|
||||||
|
vertices.push(u);
|
||||||
|
vertices.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut indices: Vec<u32> = Vec::with_capacity(grid_resolution * grid_resolution * 6);
|
||||||
|
let vpr = vertices_per_row as u32;
|
||||||
|
|
||||||
|
for j in 0..grid_resolution as u32 {
|
||||||
|
for i in 0..grid_resolution as u32 {
|
||||||
|
let a = i + j * vpr;
|
||||||
|
let b = i + 1 + j * vpr;
|
||||||
|
let c = i + (j + 1) * vpr;
|
||||||
|
let d = i + 1 + (j + 1) * vpr;
|
||||||
|
|
||||||
|
indices.push(a);
|
||||||
|
indices.push(c);
|
||||||
|
indices.push(b);
|
||||||
|
|
||||||
|
indices.push(b);
|
||||||
|
indices.push(c);
|
||||||
|
indices.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let index_count = indices.len() as i32;
|
||||||
|
|
||||||
|
TerrainMesh {
|
||||||
|
vertices,
|
||||||
|
indices,
|
||||||
|
index_count,
|
||||||
|
min_height: min_h,
|
||||||
|
max_height: max_h,
|
||||||
|
heightmap,
|
||||||
|
grid_resolution,
|
||||||
|
world_scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_ocean_mask(
|
||||||
|
heightmap: &[Vec<f32>],
|
||||||
|
sea_level: f32,
|
||||||
|
grid_resolution: usize,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let size = grid_resolution + 1;
|
||||||
|
let mut visited = vec![false; size * size];
|
||||||
|
let mut queue = VecDeque::new();
|
||||||
|
|
||||||
|
for i in 0..size {
|
||||||
|
queue.push_back((i, 0));
|
||||||
|
queue.push_back((i, size - 1));
|
||||||
|
}
|
||||||
|
for j in 1..size - 1 {
|
||||||
|
queue.push_back((0, j));
|
||||||
|
queue.push_back((size - 1, j));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some((i, j)) = queue.pop_front() {
|
||||||
|
if visited[j * size + i] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if heightmap[j][i] >= sea_level {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[j * size + i] = true;
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
queue.push_back((i - 1, j));
|
||||||
|
}
|
||||||
|
if i < size - 1 {
|
||||||
|
queue.push_back((i + 1, j));
|
||||||
|
}
|
||||||
|
if j > 0 {
|
||||||
|
queue.push_back((i, j - 1));
|
||||||
|
}
|
||||||
|
if j < size - 1 {
|
||||||
|
queue.push_back((i, j + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dilated = dilate_mask(&visited, size);
|
||||||
|
|
||||||
|
let mut mask = vec![0u8; size * size * 4];
|
||||||
|
for j in 0..size {
|
||||||
|
for i in 0..size {
|
||||||
|
let v = if dilated[j * size + i] { 255u8 } else { 0u8 };
|
||||||
|
let idx = (j * size + i) * 4;
|
||||||
|
mask[idx] = v;
|
||||||
|
mask[idx + 1] = v;
|
||||||
|
mask[idx + 2] = v;
|
||||||
|
mask[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mask
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dilate_mask(visited: &[bool], size: usize) -> Vec<bool> {
|
||||||
|
let mut result = visited.to_vec();
|
||||||
|
for j in 0..size {
|
||||||
|
for i in 0..size {
|
||||||
|
if visited[j * size + i] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut has_neighbor = false;
|
||||||
|
for (di, dj) in &[(-1, 0), (1, 0), (0, -1), (0, 1)] {
|
||||||
|
let ni = i as isize + di;
|
||||||
|
let nj = j as isize + dj;
|
||||||
|
if ni >= 0 && nj >= 0 && ni < size as isize && nj < size as isize {
|
||||||
|
if visited[nj as usize * size + ni as usize] {
|
||||||
|
has_neighbor = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_neighbor {
|
||||||
|
result[j * size + i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
66
src/water.rs
Normal file
66
src/water.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
pub struct WaterMesh {
|
||||||
|
pub vertices: Vec<f32>,
|
||||||
|
pub indices: Vec<u32>,
|
||||||
|
pub index_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_water(
|
||||||
|
terrain_scale: f32,
|
||||||
|
terrain_min_h: f32,
|
||||||
|
terrain_max_h: f32,
|
||||||
|
grid_resolution: usize,
|
||||||
|
) -> WaterMesh {
|
||||||
|
let sea_level = terrain_min_h + (terrain_max_h - terrain_min_h) * 0.40;
|
||||||
|
let water_scale = terrain_scale * 2.0;
|
||||||
|
let vertices_per_row = grid_resolution + 1;
|
||||||
|
let total_vertices = vertices_per_row * vertices_per_row;
|
||||||
|
let half = grid_resolution as f32 * 0.5;
|
||||||
|
let cell_size = water_scale / grid_resolution as f32;
|
||||||
|
|
||||||
|
let mut vertices: Vec<f32> = Vec::with_capacity(total_vertices * 8);
|
||||||
|
|
||||||
|
for j in 0..vertices_per_row {
|
||||||
|
for i in 0..vertices_per_row {
|
||||||
|
let px = (i as f32 - half) * cell_size;
|
||||||
|
let pz = (j as f32 - half) * cell_size;
|
||||||
|
let py = sea_level;
|
||||||
|
|
||||||
|
vertices.push(px);
|
||||||
|
vertices.push(py);
|
||||||
|
vertices.push(pz);
|
||||||
|
vertices.push(0.0);
|
||||||
|
vertices.push(1.0);
|
||||||
|
vertices.push(0.0);
|
||||||
|
vertices.push(i as f32 / grid_resolution as f32);
|
||||||
|
vertices.push(j as f32 / grid_resolution as f32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut indices: Vec<u32> = Vec::with_capacity(grid_resolution * grid_resolution * 6);
|
||||||
|
let vpr = vertices_per_row as u32;
|
||||||
|
|
||||||
|
for j in 0..grid_resolution as u32 {
|
||||||
|
for i in 0..grid_resolution as u32 {
|
||||||
|
let a = i + j * vpr;
|
||||||
|
let b = i + 1 + j * vpr;
|
||||||
|
let c = i + (j + 1) * vpr;
|
||||||
|
let d = i + 1 + (j + 1) * vpr;
|
||||||
|
|
||||||
|
indices.push(a);
|
||||||
|
indices.push(c);
|
||||||
|
indices.push(b);
|
||||||
|
|
||||||
|
indices.push(b);
|
||||||
|
indices.push(c);
|
||||||
|
indices.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let index_count = indices.len() as i32;
|
||||||
|
|
||||||
|
WaterMesh {
|
||||||
|
vertices,
|
||||||
|
indices,
|
||||||
|
index_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user