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.
7///
8/// It bounds the memory a single texture can claim, so a malformed file header
9/// cannot ask the decoder for an allocation the device cannot serve.
10/// 纹理的最大宽度或高度。它约束单张纹理所占内存,使畸形文件头无法向解码器索要设备提供不了
11/// 的分配。
12pub const MAX_TEXTURE_DIMENSION: u32 = 8192;
13
14/// A 2D texture owning an RGBA8 pixel buffer, with nearest-neighbor wrap sampling.
15/// 一个拥有 RGBA8 像素缓冲的 2D 纹理,支持最近邻环绕采样。
16#[derive(Clone, Debug, PartialEq)]
17pub struct Texture {
18   width: u32,
19   height: u32,
20   pixels: Vec<ColorRgba8>,
21}
22
23impl Texture {
24   /// Construct a texture, validating dimensions and buffer length.
25   /// 构造一个纹理,校验尺寸和缓冲长度。
26   ///
27   /// # Errors
28   /// 尺寸为 0 或超过 `MAX_TEXTURE_DIMENSION`、像素数量与尺寸不符,或尺寸乘积溢出时返回相应错误。
29   /// Returns an error when a dimension is zero or exceeds `MAX_TEXTURE_DIMENSION`,
30   /// when the pixel count does not match the dimensions, or when the product overflows.
31   pub fn new(width: u32, height: u32, pixels: Vec<ColorRgba8>) -> Result<Self, Error> {
32      if width == 0
33         || height == 0
34         || width > MAX_TEXTURE_DIMENSION
35         || height > MAX_TEXTURE_DIMENSION
36      {
37         return Err(Error::InvalidSize);
38      }
39      let expected = (width as usize)
40         .checked_mul(height as usize)
41         .ok_or(Error::Overflow)?;
42      if pixels.len() != expected {
43         return Err(Error::BufferMismatch);
44      }
45      Ok(Self {
46         width,
47         height,
48         pixels,
49      })
50   }
51
52   /// Construct a texture from a borrowed pixel slice.
53   /// 从借用的像素切片构造一个纹理。
54   ///
55   /// # Errors
56   /// 与 [`Texture::new`] 相同。
57   /// Same errors as [`Texture::new`].
58   pub fn from_pixels(width: u32, height: u32, pixels: &[ColorRgba8]) -> Result<Self, Error> {
59      Self::new(width, height, pixels.to_vec())
60   }
61
62   /// Build a 1×1 solid-color texture: any UV samples the same color.
63   /// 构建一个 1×1 纯色纹理:任何 UV 都采样到相同颜色。
64   #[must_use]
65   pub fn solid(color: ColorRgba8) -> Self {
66      Self {
67         width: 1,
68         height: 1,
69         pixels: alloc::vec![color],
70      }
71   }
72
73   /// Texture width in pixels.
74   /// 纹理宽度(像素)。
75   #[must_use]
76   pub fn width(&self) -> u32 {
77      self.width
78   }
79
80   /// Texture height in pixels.
81   /// 纹理高度(像素)。
82   #[must_use]
83   pub fn height(&self) -> u32 {
84      self.height
85   }
86
87   /// The underlying pixel buffer, row-major.
88   /// 底层像素缓冲,按行优先排列。
89   #[must_use]
90   pub fn pixels(&self) -> &[ColorRgba8] {
91      &self.pixels
92   }
93
94   /// Sample a texel with `[0, 1)`-wrapped nearest-neighbor addressing.
95   /// 使用 `[0, 1)` 环绕的最近邻寻址采样一个纹素。
96   ///
97   /// Out-of-range or non-finite coordinates are wrapped/clamped and never index out of bounds.
98   /// 越界或非有限坐标会被环绕/截断,绝不会产生越界索引。
99   #[must_use]
100   pub fn sample(&self, u: f32, v: f32) -> ColorRgba8 {
101      let x = sample_axis(u, self.width);
102      let y = sample_axis(v, self.height);
103      self.pixels[y * self.width as usize + x]
104   }
105
106   /// Sample and convert to linear light.
107   /// 采样并转换为线性光。
108   #[must_use]
109   pub fn sample_linear(&self, u: f32, v: f32) -> Rgb {
110      self.sample(u, v).to_linear()
111   }
112}
113
114/// Map a wrapped `[0, 1)` coordinate to a texel index in `[0, size)`.
115/// 将环绕后的 `[0, 1)` 坐标映射到 `[0, size)` 范围内的纹素索引。
116//
117// `size` is bounded by MAX_TEXTURE_DIMENSION and `wrapped` is in [0, 1), so the
118// float→i64→usize path below cannot overflow or meaningfully lose precision.
119// `size` 受 MAX_TEXTURE_DIMENSION 约束且 `wrapped` 位于 [0, 1) 内,因此下面的
120// float→i64→usize 路径不会溢出或产生有意义的精度损失。
121#[expect(
122   clippy::cast_possible_truncation,
123   clippy::cast_possible_wrap,
124   clippy::cast_precision_loss,
125   clippy::cast_sign_loss
126)]
127fn sample_axis(coord: f32, size: u32) -> usize {
128   let wrapped = wrap_uv(coord);
129   let scaled = wrapped * size as f32;
130   let index = scaled as i32;
131   // `wrapped` is in `[0, 1)`, so the index is never negative — only the upper
132   // bound needs clamping, because a coordinate just below 1 can round up to
133   // `size` in `f32`.
134   // `wrapped` 位于 `[0, 1)`,索引不会为负——只需钳制上界,因为略小于 1 的坐标在 `f32`
135   // 中可能进位到 `size`。
136   index.min(size as i32 - 1) as usize
137}
138
139/// Return the fractional part of `x` in `[0, 1)`, wrapping upward for negative
140/// input so a texture tiles on both sides of the origin.
141/// 返回 `x` 在 `[0, 1)` 范围内的小数部分;负输入向正方向环绕,因此纹理在原点两侧都能平铺。
142///
143/// `x - floor(x)` already lies in `[0, 1)` for every finite `x` (and is `NaN` for
144/// an infinity, which the index cast maps to 0), so no negative correction is
145/// needed.
146/// 对每个有限的 `x`,`x - floor(x)` 本就落在 `[0, 1)` 内(无穷时为 `NaN`,索引转换会
147/// 将其映射为 0),因此无需负数修正。
148#[inline]
149fn wrap_uv(x: f32) -> f32 {
150   x - floor(x)
151}
152
153#[cfg(test)]
154mod tests {
155   use alloc::vec;
156
157   use super::*;
158
159   fn checker() -> Texture {
160      // 2x2 texture: RGBA of known distinct colors.
161      // 2x2 纹理:已知不同颜色的 RGBA。
162      let pixels = vec![
163         ColorRgba8::from_rgb(255, 0, 0),
164         ColorRgba8::from_rgb(0, 255, 0),
165         ColorRgba8::from_rgb(0, 0, 255),
166         ColorRgba8::from_rgb(255, 255, 255),
167      ];
168      Texture::new(2, 2, pixels).unwrap()
169   }
170
171   #[test]
172   fn samples_corners() {
173      let tex = checker();
174      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
175      assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
176   }
177
178   #[test]
179   fn wraps_out_of_range_coordinates() {
180      let tex = checker();
181      // u=1.5 wraps to 0.5 -> x=1.
182      // u=1.5 环绕为 0.5 -> x=1。
183      assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
184      // Negative v wraps to the positive fractional part.
185      // 负的 v 环绕到正的分数部分。
186      assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
187   }
188
189   #[test]
190   fn rejects_zero_dimension() {
191      assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
192   }
193
194   #[test]
195   fn rejects_oversized_dimension() {
196      assert_eq!(
197         Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
198         Err(Error::InvalidSize)
199      );
200   }
201
202   #[test]
203   fn rejects_buffer_mismatch() {
204      let pixels = vec![ColorRgba8::BLACK; 3];
205      assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
206   }
207
208   #[test]
209   fn solid_samples_constant_color() {
210      let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
211      assert_eq!(tex.width(), 1);
212      assert_eq!(tex.height(), 1);
213      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
214      assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
215      assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
216   }
217}