|
| 1 | +extern crate image; |
| 2 | + |
| 3 | +use crate::utils::{apply_minecraft_transparency, fast_overlay}; |
| 4 | +use image::{imageops, DynamicImage, GenericImageView, Rgba, RgbaImage}; |
| 5 | +use imageproc::geometric_transformations::{warp_into, Interpolation, Projection}; |
| 6 | + |
| 7 | +/// Hytale skin handler - currently a stub with Minecraft-like layout |
| 8 | +/// TODO: Update texture coordinates when Hytale skin format is known |
| 9 | +pub(crate) struct HytaleSkin(DynamicImage); |
| 10 | + |
| 11 | +#[allow(dead_code)] // Stub for future use when Hytale format is known |
| 12 | +#[derive(Copy, Clone, PartialEq)] |
| 13 | +pub(crate) enum HytaleSkinVersion { |
| 14 | + Standard, // Assumed standard format, update when known |
| 15 | + Invalid, |
| 16 | +} |
| 17 | + |
| 18 | +#[derive(Clone, Copy, PartialEq)] |
| 19 | +pub(crate) enum SkinModel { |
| 20 | + Slim, |
| 21 | + Regular, |
| 22 | +} |
| 23 | + |
| 24 | +#[derive(Copy, Clone, PartialEq)] |
| 25 | +pub(crate) enum Layer { |
| 26 | + Bottom, |
| 27 | + Top, |
| 28 | + Both, |
| 29 | +} |
| 30 | + |
| 31 | +#[derive(Copy, Clone, PartialEq)] |
| 32 | +pub(crate) enum BodyPart { |
| 33 | + Head, |
| 34 | + Body, |
| 35 | + ArmLeft, |
| 36 | + ArmRight, |
| 37 | + LegLeft, |
| 38 | + LegRight, |
| 39 | +} |
| 40 | + |
| 41 | +const SKEW_A: f32 = 26.0 / 45.0; |
| 42 | +const SKEW_B: f32 = SKEW_A * 2.0; |
| 43 | + |
| 44 | +pub(crate) struct RenderOptions { |
| 45 | + pub armored: bool, |
| 46 | + pub model: SkinModel, |
| 47 | +} |
| 48 | + |
| 49 | +impl HytaleSkin { |
| 50 | + #[inline] |
| 51 | + pub fn new(skin: DynamicImage) -> HytaleSkin { |
| 52 | + HytaleSkin(skin) |
| 53 | + } |
| 54 | + |
| 55 | + #[allow(dead_code)] // Stub for future use when Hytale format is known |
| 56 | + #[inline] |
| 57 | + fn version(&self) -> HytaleSkinVersion { |
| 58 | + // TODO: Update when Hytale skin format dimensions are known |
| 59 | + // For now, accept 64x64 as the standard format (placeholder) |
| 60 | + match self.0.dimensions() { |
| 61 | + (64, 64) => HytaleSkinVersion::Standard, |
| 62 | + _ => HytaleSkinVersion::Invalid, |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + /// Get a body part from the skin texture |
| 67 | + /// TODO: Update texture coordinates when Hytale skin format is known |
| 68 | + /// Currently using Minecraft-like coordinates as a placeholder |
| 69 | + pub(crate) fn get_part(&self, layer: Layer, part: BodyPart, model: SkinModel) -> DynamicImage { |
| 70 | + let arm_width = match model { |
| 71 | + SkinModel::Slim => 3, |
| 72 | + SkinModel::Regular => 4, |
| 73 | + }; |
| 74 | + |
| 75 | + match layer { |
| 76 | + Layer::Both => { |
| 77 | + let mut bottom = self.get_part(Layer::Bottom, part, model); |
| 78 | + let mut top = self.get_part(Layer::Top, part, model); |
| 79 | + apply_minecraft_transparency(&mut top); |
| 80 | + fast_overlay(&mut bottom, &top, 0, 0); |
| 81 | + bottom |
| 82 | + } |
| 83 | + // TODO: These coordinates are placeholders - update when Hytale format is known |
| 84 | + Layer::Bottom => match part { |
| 85 | + BodyPart::Head => self.0.crop_imm(8, 8, 8, 8), |
| 86 | + BodyPart::Body => self.0.crop_imm(20, 20, 8, 12), |
| 87 | + BodyPart::ArmRight => self.0.crop_imm(36, 52, arm_width, 12), |
| 88 | + BodyPart::ArmLeft => self.0.crop_imm(44, 20, arm_width, 12), |
| 89 | + BodyPart::LegRight => self.0.crop_imm(20, 52, 4, 12), |
| 90 | + BodyPart::LegLeft => self.0.crop_imm(4, 20, 4, 12), |
| 91 | + }, |
| 92 | + Layer::Top => match part { |
| 93 | + BodyPart::Head => self.0.crop_imm(40, 8, 8, 8), |
| 94 | + BodyPart::Body => self.0.crop_imm(20, 36, 8, 12), |
| 95 | + BodyPart::ArmLeft => self.0.crop_imm(52, 52, arm_width, 12), |
| 96 | + BodyPart::ArmRight => self.0.crop_imm(44, 36, arm_width, 12), |
| 97 | + BodyPart::LegLeft => self.0.crop_imm(4, 52, 4, 12), |
| 98 | + BodyPart::LegRight => self.0.crop_imm(4, 36, 4, 12), |
| 99 | + }, |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + pub(crate) fn get_cape(&self) -> DynamicImage { |
| 104 | + // TODO: Update cape coordinates when Hytale format is known |
| 105 | + self.0.crop_imm(1, 1, 10, 16) |
| 106 | + } |
| 107 | + |
| 108 | + pub(crate) fn render_body(&self, options: RenderOptions) -> DynamicImage { |
| 109 | + let layer_type = if options.armored { |
| 110 | + Layer::Both |
| 111 | + } else { |
| 112 | + Layer::Bottom |
| 113 | + }; |
| 114 | + |
| 115 | + let img_width = match options.model { |
| 116 | + SkinModel::Slim => 14, |
| 117 | + SkinModel::Regular => 16, |
| 118 | + }; |
| 119 | + |
| 120 | + let arm_width = match options.model { |
| 121 | + SkinModel::Slim => 3, |
| 122 | + SkinModel::Regular => 4, |
| 123 | + }; |
| 124 | + |
| 125 | + let mut image = RgbaImage::new(img_width, 32); |
| 126 | + |
| 127 | + // Head (centered) |
| 128 | + imageops::overlay( |
| 129 | + &mut image, |
| 130 | + &self.get_part(layer_type, BodyPart::Head, options.model), |
| 131 | + arm_width, |
| 132 | + 0, |
| 133 | + ); |
| 134 | + // Body (centered) |
| 135 | + imageops::overlay( |
| 136 | + &mut image, |
| 137 | + &self.get_part(layer_type, BodyPart::Body, options.model), |
| 138 | + arm_width, |
| 139 | + 8, |
| 140 | + ); |
| 141 | + // Right Arm (viewer left) |
| 142 | + imageops::overlay( |
| 143 | + &mut image, |
| 144 | + &self.get_part(layer_type, BodyPart::ArmRight, options.model), |
| 145 | + 0, |
| 146 | + 8, |
| 147 | + ); |
| 148 | + // Left Arm (viewer right) |
| 149 | + imageops::overlay( |
| 150 | + &mut image, |
| 151 | + &self.get_part(layer_type, BodyPart::ArmLeft, options.model), |
| 152 | + i64::from(img_width) - arm_width, |
| 153 | + 8, |
| 154 | + ); |
| 155 | + // Right Leg |
| 156 | + imageops::overlay( |
| 157 | + &mut image, |
| 158 | + &self.get_part(layer_type, BodyPart::LegLeft, options.model), |
| 159 | + arm_width, |
| 160 | + 20, |
| 161 | + ); |
| 162 | + // Left Leg |
| 163 | + imageops::overlay( |
| 164 | + &mut image, |
| 165 | + &self.get_part(layer_type, BodyPart::LegRight, options.model), |
| 166 | + arm_width + 4, |
| 167 | + 20, |
| 168 | + ); |
| 169 | + |
| 170 | + DynamicImage::ImageRgba8(image) |
| 171 | + } |
| 172 | + |
| 173 | + pub(crate) fn render_cube(&self, size: u32, options: RenderOptions) -> DynamicImage { |
| 174 | + let scale = (size as f32) / 20.0_f32; |
| 175 | + |
| 176 | + let x_render_offset = scale.ceil() as i64; |
| 177 | + let z_render_offset = x_render_offset / 2; |
| 178 | + |
| 179 | + let mut render = RgbaImage::new(size, size); |
| 180 | + |
| 181 | + let z_offset = scale * 3.0; |
| 182 | + let x_offset = scale * 2.0; |
| 183 | + |
| 184 | + // TODO: Update these coordinates when Hytale format is known |
| 185 | + let head_orig_top = self.0.crop_imm(8, 0, 8, 8); |
| 186 | + let head_orig_right = self.0.crop_imm(0, 8, 8, 8); |
| 187 | + let head_orig_front = self.0.crop_imm(8, 8, 8, 8); |
| 188 | + |
| 189 | + let head_orig_top_overlay = self.0.crop_imm(40, 0, 8, 8); |
| 190 | + let head_orig_right_overlay = self.0.crop_imm(32, 8, 8, 8); |
| 191 | + let head_orig_front_overlay = self.0.crop_imm(40, 8, 8, 8); |
| 192 | + |
| 193 | + let head_orig_right = head_orig_right.brighten(-4); |
| 194 | + let head_orig_right_overlay = head_orig_right_overlay.brighten(-4); |
| 195 | + |
| 196 | + let mut scratch = RgbaImage::new(size, size); |
| 197 | + |
| 198 | + // head top |
| 199 | + let head_top_skew = |
| 200 | + Projection::from_matrix([1.0, 1.0, 0.0, -SKEW_A, SKEW_A, 0.0, 0.0, 0.0, 1.0]).unwrap() |
| 201 | + * Projection::translate(-0.5 - z_offset, x_offset + z_offset - 0.5) |
| 202 | + * Projection::scale(scale, scale + (1.0 / 8.0)); |
| 203 | + warp_into( |
| 204 | + &head_orig_top.into_rgba8(), |
| 205 | + &head_top_skew, |
| 206 | + Interpolation::Nearest, |
| 207 | + Rgba([0, 0, 0, 0]), |
| 208 | + &mut scratch, |
| 209 | + ); |
| 210 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 211 | + |
| 212 | + // head front |
| 213 | + let head_front_skew = |
| 214 | + Projection::from_matrix([1.0, 0.0, 0.0, -SKEW_A, SKEW_B, SKEW_A, 0.0, 0.0, 1.0]) |
| 215 | + .unwrap() * Projection::translate( |
| 216 | + x_offset + 7.5 * scale - 0.5, |
| 217 | + (x_offset + 8.0 * scale) + z_offset - 0.5, |
| 218 | + ) * Projection::scale(scale, scale); |
| 219 | + warp_into( |
| 220 | + &head_orig_front.into_rgba8(), |
| 221 | + &head_front_skew, |
| 222 | + Interpolation::Nearest, |
| 223 | + Rgba([0, 0, 0, 0]), |
| 224 | + &mut scratch, |
| 225 | + ); |
| 226 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 227 | + |
| 228 | + // head right |
| 229 | + let head_right_skew = |
| 230 | + Projection::from_matrix([1.0, 0.0, 0.0, SKEW_A, SKEW_B, 0.0, 0.0, 0.0, 1.0]).unwrap() |
| 231 | + * Projection::translate(x_offset - (scale / 2.0), z_offset + scale) |
| 232 | + * Projection::scale(scale + (0.5 / 8.0), scale + (1.0 / 8.0)); |
| 233 | + warp_into( |
| 234 | + &head_orig_right.into_rgba8(), |
| 235 | + &head_right_skew, |
| 236 | + Interpolation::Nearest, |
| 237 | + Rgba([0, 0, 0, 0]), |
| 238 | + &mut scratch, |
| 239 | + ); |
| 240 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 241 | + |
| 242 | + if options.armored { |
| 243 | + warp_into( |
| 244 | + &head_orig_top_overlay.into_rgba8(), |
| 245 | + &head_top_skew, |
| 246 | + Interpolation::Nearest, |
| 247 | + Rgba([0, 0, 0, 0]), |
| 248 | + &mut scratch, |
| 249 | + ); |
| 250 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 251 | + |
| 252 | + warp_into( |
| 253 | + &head_orig_front_overlay.into_rgba8(), |
| 254 | + &head_front_skew, |
| 255 | + Interpolation::Nearest, |
| 256 | + Rgba([0, 0, 0, 0]), |
| 257 | + &mut scratch, |
| 258 | + ); |
| 259 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 260 | + |
| 261 | + warp_into( |
| 262 | + &head_orig_right_overlay.into_rgba8(), |
| 263 | + &head_right_skew, |
| 264 | + Interpolation::Nearest, |
| 265 | + Rgba([0, 0, 0, 0]), |
| 266 | + &mut scratch, |
| 267 | + ); |
| 268 | + imageops::overlay(&mut render, &scratch, x_render_offset, z_render_offset); |
| 269 | + } |
| 270 | + |
| 271 | + DynamicImage::ImageRgba8(render) |
| 272 | + } |
| 273 | +} |
0 commit comments