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