]> git.lizzy.rs Git - mt_client.git/blobdiff - src/gfx/map.rs
Basic map rendering
[mt_client.git] / src / gfx / map.rs
diff --git a/src/gfx/map.rs b/src/gfx/map.rs
new file mode 100644 (file)
index 0000000..d95a4d2
--- /dev/null
@@ -0,0 +1,447 @@
+use super::{media::MediaMgr, state::State, util::MatrixUniform};
+use cgmath::{prelude::*, Matrix4, Point3, Vector3};
+use mt_net::{MapBlock, NodeDef};
+use rand::Rng;
+use std::{collections::HashMap, ops::Range};
+use wgpu::util::DeviceExt;
+
+pub struct MapRender {
+    pipeline: wgpu::RenderPipeline,
+    textures: HashMap<String, [Range<f32>; 2]>,
+    nodes: HashMap<u16, NodeDef>,
+    atlas: wgpu::BindGroup,
+    model: wgpu::BindGroupLayout,
+    blocks: HashMap<[i16; 3], BlockMesh>,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+struct Vertex {
+    pos: [f32; 3],
+    tex_coords: [f32; 2],
+}
+
+impl Vertex {
+    const ATTRIBS: [wgpu::VertexAttribute; 2] =
+        wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2];
+
+    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+        wgpu::VertexBufferLayout {
+            array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+            step_mode: wgpu::VertexStepMode::Vertex,
+            attributes: &Self::ATTRIBS,
+        }
+    }
+}
+
+struct BlockMesh {
+    vertex_buffer: wgpu::Buffer,
+    num_vertices: u32,
+    model: MatrixUniform,
+}
+
+impl MapRender {
+    pub fn render<'a>(&'a self, state: &'a State, pass: &mut wgpu::RenderPass<'a>) {
+        pass.set_pipeline(&self.pipeline);
+        pass.set_bind_group(0, &self.atlas, &[]);
+        pass.set_bind_group(1, &state.camera_uniform.bind_group, &[]);
+
+        for mesh in self.blocks.values() {
+            pass.set_bind_group(2, &mesh.model.bind_group, &[]);
+            pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
+            pass.draw(0..mesh.num_vertices, 0..1);
+        }
+    }
+
+    pub fn add_block(&mut self, state: &mut State, pos: Point3<i16>, block: Box<MapBlock>) {
+        let mut vertices = Vec::with_capacity(10000);
+        for (index, content) in block.param_0.iter().enumerate() {
+            let def = match self.nodes.get(content) {
+                Some(x) => x,
+                None => continue,
+            };
+
+            use lerp::Lerp;
+            use mt_net::DrawType;
+            use std::array::from_fn as array;
+
+            match def.draw_type {
+                DrawType::Cube => {
+                    let pos: [i16; 3] = array(|i| ((index >> (4 * i)) & 0xf) as i16);
+                    for (f, face) in CUBE.iter().enumerate() {
+                        let dir = FACE_DIR[f];
+                        let npos: [i16; 3] = array(|i| dir[i] + pos[i]);
+                        if npos.iter().all(|x| (0..16).contains(x)) {
+                            let nindex = npos[0] | (npos[1] << 4) | (npos[2] << 8);
+
+                            if let Some(ndef) = self.nodes.get(&block.param_0[nindex as usize]) {
+                                if ndef.draw_type == DrawType::Cube {
+                                    continue;
+                                }
+                            }
+                        }
+
+                        let tile = &def.tiles[f];
+                        let rect = self.textures.get(&tile.texture).unwrap();
+
+                        for vertex in face.iter() {
+                            /*println!(
+                                "{:?} {:?} {:?} {:?}",
+                                (vertex.1[0], vertex.1[1]),
+                                (rect[0].start, rect[1].start),
+                                (rect[0].end, rect[1].end),
+                                (
+                                    vertex.1[0].lerp(rect[0].start, rect[0].end),
+                                    vertex.1[1].lerp(rect[1].start, rect[1].end)
+                                )
+                            );*/
+                            vertices.push(Vertex {
+                                pos: array(|i| pos[i] as f32 - 8.5 + vertex.0[i]),
+                                tex_coords: array(|i| rect[i].start.lerp(rect[i].end, vertex.1[i])),
+                            })
+                        }
+                    }
+                }
+                DrawType::None => {}
+                _ => {
+                    // TODO
+                }
+            }
+        }
+
+        self.blocks.insert(
+            pos.into(),
+            BlockMesh {
+                vertex_buffer: state
+                    .device
+                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                        label: Some("mapblock.vertex_buffer"),
+                        contents: bytemuck::cast_slice(&vertices),
+                        usage: wgpu::BufferUsages::VERTEX,
+                    }),
+                num_vertices: vertices.len() as u32,
+                model: MatrixUniform::new(
+                    &state.device,
+                    &self.model,
+                    Matrix4::from_translation(
+                        pos.cast::<f32>().unwrap().to_vec() * 16.0 + Vector3::new(8.5, 8.5, 8.5),
+                    ),
+                    "mapblock",
+                    false,
+                ),
+            },
+        );
+    }
+
+    pub fn new(state: &mut State, media: &MediaMgr, nodes: HashMap<u16, NodeDef>) -> Self {
+        let mut rng = rand::thread_rng();
+        let mut atlas_map = HashMap::new();
+        let mut atlas_alloc = guillotiere::SimpleAtlasAllocator::new(guillotiere::size2(1, 1));
+
+        for node in nodes.values() {
+            let tiles = node
+                .tiles
+                .iter()
+                .chain(node.overlay_tiles.iter())
+                .chain(node.special_tiles.iter());
+
+            let load_texture = |texture: &str| {
+                let payload = media
+                    .get(texture)
+                    .ok_or_else(|| format!("texture not found: {texture}"))?;
+
+                image::load_from_memory(payload)
+                    .or_else(|_| {
+                        image::load_from_memory_with_format(payload, image::ImageFormat::Tga)
+                    })
+                    .map_err(|e| format!("failed to load texture {texture}: {e}"))
+                    .map(|x| image::imageops::flip_vertical(&x))
+            };
+
+            let mut make_texture = |texture: &str| {
+                texture
+                    .split('^')
+                    .map(|part| match load_texture(part) {
+                        Ok(v) => v,
+                        Err(e) => {
+                            if !texture.is_empty() && !texture.contains('[') {
+                                eprintln!("{e}");
+                            }
+
+                            let mut img = image::RgbImage::new(1, 1);
+                            rng.fill(&mut img.get_pixel_mut(0, 0).0);
+
+                            image::DynamicImage::from(img).to_rgba8()
+                        }
+                    })
+                    .reduce(|mut base, top| {
+                        image::imageops::overlay(&mut base, &top, 0, 0);
+                        base
+                    })
+                    .unwrap()
+            };
+
+            for tile in tiles {
+                atlas_map.entry(tile.texture.clone()).or_insert_with(|| {
+                    let img = make_texture(&tile.texture);
+
+                    let dimensions = img.dimensions();
+                    let size = guillotiere::size2(dimensions.0 as i32, dimensions.1 as i32);
+
+                    loop {
+                        match atlas_alloc.allocate(size) {
+                            None => {
+                                let mut atlas_size = atlas_alloc.size();
+                                atlas_size.width *= 2;
+                                atlas_size.height *= 2;
+                                atlas_alloc.grow(atlas_size);
+                            }
+                            Some(v) => return (img, v),
+                        }
+                    }
+                });
+            }
+        }
+
+        let atlas_size = atlas_alloc.size();
+        let mut atlas = image::RgbaImage::new(atlas_size.width as u32, atlas_size.height as u32);
+
+        let textures = atlas_map
+            .into_iter()
+            .map(|(name, (img, rect))| {
+                let w = atlas_size.width as f32;
+                let h = atlas_size.height as f32;
+
+                let x = (rect.min.x as f32 / w)..(rect.max.x as f32 / w);
+                let y = (rect.min.y as f32 / h)..(rect.max.y as f32 / h);
+
+                use image::GenericImage;
+                atlas
+                    .copy_from(&img, rect.min.x as u32, rect.min.y as u32)
+                    .unwrap();
+
+                (name, [x, y])
+            })
+            .collect();
+
+        let size = wgpu::Extent3d {
+            width: atlas_size.width as u32,
+            height: atlas_size.height as u32,
+            depth_or_array_layers: 1,
+        };
+
+        let atlas_texture = state.device.create_texture(&wgpu::TextureDescriptor {
+            size,
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format: wgpu::TextureFormat::Rgba8UnormSrgb,
+            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+            label: Some("tile_atlas"),
+            view_formats: &[],
+        });
+
+        state.queue.write_texture(
+            wgpu::ImageCopyTexture {
+                texture: &atlas_texture,
+                mip_level: 0,
+                origin: wgpu::Origin3d::ZERO,
+                aspect: wgpu::TextureAspect::All,
+            },
+            &atlas,
+            wgpu::ImageDataLayout {
+                offset: 0,
+                bytes_per_row: std::num::NonZeroU32::new(4 * atlas_size.width as u32),
+                rows_per_image: std::num::NonZeroU32::new(atlas_size.height as u32),
+            },
+            size,
+        );
+
+        let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+        let atlas_sampler = state.device.create_sampler(&wgpu::SamplerDescriptor {
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            // "We've got you surrounded, stop using Nearest filter"
+            // - "I hate bilinear filtering I hate bilinear filtering I hate bilinear filtering"
+            mag_filter: wgpu::FilterMode::Nearest,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Nearest,
+            ..Default::default()
+        });
+
+        let atlas_bind_group_layout =
+            state
+                .device
+                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                    entries: &[
+                        wgpu::BindGroupLayoutEntry {
+                            binding: 0,
+                            visibility: wgpu::ShaderStages::FRAGMENT,
+                            ty: wgpu::BindingType::Texture {
+                                multisampled: false,
+                                view_dimension: wgpu::TextureViewDimension::D2,
+                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                            },
+                            count: None,
+                        },
+                        wgpu::BindGroupLayoutEntry {
+                            binding: 1,
+                            visibility: wgpu::ShaderStages::FRAGMENT,
+                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                            count: None,
+                        },
+                    ],
+                    label: Some("atlas.bind_group_layout"),
+                });
+
+        let atlas_bind_group = state.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            layout: &atlas_bind_group_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&atlas_view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&atlas_sampler),
+                },
+            ],
+            label: Some("atlas.bind_group"),
+        });
+
+        let model_bind_group_layout = MatrixUniform::layout(&state.device, "mapblock");
+
+        let shader = state
+            .device
+            .create_shader_module(wgpu::include_wgsl!("../../assets/shaders/map.wgsl"));
+
+        let pipeline_layout =
+            state
+                .device
+                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                    label: None,
+                    bind_group_layouts: &[
+                        &atlas_bind_group_layout,
+                        &model_bind_group_layout,
+                        &state.camera_bind_group_layout,
+                    ],
+                    push_constant_ranges: &[],
+                });
+
+        let pipeline = state
+            .device
+            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+                label: None,
+                layout: Some(&pipeline_layout),
+                vertex: wgpu::VertexState {
+                    module: &shader,
+                    entry_point: "vs_main",
+                    buffers: &[Vertex::desc()],
+                },
+                fragment: Some(wgpu::FragmentState {
+                    module: &shader,
+                    entry_point: "fs_main",
+                    targets: &[Some(wgpu::ColorTargetState {
+                        format: state.config.format,
+                        blend: Some(wgpu::BlendState::REPLACE),
+                        write_mask: wgpu::ColorWrites::ALL,
+                    })],
+                }),
+                primitive: wgpu::PrimitiveState {
+                    topology: wgpu::PrimitiveTopology::TriangleList,
+                    strip_index_format: None,
+                    front_face: wgpu::FrontFace::Ccw,
+                    cull_mode: Some(wgpu::Face::Back),
+                    polygon_mode: wgpu::PolygonMode::Fill,
+                    unclipped_depth: false,
+                    conservative: false,
+                },
+                depth_stencil: Some(wgpu::DepthStencilState {
+                    format: wgpu::TextureFormat::Depth32Float,
+                    depth_write_enabled: true,
+                    depth_compare: wgpu::CompareFunction::Less,
+                    stencil: wgpu::StencilState::default(),
+                    bias: wgpu::DepthBiasState::default(),
+                }),
+                multisample: wgpu::MultisampleState {
+                    count: 1,
+                    mask: !0,
+                    alpha_to_coverage_enabled: false,
+                },
+                multiview: None,
+            });
+
+        Self {
+            pipeline,
+            nodes,
+            textures,
+            atlas: atlas_bind_group,
+            model: model_bind_group_layout,
+            blocks: HashMap::new(),
+        }
+    }
+}
+
+#[rustfmt::skip]
+const CUBE: [[([f32; 3], [f32; 2]); 6]; 6] = [
+       [
+               ([-0.5,  0.5, -0.5], [ 0.0,  1.0]),
+               ([ 0.5,  0.5,  0.5], [ 1.0,  0.0]),
+               ([ 0.5,  0.5, -0.5], [ 1.0,  1.0]),
+               ([ 0.5,  0.5,  0.5], [ 1.0,  0.0]),
+               ([-0.5,  0.5, -0.5], [ 0.0,  1.0]),
+               ([-0.5,  0.5,  0.5], [ 0.0,  0.0]),
+       ],
+       [
+               ([-0.5, -0.5, -0.5], [ 0.0,  1.0]),
+               ([ 0.5, -0.5, -0.5], [ 1.0,  1.0]),
+               ([ 0.5, -0.5,  0.5], [ 1.0,  0.0]),
+               ([ 0.5, -0.5,  0.5], [ 1.0,  0.0]),
+               ([-0.5, -0.5,  0.5], [ 0.0,  0.0]),
+               ([-0.5, -0.5, -0.5], [ 0.0,  1.0]),
+       ],
+       [
+               ([ 0.5,  0.5,  0.5], [ 1.0,  1.0]),
+               ([ 0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([ 0.5,  0.5, -0.5], [ 0.0,  1.0]),
+               ([ 0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([ 0.5,  0.5,  0.5], [ 1.0,  1.0]),
+               ([ 0.5, -0.5,  0.5], [ 1.0,  0.0]),
+       ],
+       [
+               ([-0.5,  0.5,  0.5], [ 1.0,  1.0]),
+               ([-0.5,  0.5, -0.5], [ 0.0,  1.0]),
+               ([-0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([-0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([-0.5, -0.5,  0.5], [ 1.0,  0.0]),
+               ([-0.5,  0.5,  0.5], [ 1.0,  1.0]),
+       ],
+       [
+               ([-0.5, -0.5,  0.5], [ 0.0,  0.0]),
+               ([ 0.5, -0.5,  0.5], [ 1.0,  0.0]),
+               ([ 0.5,  0.5,  0.5], [ 1.0,  1.0]),
+               ([ 0.5,  0.5,  0.5], [ 1.0,  1.0]),
+               ([-0.5,  0.5,  0.5], [ 0.0,  1.0]),
+               ([-0.5, -0.5,  0.5], [ 0.0,  0.0]),
+       ],
+       [
+               ([-0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([ 0.5,  0.5, -0.5], [ 1.0,  1.0]),
+               ([ 0.5, -0.5, -0.5], [ 1.0,  0.0]),
+               ([ 0.5,  0.5, -0.5], [ 1.0,  1.0]),
+               ([-0.5, -0.5, -0.5], [ 0.0,  0.0]),
+               ([-0.5,  0.5, -0.5], [ 0.0,  1.0]),
+       ],
+];
+
+#[rustfmt::skip]
+const FACE_DIR: [[i16; 3]; 6] = [
+       [ 0,  1,  0],
+       [ 0, -1,  0],
+       [ 1,  0,  0],
+       [-1,  0,  0],
+       [ 0,  0,  1],
+       [ 0,  0, -1],
+];