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_precision_loss,
124   clippy::cast_sign_loss
125)]
126fn sample_axis(coord: f32, size: u32) -> usize {
127   let wrapped = wrap_uv(coord);
128   let scaled = wrapped * size as f32;
129   let index = scaled as i64;
130   index.clamp(0, i64::from(size) - 1) as usize
131}
132
133/// Return the fractional part of `x` in `[0, 1)`, wrapping upward for negative
134/// input so a texture tiles on both sides of the origin.
135/// 返回 `x` 在 `[0, 1)` 范围内的小数部分;负输入向正方向环绕,因此纹理在原点两侧都能平铺。
136fn wrap_uv(x: f32) -> f32 {
137   let frac = x - floor(x);
138   if frac < 0.0 { frac + 1.0 } else { frac }
139}
140
141#[cfg(test)]
142mod tests {
143   use alloc::vec;
144
145   use super::*;
146
147   fn checker() -> Texture {
148      // 2x2 texture: RGBA of known distinct colors.
149      // 2x2 纹理:已知不同颜色的 RGBA。
150      let pixels = vec![
151         ColorRgba8::from_rgb(255, 0, 0),
152         ColorRgba8::from_rgb(0, 255, 0),
153         ColorRgba8::from_rgb(0, 0, 255),
154         ColorRgba8::from_rgb(255, 255, 255),
155      ];
156      Texture::new(2, 2, pixels).unwrap()
157   }
158
159   #[test]
160   fn samples_corners() {
161      let tex = checker();
162      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(255, 0, 0));
163      assert_eq!(tex.sample(0.99, 0.99), ColorRgba8::from_rgb(255, 255, 255));
164   }
165
166   #[test]
167   fn wraps_out_of_range_coordinates() {
168      let tex = checker();
169      // u=1.5 wraps to 0.5 -> x=1.
170      // u=1.5 环绕为 0.5 -> x=1。
171      assert_eq!(tex.sample(1.5, 0.0), ColorRgba8::from_rgb(0, 255, 0));
172      // Negative v wraps to the positive fractional part.
173      // 负的 v 环绕到正的分数部分。
174      assert_eq!(tex.sample(0.0, -0.5), tex.sample(0.0, 0.5));
175   }
176
177   #[test]
178   fn rejects_zero_dimension() {
179      assert_eq!(Texture::new(0, 2, Vec::new()), Err(Error::InvalidSize));
180   }
181
182   #[test]
183   fn rejects_oversized_dimension() {
184      assert_eq!(
185         Texture::new(MAX_TEXTURE_DIMENSION + 1, 2, Vec::new()),
186         Err(Error::InvalidSize)
187      );
188   }
189
190   #[test]
191   fn rejects_buffer_mismatch() {
192      let pixels = vec![ColorRgba8::BLACK; 3];
193      assert_eq!(Texture::new(2, 2, pixels), Err(Error::BufferMismatch));
194   }
195
196   #[test]
197   fn solid_samples_constant_color() {
198      let tex = Texture::solid(ColorRgba8::from_rgb(10, 20, 30));
199      assert_eq!(tex.width(), 1);
200      assert_eq!(tex.height(), 1);
201      assert_eq!(tex.sample(0.0, 0.0), ColorRgba8::from_rgb(10, 20, 30));
202      assert_eq!(tex.sample(0.5, 0.5), ColorRgba8::from_rgb(10, 20, 30));
203      assert_eq!(tex.sample(1.5, -0.5), ColorRgba8::from_rgb(10, 20, 30));
204   }
205}