//! **Minerales** embebidos en la roca del terreno: vetas, pepitas, cristales, //! estratos y geodas. A diferencia de la [`Forma`](crate::Forma) de flora (que se //! posa *sobre* el suelo), un mineral **reemplaza** vóxels sólidos bajo la piel del //! terreno, así que se lee en las paredes de roca expuestas (acantilados, cortes). //! //! Cada [`Mineral`] trae **tipo, color y dureza Mohs** intrínsecos; el [`Aspecto`] //! decide cómo se distribuye en la roca, y [`MineralUso`] lo instancia en un bioma //! con su abundancia y banda de profundidad. La dureza es visible: los blandos //! salen terrosos/granulados, los duros cristalinos y las gemas (Mohs ≥ 7) //! destellan. Todo es función pura de la posición de mundo → seamless en streaming. use serde::{Deserialize, Serialize}; use crate::terrain::{hash2, smooth}; /// **Cómo se distribuye** un mineral dentro de la roca. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Aspecto { /// Vetas: láminas finas diagonales que cruzan la roca. Veta, /// Pepitas: bolsones sólidos aislados. Pepita, /// Diseminado: motas sueltas de un vóxel salpicadas. Diseminado, /// Estrato: capa que sigue el relieve, rota lateralmente. Estrato, /// Geoda: bolsones redondeados (bolsas cristalinas). Geoda, /// Cristal: racimos con sesgo vertical (cristales que crecen hacia arriba). Cristal, } impl Aspecto { /// Todos los aspectos (para ciclar en un editor). pub const ALL: [Aspecto; 6] = [ Aspecto::Veta, Aspecto::Pepita, Aspecto::Diseminado, Aspecto::Estrato, Aspecto::Geoda, Aspecto::Cristal, ]; /// Nombre legible (español). pub fn label(self) -> &'static str { match self { Aspecto::Veta => "veta", Aspecto::Pepita => "pepita", Aspecto::Diseminado => "diseminado", Aspecto::Estrato => "estrato", Aspecto::Geoda => "geoda", Aspecto::Cristal => "cristal", } } /// El aspecto siguiente (cicla). pub fn next(self) -> Aspecto { let i = Aspecto::ALL.iter().position(|&a| a == self).unwrap_or(0); Aspecto::ALL[(i + 1) % Aspecto::ALL.len()] } } /// **Catálogo de minerales** semilla: tipo con color y dureza Mohs de fábrica, más /// el aspecto y la banda de profundidad típicos. Ordenado de blando/superficial a /// duro/profundo. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mineral { /// Carbón — negro mate, blando, en estratos poco profundos. Carbon, /// Sal (halita) — blanco rosado, blando, en estratos. Sal, /// Cobre (malaquita) — verde azulado, en vetas. Cobre, /// Hierro (hematita) — pardo rojizo, en vetas. Hierro, /// Plata — gris claro metálico, en vetas. Plata, /// Oro — dorado, en pepitas, profundo. Oro, /// Lapislázuli — azul intenso, en estratos. Lapis, /// Cuarzo — blanco translúcido, duro, en vetas. Cuarzo, /// Amatista — violeta, dura, en geodas. Amatista, /// Esmeralda — verde vivo, dura, en cristales. Esmeralda, /// Rubí — rojo, muy duro, en cristales profundos. Rubi, /// Diamante — blanco cian, el más duro, en pepitas muy profundas. Diamante, // --- Exóticos --- /// Obsidiana — vidrio volcánico negro y lustroso. Obsidiana, /// Pirita — «oro de los tontos», latón metálico en cubos. Pirita, /// Zafiro — azul profundo, casi tan duro como el diamante. Zafiro, /// Topacio — gema dorada translúcida. Topacio, /// Turquesa — celeste opaco en vetas. Turquesa, /// Ópalo — nácar pálido iridiscente, en geodas. Opalo, /// Ámbar — resina fósil anaranjada, cerca de la superficie. Ambar, /// Fluorita — cristal violeta translúcido. Fluorita, /// Jade — verde sedoso en vetas. Jade, /// Cinabrio — bermellón intenso (mena de mercurio). Cinabrio, /// Bismuto — iridiscente rosa-violeta escalonado, profundo. Bismuto, /// Uraninita — verde radiactivo, rara y muy profunda. Uraninita, } impl Mineral { /// Color RGB de fábrica. pub fn color(self) -> [u8; 3] { match self { Mineral::Carbon => [34, 34, 40], Mineral::Sal => [232, 220, 224], Mineral::Cobre => [70, 176, 150], Mineral::Hierro => [150, 96, 74], Mineral::Plata => [200, 206, 214], Mineral::Oro => [232, 192, 84], Mineral::Lapis => [42, 72, 182], Mineral::Cuarzo => [224, 226, 236], Mineral::Amatista => [150, 92, 206], Mineral::Esmeralda => [40, 182, 112], Mineral::Rubi => [200, 44, 64], Mineral::Diamante => [196, 232, 240], Mineral::Obsidiana => [26, 24, 32], Mineral::Pirita => [198, 168, 84], Mineral::Zafiro => [40, 78, 190], Mineral::Topacio => [222, 182, 92], Mineral::Turquesa => [64, 190, 190], Mineral::Opalo => [224, 214, 228], Mineral::Ambar => [216, 138, 44], Mineral::Fluorita => [150, 96, 210], Mineral::Jade => [72, 168, 116], Mineral::Cinabrio => [190, 40, 44], Mineral::Bismuto => [186, 132, 196], Mineral::Uraninita => [124, 202, 62], } } /// **Dureza Mohs** `[1,10]`. pub fn dureza(self) -> f32 { match self { Mineral::Carbon => 1.5, Mineral::Sal => 2.5, Mineral::Cobre => 3.5, Mineral::Plata => 2.7, Mineral::Oro => 2.7, Mineral::Hierro => 5.5, Mineral::Lapis => 5.5, Mineral::Cuarzo => 7.0, Mineral::Amatista => 7.0, Mineral::Esmeralda => 7.5, Mineral::Rubi => 9.0, Mineral::Diamante => 10.0, Mineral::Cinabrio => 2.2, Mineral::Ambar => 2.5, Mineral::Bismuto => 2.5, Mineral::Fluorita => 4.0, Mineral::Turquesa => 5.5, Mineral::Obsidiana => 5.5, Mineral::Uraninita => 6.0, Mineral::Opalo => 6.0, Mineral::Pirita => 6.2, Mineral::Jade => 6.5, Mineral::Topacio => 8.0, Mineral::Zafiro => 9.0, } } /// Aspecto típico de este mineral. pub fn aspecto(self) -> Aspecto { match self { Mineral::Carbon | Mineral::Sal | Mineral::Lapis => Aspecto::Estrato, Mineral::Cobre | Mineral::Hierro | Mineral::Plata | Mineral::Cuarzo => Aspecto::Veta, Mineral::Oro | Mineral::Diamante => Aspecto::Pepita, Mineral::Amatista => Aspecto::Geoda, Mineral::Esmeralda | Mineral::Rubi => Aspecto::Cristal, Mineral::Obsidiana | Mineral::Turquesa | Mineral::Jade | Mineral::Cinabrio => Aspecto::Veta, Mineral::Pirita | Mineral::Ambar | Mineral::Uraninita => Aspecto::Pepita, Mineral::Zafiro | Mineral::Topacio | Mineral::Fluorita => Aspecto::Cristal, Mineral::Opalo | Mineral::Bismuto => Aspecto::Geoda, } } /// Banda de profundidad típica `(min, max)` como fracción bajo la superficie /// `[0,1]` (0 = superficie, 1 = fondo del mundo). pub fn profundidad(self) -> (f32, f32) { match self { Mineral::Carbon => (0.04, 0.30), Mineral::Sal => (0.10, 0.45), Mineral::Cobre => (0.12, 0.70), Mineral::Hierro => (0.12, 0.85), Mineral::Plata => (0.30, 0.85), Mineral::Lapis => (0.40, 0.85), Mineral::Oro => (0.45, 0.95), Mineral::Cuarzo => (0.10, 0.80), Mineral::Amatista => (0.35, 0.85), Mineral::Esmeralda => (0.50, 0.95), Mineral::Rubi => (0.55, 0.95), Mineral::Diamante => (0.70, 1.00), Mineral::Ambar => (0.02, 0.25), Mineral::Turquesa => (0.10, 0.50), Mineral::Cinabrio => (0.15, 0.55), Mineral::Obsidiana => (0.10, 0.60), Mineral::Jade => (0.20, 0.70), Mineral::Pirita => (0.20, 0.72), Mineral::Opalo => (0.30, 0.80), Mineral::Fluorita => (0.30, 0.80), Mineral::Topacio => (0.45, 0.90), Mineral::Zafiro => (0.55, 0.95), Mineral::Bismuto => (0.60, 0.95), Mineral::Uraninita => (0.72, 1.00), } } /// **Brillo/reflectividad** `[0,1]` de fábrica: cuánto lustre especular y /// glaseado devuelve al sol. Terrosos (carbón, sal) ~0; metales (plata, oro) /// medio-alto; gemas cristalinas (cuarzo→diamante) alto. Lo aprovecha el /// ray-march (a diferencia de la luz plana de Minecraft) como reflejo /// dependiente de la vista. pub fn brillo(self) -> f32 { match self { Mineral::Carbon => 0.05, Mineral::Sal => 0.15, Mineral::Hierro => 0.25, Mineral::Lapis => 0.22, Mineral::Cobre => 0.4, Mineral::Plata => 0.72, Mineral::Oro => 0.78, Mineral::Cuarzo => 0.82, Mineral::Amatista => 0.85, Mineral::Esmeralda => 0.9, Mineral::Rubi => 0.95, Mineral::Diamante => 1.0, Mineral::Cinabrio => 0.5, Mineral::Turquesa => 0.52, Mineral::Jade => 0.6, Mineral::Uraninita => 0.6, Mineral::Ambar => 0.68, Mineral::Fluorita => 0.78, Mineral::Pirita => 0.85, Mineral::Opalo => 0.88, Mineral::Obsidiana => 0.9, Mineral::Topacio => 0.9, Mineral::Bismuto => 0.92, Mineral::Zafiro => 0.95, } } /// Abundancia típica `[0,1]` (los raros/preciosos, más bajos). pub fn abundancia(self) -> f32 { match self { Mineral::Carbon | Mineral::Hierro | Mineral::Cuarzo => 0.5, Mineral::Sal | Mineral::Cobre => 0.4, Mineral::Plata | Mineral::Lapis | Mineral::Amatista => 0.3, Mineral::Oro => 0.25, Mineral::Esmeralda => 0.18, Mineral::Rubi => 0.16, Mineral::Diamante => 0.12, Mineral::Obsidiana => 0.35, Mineral::Pirita | Mineral::Turquesa | Mineral::Jade | Mineral::Fluorita => 0.22, Mineral::Cinabrio => 0.20, Mineral::Ambar => 0.18, Mineral::Opalo | Mineral::Topacio => 0.16, Mineral::Zafiro => 0.14, Mineral::Bismuto => 0.12, Mineral::Uraninita => 0.10, } } /// Todos los minerales (catálogo, para ciclar en un editor). pub const ALL: [Mineral; 24] = [ Mineral::Carbon, Mineral::Sal, Mineral::Cobre, Mineral::Hierro, Mineral::Plata, Mineral::Oro, Mineral::Lapis, Mineral::Cuarzo, Mineral::Amatista, Mineral::Esmeralda, Mineral::Rubi, Mineral::Diamante, Mineral::Obsidiana, Mineral::Pirita, Mineral::Zafiro, Mineral::Topacio, Mineral::Turquesa, Mineral::Opalo, Mineral::Ambar, Mineral::Fluorita, Mineral::Jade, Mineral::Cinabrio, Mineral::Bismuto, Mineral::Uraninita, ]; /// Nombre legible (español). pub fn label(self) -> &'static str { match self { Mineral::Carbon => "carbón", Mineral::Sal => "sal", Mineral::Cobre => "cobre", Mineral::Hierro => "hierro", Mineral::Plata => "plata", Mineral::Oro => "oro", Mineral::Lapis => "lapislázuli", Mineral::Cuarzo => "cuarzo", Mineral::Amatista => "amatista", Mineral::Esmeralda => "esmeralda", Mineral::Rubi => "rubí", Mineral::Diamante => "diamante", Mineral::Obsidiana => "obsidiana", Mineral::Pirita => "pirita", Mineral::Zafiro => "zafiro", Mineral::Topacio => "topacio", Mineral::Turquesa => "turquesa", Mineral::Opalo => "ópalo", Mineral::Ambar => "ámbar", Mineral::Fluorita => "fluorita", Mineral::Jade => "jade", Mineral::Cinabrio => "cinabrio", Mineral::Bismuto => "bismuto", Mineral::Uraninita => "uraninita", } } /// El mineral siguiente (cicla). pub fn next(self) -> Mineral { let i = Mineral::ALL.iter().position(|&m| m == self).unwrap_or(0); Mineral::ALL[(i + 1) % Mineral::ALL.len()] } } /// **Uso de un mineral en un bioma**, con **todos sus factores editables**: el tipo /// semilla (nombre) + cómo se distribuye ([`Aspecto`]), su abundancia `[0,1]`, la /// banda de profundidad `(min, max)`, y los factores autorables **color**, **dureza** /// y **brillo** — que arrancan de los de fábrica del mineral pero se pueden ajustar /// en la UI del studio sin tocar el catálogo. `mineral_at` usa estos campos /// (no el enum), así que editar cualquiera cambia el render. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct MineralUso { pub mineral: Mineral, pub aspecto: Aspecto, pub abundancia: f32, pub prof_min: f32, pub prof_max: f32, /// Color efectivo (default = [`Mineral::color`]). pub color: [u8; 3], /// Dureza Mohs efectiva (default = [`Mineral::dureza`]); afecta el grano. pub dureza: f32, /// Brillo/reflectividad efectivo (default = [`Mineral::brillo`]); afecta el lustre. pub brillo: f32, } impl MineralUso { /// Instancia con **todos** los valores típicos del propio mineral. pub fn of(mineral: Mineral) -> Self { let (prof_min, prof_max) = mineral.profundidad(); Self { mineral, aspecto: mineral.aspecto(), abundancia: mineral.abundancia(), prof_min, prof_max, color: mineral.color(), dureza: mineral.dureza(), brillo: mineral.brillo(), } } /// Igual que [`of`](Self::of) pero forzando la abundancia (útil para el demo o /// para un bioma más rico/pobre en un mineral). pub fn with_abundancia(mineral: Mineral, abundancia: f32) -> Self { Self { abundancia, ..Self::of(mineral) } } /// Reinicia los factores autorables (color/dureza/brillo/aspecto/profundidad) a /// los de fábrica de `mineral`, conservando la abundancia. Lo usa la UI al /// cambiar el tipo semilla. pub fn reset_to(&mut self, mineral: Mineral) { let ab = self.abundancia; *self = Self::of(mineral); self.abundancia = ab; } } // ============================================================================= // Distribución espacial (ruido de mundo) // ============================================================================= /// Hash 3D determinista `[0,1)` (combina [`hash2`] sobre las tres coordenadas). #[inline] fn hash3(x: i32, y: i32, z: i32, seed: u32) -> f32 { let a = x.wrapping_mul(0x1B).wrapping_add(z); let b = y.wrapping_mul(0x2F).wrapping_sub(z.wrapping_mul(0x11)); hash2(a, b, seed) } /// Ruido de valor 3D suave `[0,1]` (interpolación trilineal de la retícula hash). fn value3(fx: f32, fy: f32, fz: f32, seed: u32) -> f32 { let (xi, yi, zi) = (fx.floor() as i32, fy.floor() as i32, fz.floor() as i32); let (tx, ty, tz) = (smooth(fx - fx.floor()), smooth(fy - fy.floor()), smooth(fz - fz.floor())); let c = |dx, dy, dz| hash3(xi + dx, yi + dy, zi + dz, seed); let l = |a: f32, b: f32, t: f32| a + (b - a) * t; let x00 = l(c(0, 0, 0), c(1, 0, 0), tx); let x10 = l(c(0, 1, 0), c(1, 1, 0), tx); let x01 = l(c(0, 0, 1), c(1, 0, 1), tx); let x11 = l(c(0, 1, 1), c(1, 1, 1), tx); l(l(x00, x10, ty), l(x01, x11, ty), tz) } /// `true` si el vóxel de mundo `(wx,y,wz)` cae dentro de un depósito de `aspecto` /// con abundancia `ab`. Puramente espacial (la banda de profundidad se filtra fuera). pub(crate) fn aspecto_hit(aspecto: Aspecto, wx: i32, y: u32, wz: i32, seed: u32, ab: f32) -> bool { let (fx, fy, fz) = (wx as f32, y as f32, wz as f32); match aspecto { Aspecto::Diseminado => hash3(wx, y as i32, wz, seed) < ab * 0.35, Aspecto::Veta => { let n = value3(fx * 0.22, fy * 0.22, fz * 0.22, seed); (n - 0.5).abs() < 0.03 + ab * 0.05 } Aspecto::Pepita => { let n = value3(fx * 0.16, fy * 0.16, fz * 0.16, seed ^ 0x55); n > 1.0 - (0.06 + ab * 0.14) } Aspecto::Cristal => { let n = value3(fx * 0.2, fy * 0.12, fz * 0.2, seed ^ 0xC7); n > 1.0 - (0.05 + ab * 0.12) } Aspecto::Estrato => { let n = value3(fx * 0.08, fy * 0.5, fz * 0.08, seed ^ 0x09); n < 0.55 + ab * 0.4 } Aspecto::Geoda => { let n = value3(fx * 0.13, fy * 0.13, fz * 0.13, seed ^ 0x6A); n > 1.0 - (0.05 + ab * 0.10) } } } /// Colorea un vóxel de mineral aplicando la **dureza** como textura: blandos → /// terrosos/granulados; duros → lisos; gemas (Mohs ≥ 7) → destello en algunos vóxels. pub(crate) fn mineral_shade(base: [u8; 3], dureza: f32, wx: i32, y: u32, wz: i32, seed: u32) -> [u8; 3] { let d = (dureza / 10.0).clamp(0.0, 1.0); // Grano decreciente con la dureza. let grain = 0.45 * (1.0 - d); let h = hash2(wx.wrapping_mul(9).wrapping_add(y as i32 * 7), wz, seed ^ 0x6817); let bright = 1.0 + (h - 0.5) * 2.0 * grain * 0.3; let mut c = [ (base[0] as f32 * bright).clamp(0.0, 255.0) as u8, (base[1] as f32 * bright).clamp(0.0, 255.0) as u8, (base[2] as f32 * bright).clamp(0.0, 255.0) as u8, ]; if dureza >= 7.0 { // Destello de gema. let s = hash2(wx.wrapping_mul(5).wrapping_add(y as i32), wz, seed ^ 0x6E3); if s > 0.72 { let b = 1.28; c = [ (c[0] as f32 * b).min(255.0) as u8, (c[1] as f32 * b).min(255.0) as u8, (c[2] as f32 * b).min(255.0) as u8, ]; } } c } #[cfg(test)] mod tests { use super::*; #[test] fn catalogo_coherente() { // Dureza en rango Mohs y duro ⇒ menos abundante que el más común. for m in Mineral::ALL { let d = m.dureza(); assert!((1.0..=10.0).contains(&d), "{} dureza fuera de Mohs", m.label()); let (a, b) = m.profundidad(); assert!(a >= 0.0 && b <= 1.0 && a < b, "{} banda inválida", m.label()); } assert!(Mineral::Diamante.abundancia() < Mineral::Hierro.abundancia()); assert!(Mineral::Diamante.dureza() > Mineral::Carbon.dureza()); } #[test] fn aspecto_es_determinista_y_esparce() { let seed = 7; // Determinista. assert_eq!( aspecto_hit(Aspecto::Veta, 12, 8, -4, seed, 0.5), aspecto_hit(Aspecto::Veta, 12, 8, -4, seed, 0.5) ); // Al menos algunos hits en un volumen (no todo vacío ni todo lleno). let mut hits = 0; let mut total = 0; for wx in 0..40 { for y in 0..40u32 { for wz in 0..40 { total += 1; if aspecto_hit(Aspecto::Pepita, wx, y, wz, seed, 0.5) { hits += 1; } } } } assert!(hits > 0 && hits < total, "pepita: {hits}/{total}"); } #[test] fn brillo_ordena_de_mate_a_gema() { // Terroso mate < metal < gema; el brillo se codifica en un alpha que sigue // siendo sólido para el shader (>127). assert!(Mineral::Carbon.brillo() < Mineral::Oro.brillo()); assert!(Mineral::Oro.brillo() < Mineral::Diamante.brillo()); for m in Mineral::ALL { let b = m.brillo(); assert!((0.0..=1.0).contains(&b), "{} brillo fuera de rango", m.label()); let a = llimphi_3d::VoxelGrid::shine_to_alpha(b); assert!(a > 127, "{} alpha {a} dejaría de ser sólido", m.label()); } // El más lustroso debe dar el alpha más bajo (más brillo). assert!( llimphi_3d::VoxelGrid::shine_to_alpha(Mineral::Diamante.brillo()) < llimphi_3d::VoxelGrid::shine_to_alpha(Mineral::Carbon.brillo()) ); } #[test] fn dureza_afecta_el_grano() { // Un mineral blando varía más de color entre vóxels que uno duro (comparado // bajo el umbral de destello de gema, 7, para aislar el grano). let spread = |dureza: f32| { let mut min = 255i32; let mut max = 0i32; for k in 0..30 { let c = mineral_shade([120, 120, 120], dureza, k, 3, k * 2, 1); min = min.min(c[0] as i32); max = max.max(c[0] as i32); } max - min }; assert!(spread(1.5) > spread(6.0), "blando debería granular más que duro"); } }