Skip to main content

he3d_core/
texture.rs

1use alloc::vec::Vec;
2use he3d_math::scalar::floor;
3
4use crate::{ColorRgba8, Error, Rgb};
5
6/// Maximum width or height of a texture, matching the reference renderer.
7/// 纹理的最大宽度或高度,与参考渲染器一致。
8pub const MAX_TEXTURE_DIMENSION: u32 = 8192;
9
10/// A 2D texture owning an RGBA8 pixel buffer, with nearest-neighbor wrap sampling.
11/// 一个拥有 RGBA8 像素缓冲的 2D 纹理,支持最近邻环绕采样。
12#[derive(Clone, Debug, PartialEq)]
13pub struct Texture {
14   width: u32,
15   height: u32,
16   pixels: Vec<ColorRgba8>,
17}
18
19impl Texture {
20   /// Construct a texture, validating dimensions and buffer length.
21   /// 构造一个纹理,校验尺寸和缓冲长度。
22   pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
23      if width == 0
24         || height == 0
25         || width > MAX_TEXTURE_DIMENSION
26         || height > MAX_TEXTURE_DIMENSION
27      {
28         return Err(Error::InvalidSize);
29      }
30      let expected = (width as usize)
31         .checked_mul(height as usize)
32         .ok_or(Error::Overflow)?;
33      if pixels.len() != expected {
34         return Err(Error::BufferMismatch);
35      }
36      Ok(Self {
37         width,
38         height,
39         pixels,
40      })
41   }
42
43   /// Construct a texture from a borrowed pixel slice.
44   /// 从借用的像素切片构造一个纹理。
45   pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
46      Self::new(width, height, pixels.to_vec())
47   }
48
49   /// Build a 1×1 solid-color texture: any UV samples the same color.
50   /// 构建一个 1×1 纯色纹理:任何 UV 都采样到相同颜色。
51   pub fn solid(color: ColorRgba8) -> Self {
52      Self {
53         width: 1,
54         height: 1,
55         pixels: alloc::vec![color],
56      }
57   }
58
59   pub fn width(&self) -> u32 {
60      self.width
61   }
62
63   pub fn height(&self) -> u32 {
64      self.height
65   }
66
67   pub fn pixels(&self) -> &[ColorRgba8] {
68      &self.pixels
69   }
70
71   /// Sample a texel with `[0, 1)`-wrapped nearest-neighbor addressing.
72   /// 使用 `[0, 1)` 环绕的最近邻寻址采样一个纹素。
73   ///
74   /// Out-of-range or non-finite coordinates are wrapped/clamped and never index out of bounds.
75   /// 越界或非有限坐标会被环绕/截断,绝不会产生越界索引。
76   pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
77      let x = self.sample_axis(u, self.width);
78      let y = self.sample_axis(v, self.height);
79      self.pixels[y * self.width as usize + x]
80   }
81
82   /// Sample and convert to linear light.
83   /// 采样并转换为线性光。
84   pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
85      self.sample(u, v).to_linear()
86   }
87
88   fn sample_axis(&self, coord: f32, size: u32) -> usize {
89      let wrapped = wrap_uv(coord);
90      let index = (wrapped * size as f32) as i64;
91      index.clamp(0, size as i64 - 1) as usize
92   }
93}
94
95/// Return the fractional part of `x` in `[0, 1)`, matching the reference renderer's `fracf` wrap semantics.
96/// 返回 `x` 在 `[0, 1)` 范围内的小数部分,与参考渲染器的 `fracf` 环绕语义一致。
97fn wrap_uv(x: f32) -> f32 {
98   let frac = x - floor(x);
99   if frac < 0.0 { frac + 1.0 } else { frac }
100}
101
102#[cfg(test)]
103mod tests {
104   use alloc::vec;
105
106   use super::*;
107
108   fn checker() -> Texture {
109      // 2x2 texture: RGBA of known distinct colors.
110      // 2x2 纹理:已知不同颜色的 RGBA。
111      let pixels = vec![
112         ColorRgba8::from_rgb(255, 0, 0),
113         ColorRgba8::from_rgb(0, 255, 0),
114         ColorRgba8::from_rgb(0, 0, 255),
115         ColorRgba8::from_rgb(255, 255, 255),
116      ];
117      Texture::new(2, 2, pixels).unwrap()
118   }
119
120   #[test]
121   fn samples_corners() {
122      let tex = checker();
123      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
124      assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
125   }
126
127   #[test]
128   fn wraps_out_of_range_coordinates() {
129      let tex = checker();
130      // u=1.5 wraps to 0.5 -> x=1.
131      // u=1.5 环绕为 0.5 -> x=1。
132      assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
133      // Negative v wraps to the positive fractional part.
134      // 负的 v 环绕到正的分数部分。
135      assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
136   }
137
138   #[test]
139   fn rejects_zero_dimension() {
140      assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
141   }
142
143   #[test]
144   fn rejects_oversized_dimension() {
145      assert_eq!(
146         Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
147         Err(Error::InvalidSize)
148      );
149   }
150
151   #[test]
152   fn rejects_buffer_mismatch() {
153      let pixels = vec![ColorRgba8::BLACK; 3];
154      assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
155   }
156
157   #[test]
158   fn solid_samples_constant_color() {
159      let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
160      assert_eq!(tex.width(), 1);
161      assert_eq!(tex.height(), 1);
162      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
163      assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
164      assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
165   }
166}