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   ///
23   /// # Errors
24   /// 尺寸为 0 或超过 `MAX_TEXTURE_DIMENSION`、像素数量与尺寸不符,或尺寸乘积溢出时返回相应错误。
25   /// Returns an error when a dimension is zero or exceeds `MAX_TEXTURE_DIMENSION`,
26   /// when the pixel count does not match the dimensions, or when the product overflows.
27   pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
28      if width == 0
29         || height == 0
30         || width > MAX_TEXTURE_DIMENSION
31         || height > MAX_TEXTURE_DIMENSION
32      {
33         return Err(Error::InvalidSize);
34      }
35      let expected = (width as usize)
36         .checked_mul(height as usize)
37         .ok_or(Error::Overflow)?;
38      if pixels.len() != expected {
39         return Err(Error::BufferMismatch);
40      }
41      Ok(Self {
42         width,
43         height,
44         pixels,
45      })
46   }
47
48   /// Construct a texture from a borrowed pixel slice.
49   /// 从借用的像素切片构造一个纹理。
50   ///
51   /// # Errors
52   /// 与 [`Texture::new`] 相同。
53   /// Same errors as [`Texture::new`].
54   pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
55      Self::new(width, height, pixels.to_vec())
56   }
57
58   /// Build a 1×1 solid-color texture: any UV samples the same color.
59   /// 构建一个 1×1 纯色纹理:任何 UV 都采样到相同颜色。
60   #[must_use]
61   pub fn solid(color: ColorRgba8) -> Self {
62      Self {
63         width: 1,
64         height: 1,
65         pixels: alloc::vec![color],
66      }
67   }
68
69   /// Texture width in pixels.
70   /// 纹理宽度(像素)。
71   #[must_use]
72   pub fn width(&self) -> u32 {
73      self.width
74   }
75
76   /// Texture height in pixels.
77   /// 纹理高度(像素)。
78   #[must_use]
79   pub fn height(&self) -> u32 {
80      self.height
81   }
82
83   /// The underlying pixel buffer, row-major.
84   /// 底层像素缓冲,按行优先排列。
85   #[must_use]
86   pub fn pixels(&self) -> &[ColorRgba8] {
87      &self.pixels
88   }
89
90   /// Sample a texel with `[0, 1)`-wrapped nearest-neighbor addressing.
91   /// 使用 `[0, 1)` 环绕的最近邻寻址采样一个纹素。
92   ///
93   /// Out-of-range or non-finite coordinates are wrapped/clamped and never index out of bounds.
94   /// 越界或非有限坐标会被环绕/截断,绝不会产生越界索引。
95   #[must_use]
96   pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
97      let x = sample_axis(u, self.width);
98      let y = sample_axis(v, self.height);
99      self.pixels[y * self.width as usize + x]
100   }
101
102   /// Sample and convert to linear light.
103   /// 采样并转换为线性光。
104   #[must_use]
105   pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
106      self.sample(u, v).to_linear()
107   }
108}
109
110/// Map a wrapped `[0, 1)` coordinate to a texel index in `[0, size)`.
111/// 将环绕后的 `[0, 1)` 坐标映射到 `[0, size)` 范围内的纹素索引。
112//
113// `size` is bounded by MAX_TEXTURE_DIMENSION and `wrapped` is in [0, 1), so the
114// float→i64→usize path below cannot overflow or meaningfully lose precision.
115// `size` 受 MAX_TEXTURE_DIMENSION 约束且 `wrapped` 位于 [0, 1) 内,因此下面的
116// float→i64→usize 路径不会溢出或产生有意义的精度损失。
117#[expect(
118   clippy::cast_possible_truncation,
119   clippy::cast_precision_loss,
120   clippy::cast_sign_loss
121)]
122fn sample_axis(coord: f32, size: u32) -> usize {
123   let wrapped = wrap_uv(coord);
124   let scaled = wrapped * size as f32;
125   let index = scaled as i64;
126   index.clamp(0, i64::from(size) - 1) as usize
127}
128
129/// Return the fractional part of `x` in `[0, 1)`, matching the reference renderer's `fracf` wrap semantics.
130/// 返回 `x` 在 `[0, 1)` 范围内的小数部分,与参考渲染器的 `fracf` 环绕语义一致。
131fn wrap_uv(x: f32) -> f32 {
132   let frac = x - floor(x);
133   if frac < 0.0 { frac + 1.0 } else { frac }
134}
135
136#[cfg(test)]
137mod tests {
138   use alloc::vec;
139
140   use super::*;
141
142   fn checker() -> Texture {
143      // 2x2 texture: RGBA of known distinct colors.
144      // 2x2 纹理:已知不同颜色的 RGBA。
145      let pixels = vec![
146         ColorRgba8::from_rgb(255, 0, 0),
147         ColorRgba8::from_rgb(0, 255, 0),
148         ColorRgba8::from_rgb(0, 0, 255),
149         ColorRgba8::from_rgb(255, 255, 255),
150      ];
151      Texture::new(2, 2, pixels).unwrap()
152   }
153
154   #[test]
155   fn samples_corners() {
156      let tex = checker();
157      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
158      assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
159   }
160
161   #[test]
162   fn wraps_out_of_range_coordinates() {
163      let tex = checker();
164      // u=1.5 wraps to 0.5 -> x=1.
165      // u=1.5 环绕为 0.5 -> x=1。
166      assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
167      // Negative v wraps to the positive fractional part.
168      // 负的 v 环绕到正的分数部分。
169      assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
170   }
171
172   #[test]
173   fn rejects_zero_dimension() {
174      assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
175   }
176
177   #[test]
178   fn rejects_oversized_dimension() {
179      assert_eq!(
180         Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
181         Err(Error::InvalidSize)
182      );
183   }
184
185   #[test]
186   fn rejects_buffer_mismatch() {
187      let pixels = vec![ColorRgba8::BLACK; 3];
188      assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
189   }
190
191   #[test]
192   fn solid_samples_constant_color() {
193      let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
194      assert_eq!(tex.width(), 1);
195      assert_eq!(tex.height(), 1);
196      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
197      assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
198      assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
199   }
200}