MONKEY (skybox): second texture stage for two-texture skybox batches
RenderSubmesh carries stage 1 (texture, UV set, transform, Mod/Mod2x) for textureCount 2 batches; M2Vertex reads UV1. WowModelExt gains a stage1 uniform + texture binding 94/95 and the WOW_STAGE1 shader def, so every one-texture batch compiles the old code. The skybox lane binds stage 1 on its own material copies and drives its transform rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f13a7c9ead
commit
22d75ea613
@@ -889,6 +889,7 @@ fn warm_quad(colors: bool, skinned: bool) -> RenderSubmesh {
|
||||
rgb_seq: None,
|
||||
wmo_batch: None,
|
||||
section: None,
|
||||
stage1: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ pub struct WowModelKey {
|
||||
/// The WMO-skybox lane (`clutter_fade.z` bit 13, `model_render::SKY_DEPTH_MARKER`), a key axis
|
||||
/// because only this model's depth is pinned.
|
||||
sky_depth: bool,
|
||||
/// MONKEY (skybox): a second texture stage (`stage1.x`), the `WOW_STAGE1` shader def.
|
||||
stage1: bool,
|
||||
// The WMO batch order is deliberately not a key axis (a pipeline per batch index stalls a
|
||||
// city's first sight): the file-order layering (`0x6b4f10`/`0x6b5190`) rides `sun_scale.y`.
|
||||
}
|
||||
@@ -89,6 +91,7 @@ impl From<&WowModelExt> for WowModelKey {
|
||||
zfill: markers & 0x200 != 0,
|
||||
far_side: markers & 0x800 != 0,
|
||||
sky_depth: markers & 0x2000 != 0,
|
||||
stage1: e.stage1.x > 0.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +125,14 @@ pub struct WowModelExt {
|
||||
/// tint, `z` = the texture-transform affine, `w` = the UI tile's cell clip.
|
||||
#[uniform(100)]
|
||||
pub anim_slots: Vec4,
|
||||
/// MONKEY (skybox): a two-texture batch's stage 1: `x` = 0 none, 1 Mod, 2 Mod2x; `z`/`w` = the
|
||||
/// `matanim` rows of its translation and affine, read on UV set B. `0` on every other batch.
|
||||
#[uniform(100)]
|
||||
pub stage1: Vec4,
|
||||
/// MONKEY (skybox): stage 1's texture; `None` (the fallback image) unless `stage1.x > 0`.
|
||||
#[texture(94)]
|
||||
#[sampler(95)]
|
||||
pub stage1_texture: Option<Handle<Image>>,
|
||||
/// The shared global light (`lighting::global_light`), updated in place once a frame; the
|
||||
/// vertex stage reads its point-light table, since Bevy's clusterable lights are fragment-only.
|
||||
#[storage(90, read_only, buffer, visibility(vertex, fragment))]
|
||||
@@ -254,6 +265,13 @@ impl MaterialExtension for WowModelExt {
|
||||
ds.bias.constant = 0;
|
||||
}
|
||||
}
|
||||
// MONKEY (skybox): the second texture stage compiles only where a batch has one.
|
||||
if key.bind_group_data.stage1 {
|
||||
descriptor.vertex.shader_defs.push("WOW_STAGE1".into());
|
||||
if let Some(fragment) = descriptor.fragment.as_mut() {
|
||||
fragment.shader_defs.push("WOW_STAGE1".into());
|
||||
}
|
||||
}
|
||||
// The skybox pins clip z to 0, reverse-Z far, in the vertex stage; its bias is sort-only.
|
||||
if key.bind_group_data.sky_depth {
|
||||
descriptor.vertex.shader_defs.push("WOW_SKY_DEPTH".into());
|
||||
|
||||
@@ -47,8 +47,14 @@ struct ModelParams {
|
||||
// Rows of `wow_light.matanim`, 0 = identity: x = UV scroll, y = tint, z = texture-transform
|
||||
// affine, w = the UI tile's cell clip.
|
||||
anim_slots: vec4<f32>,
|
||||
// MONKEY (skybox): stage 1 of a two-texture batch: x = 0 none / 1 Mod / 2 Mod2x, z/w = the
|
||||
// matanim rows of its translation and affine.
|
||||
stage1: vec4<f32>,
|
||||
};
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> m: ModelParams;
|
||||
// MONKEY (skybox): stage 1's texture, the fallback image off `WOW_STAGE1`.
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(94) var stage1_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(95) var stage1_sampler: sampler;
|
||||
|
||||
// A fully covered character shadow retains 45% of the authored model lighting.
|
||||
const SHADOW_SUN_FLOOR: f32 = 0.45;
|
||||
@@ -1286,6 +1292,22 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
|
||||
base_color.a = base_color.a * ramp;
|
||||
}
|
||||
|
||||
// MONKEY (skybox): stage 1 multiplies in on UV set B, moved by its own texture transform
|
||||
// (the stage-0 law above): colour ×1 (Mod) or ×2 (Mod2x), alpha ×1.
|
||||
#ifdef WOW_STAGE1
|
||||
#ifdef VERTEX_UVS_B
|
||||
{
|
||||
let t1 = wow_light.matanim[u32(m.stage1.z)];
|
||||
let a1 = wow_light.matanim[u32(m.stage1.w)];
|
||||
let d1 = (in.uv_b + t1.xy - vec2<f32>(0.5, 0.5)) * vec2<f32>(1.0 + a1.z, 1.0 + a1.w);
|
||||
let c1 = 1.0 + a1.x;
|
||||
let uv1 = vec2<f32>(0.5 + d1.x * c1 - d1.y * a1.y, 0.5 + d1.x * a1.y + d1.y * c1);
|
||||
let s1 = textureSampleBias(stage1_texture, stage1_sampler, uv1, view.mip_bias);
|
||||
let k1 = select(1.0, 2.0, m.stage1.x > 1.5);
|
||||
base_color = vec4<f32>(base_color.rgb * s1.rgb * k1, base_color.a * s1.a);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
// The MeshTag (mesh_tag.rs): bit 31 = highlight, bit 30 = interior fog; payload bits 0-5 = the
|
||||
// fade alpha (a zero payload is untagged, opaque), 19-29 = the rig slot, 6-13 = the ground
|
||||
// shade (0 lit, 255 MCSH-shadowed) or, on an interior-prop material, 6-18 = the SH-probe slot.
|
||||
|
||||
@@ -577,6 +577,9 @@ impl WorldAssets {
|
||||
tint: Vec4::ONE, // clutter has no animated M2Color tint and is not a WMO batch
|
||||
sidn: Vec4::ZERO, // clutter is never SIDN/WINDOW glass (WMO-only)
|
||||
anim_slots: Vec4::ZERO,
|
||||
// MONKEY (skybox): no second stage.
|
||||
stage1: Vec4::ZERO,
|
||||
stage1_texture: None,
|
||||
light_buf: self.shared_light.clone(),
|
||||
// MONKEY (torch shadows Phase 3A): the shared torch receiver bindings.
|
||||
torch_depth: self.torch.depth.clone(),
|
||||
|
||||
@@ -275,7 +275,7 @@ pub use models::{
|
||||
CoverageReader, EmitterBoneLink, EventMarker, FogPolicy, FootprintTris, GlobalSeqBone,
|
||||
GlobalSeqChannel, GroundQuad, KeyAnim, M2AnimSummary, M2Attachment, M2Bounds, M2CameraTracks,
|
||||
M2Light, M2PaneCamera, M2PortraitCamera, ModelAnimation, ModelBlend, ParentArm, ParentBasis,
|
||||
PlayableAnim, RenderSubmesh, RgbAnim, ScalarAnim, SeqLoops, ShippedGlueScene, Skeleton,
|
||||
PlayableAnim, RenderSubmesh, RgbAnim, StageTwo, ScalarAnim, SeqLoops, ShippedGlueScene, Skeleton,
|
||||
SkeletonBone, StringAnchors, UvAnim, UvRotAnim, WmoBatchClass, WmoDoodad, WmoDoodadSet, WmoFog,
|
||||
WmoGroupHeader, WmoGroupInfo, WmoLight, WmoPortalInfo, WmoPortalRef, WmoPortals, WmoRoot,
|
||||
ALPHA_KEY_REF, DEGENERATE_RING_FOOTPRINT, GLUE_AUTHORED_ASPECT, NO_GROUP_LIQUID,
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::tex_anim;
|
||||
use super::{le_u16, le_u32, model_path, remap_submesh};
|
||||
use super::{
|
||||
AlphaAnim, Billboard, BillboardKind, BoneScaleAnim, CharSkinSlot, FogPolicy, ModelBlend,
|
||||
RenderSubmesh,
|
||||
RenderSubmesh, StageTwo,
|
||||
};
|
||||
|
||||
fn parent_dir(path: &str) -> &str {
|
||||
@@ -479,6 +479,33 @@ pub fn parse_m2_render_submeshes(
|
||||
let rgb_seq = rgb_track
|
||||
.and_then(|t| mat_anim::bake_rgb_seqs(t, &model.global_sequences, &seq_slots))
|
||||
.filter(|set| set.uniform().is_none());
|
||||
// MONKEY (skybox): stage 1 of a two-texture batch; an env-mapped stage 1 is left out.
|
||||
let stage1 = (batch.texture_count >= 2 && !model.stage_is_env_mapped(batch, 1))
|
||||
.then(|| {
|
||||
let rec = model
|
||||
.raw_data
|
||||
.texture_lookup_table
|
||||
.get(batch.texture_combo_index as usize + 1)
|
||||
.and_then(|&ti| model.textures.get(ti as usize))?;
|
||||
let set = model
|
||||
.texture_unit_lookup
|
||||
.get(batch.texture_coord_combo_index as usize + 1)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let combo = batch.texture_transform_combo_index.wrapping_add(1);
|
||||
Some((
|
||||
resolve_texture(rec, dir, skins).0,
|
||||
(rec.wrap_x, rec.wrap_y),
|
||||
set == 1,
|
||||
batch.shader_id & 0xf == 4,
|
||||
tex_anim::bake_uv_anim(model, combo, seq0_slot),
|
||||
tex_anim::bake_uv_rot_seqs(model, combo, &seq_slots)
|
||||
.and_then(|s| s.seq(None).cloned()),
|
||||
tex_anim::bake_uv_scale_seqs(model, combo, &seq_slots)
|
||||
.and_then(|s| s.seq(None).cloned()),
|
||||
))
|
||||
})
|
||||
.flatten();
|
||||
// The reference turns each billboard bone to the camera about its own pivot, and a batch
|
||||
// can hold cards on several bones (a candelabra's glows): one submesh per billboard bone.
|
||||
let make_billboard = |bone_idx: usize| -> Option<Billboard> {
|
||||
@@ -591,6 +618,26 @@ pub fn parse_m2_render_submeshes(
|
||||
sub.uv_scale_seq = uv_scale_seq.clone();
|
||||
sub.rgb_anim = rgb_anim.clone();
|
||||
sub.rgb_seq = rgb_seq.clone();
|
||||
// MONKEY (skybox): stage 1's UVs follow the submesh's own vertex order.
|
||||
sub.stage1 = stage1.as_ref().map(
|
||||
|(texture, (wrap_x, wrap_y), uv1, mod2x, uv_anim, uv_rot, uv_scale)| StageTwo {
|
||||
texture: texture.clone(),
|
||||
wrap_x: *wrap_x,
|
||||
wrap_y: *wrap_y,
|
||||
uvs: globals
|
||||
.iter()
|
||||
.map(|&g| {
|
||||
let v = &model.vertices[g as usize];
|
||||
let c = if *uv1 { &v.tex_coords2 } else { &v.tex_coords };
|
||||
[c.x, c.y]
|
||||
})
|
||||
.collect(),
|
||||
mod2x: *mod2x,
|
||||
uv_anim: uv_anim.clone(),
|
||||
uv_rot: uv_rot.clone(),
|
||||
uv_scale: uv_scale.clone(),
|
||||
},
|
||||
);
|
||||
out.push(sub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ fn remap_submesh(
|
||||
wmo_batch: None,
|
||||
env_map: false,
|
||||
section: None,
|
||||
stage1: None,
|
||||
},
|
||||
globals,
|
||||
)
|
||||
|
||||
@@ -262,6 +262,24 @@ pub enum FogPolicy {
|
||||
Off = 4,
|
||||
}
|
||||
|
||||
/// MONKEY (skybox): a two-texture batch's second stage (`textureCount 2`, which no stock 1.12
|
||||
/// model authors): texture `textureLookup[combo + 1]` on the UV set `texUnitLookup[coord + 1]`,
|
||||
/// moved by `texAnimLookup[transform + 1]`, multiplied into stage 0 (`Mod`, or `Mod2x` on the
|
||||
/// modern `0x4014` combiner).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StageTwo {
|
||||
pub texture: Option<String>,
|
||||
pub wrap_x: bool,
|
||||
pub wrap_y: bool,
|
||||
/// Per vertex, parallel to [`RenderSubmesh::positions`], from the stage's own UV set.
|
||||
pub uvs: Vec<[f32; 2]>,
|
||||
/// The stage's colour doubles (`Mod2x`).
|
||||
pub mod2x: bool,
|
||||
pub uv_anim: Option<UvAnim>,
|
||||
pub uv_rot: Option<super::key_anim::KeyAnim<[f32; 4]>>,
|
||||
pub uv_scale: Option<super::key_anim::KeyAnim<[f32; 2]>>,
|
||||
}
|
||||
|
||||
/// One render batch: self-contained geometry and its material.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderSubmesh {
|
||||
@@ -346,6 +364,8 @@ pub struct RenderSubmesh {
|
||||
/// over its base); the reference draws both from one vertex array under LEQUAL, the later
|
||||
/// winning exactly (`0x70c190`), so consolidators refuse a section they cannot take whole.
|
||||
pub section: Option<u16>,
|
||||
/// MONKEY (skybox): the second texture stage, `None` on every one-texture batch.
|
||||
pub stage1: Option<StageTwo>,
|
||||
/// Texture coordinates generated as a view-space sphere map, not read from [`Self::uvs`]
|
||||
/// (`texture_unit_lookup[texCoordSet] > 2`): `uv = normalize(P − 2(P·N)N).xy · 0.5 + 0.5`.
|
||||
/// Such a mesh parks its UVs at one point (`GnomeSubwayGlass.m2`).
|
||||
@@ -416,6 +436,7 @@ impl Default for RenderSubmesh {
|
||||
wmo_batch: None,
|
||||
env_map: false,
|
||||
section: None,
|
||||
stage1: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,11 @@ pub fn parse_m2(cursor: &mut Cursor<&[u8]>) -> Result<M2Format> {
|
||||
x: v.f32_at(32).ok_or(Error::Truncated)?,
|
||||
y: v.f32_at(36).ok_or(Error::Truncated)?,
|
||||
},
|
||||
// MONKEY (skybox): the second UV set.
|
||||
tex_coords2: C2 {
|
||||
x: v.f32_at(40).ok_or(Error::Truncated)?,
|
||||
y: v.f32_at(44).ok_or(Error::Truncated)?,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ impl M2Bone {
|
||||
}
|
||||
|
||||
/// One M2 vertex, 48 bytes: position @0, weights @0x0c, indices @0x10, normal @0x14, UV @0x20,
|
||||
/// and an unread second UV @0x28.
|
||||
/// and a second UV @0x28.
|
||||
pub struct M2Vertex {
|
||||
pub position: C3,
|
||||
/// Per-vertex bone influence weights (`/255`), paired with [`Self::bone_indices`].
|
||||
@@ -114,6 +114,8 @@ pub struct M2Vertex {
|
||||
pub bone_indices: [u8; 4],
|
||||
pub normal: C3,
|
||||
pub tex_coords: C2,
|
||||
/// MONKEY (skybox): the second UV set, which a two-texture batch's stage 1 can read.
|
||||
pub tex_coords2: C2,
|
||||
}
|
||||
|
||||
/// The texture lookup table and the collision hull's raw bytes.
|
||||
|
||||
@@ -323,6 +323,9 @@ pub fn model_material(
|
||||
},
|
||||
// Zero until a sampler registers this material and bakes its table slot in once.
|
||||
anim_slots: Vec4::ZERO,
|
||||
// MONKEY (skybox): no second stage.
|
||||
stage1: Vec4::ZERO,
|
||||
stage1_texture: None,
|
||||
light_buf: light.clone(),
|
||||
// MONKEY (torch shadows Phase 3A): the shared torch receiver bindings.
|
||||
torch_depth: torch.depth.clone(),
|
||||
@@ -446,6 +449,9 @@ pub fn zfill_material(
|
||||
tint: Vec4::new(1.0, 1.0, 1.0, 0.0),
|
||||
sidn: Vec4::ZERO,
|
||||
anim_slots: Vec4::ZERO,
|
||||
// MONKEY (skybox): no second stage.
|
||||
stage1: Vec4::ZERO,
|
||||
stage1_texture: None,
|
||||
light_buf: light.clone(),
|
||||
// MONKEY (torch shadows Phase 3A): the shared torch receiver bindings.
|
||||
torch_depth: torch.depth.clone(),
|
||||
|
||||
@@ -481,6 +481,10 @@ fn build_skybox(
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, sub.vertex_colors.clone());
|
||||
}
|
||||
// MONKEY (skybox): stage 1 reads UV set B.
|
||||
if let Some(st) = sub.stage1.as_ref().filter(|s| s.uvs.len() == sub.positions.len()) {
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_1, st.uvs.clone());
|
||||
}
|
||||
mesh.insert_indices(Indices::U32(sub.indices.clone()));
|
||||
// The batch's authored address mode: Caverns of Time's belts wrap their UVs.
|
||||
let texture = sub
|
||||
@@ -493,9 +497,41 @@ fn build_skybox(
|
||||
// The authored batch order after the layer's base (0 is unordered): every batch shares
|
||||
// one sort distance.
|
||||
let order = base + u16::try_from(i + 1).unwrap_or(0);
|
||||
let Some(pair) = mats.skybox(sub, texture, order, uv.as_ref(), tint.as_ref()) else {
|
||||
let Some(mut pair) = mats.skybox(sub, texture, order, uv.as_ref(), tint.as_ref())
|
||||
else {
|
||||
return; // light buffer vanished mid-build; `built` is unlatched, so we retry
|
||||
};
|
||||
// MONKEY (skybox): a two-texture batch takes its own copies with stage 1 bound, so
|
||||
// the deduped one-texture materials stay as they were.
|
||||
if let Some(st) = sub.stage1.as_ref().filter(|s| s.uvs.len() == sub.positions.len()) {
|
||||
let tex1 = st
|
||||
.texture
|
||||
.as_deref()
|
||||
.and_then(|t| world_assets.texture(t, (st.wrap_x, st.wrap_y), &mut images));
|
||||
let mode = if st.mod2x { 2.0 } else { 1.0 };
|
||||
let mut with_stage1 = |h: &Handle<WowModelMaterial>| {
|
||||
let mut m = crate::model_render::lazy::with_material_mut(
|
||||
mats.materials(),
|
||||
h.id(),
|
||||
|m| m.clone(),
|
||||
)?;
|
||||
m.extension.stage1 = Vec4::new(mode, 0.0, 0.0, 0.0);
|
||||
m.extension.stage1_texture = tex1.clone();
|
||||
Some(mats.materials().add(m))
|
||||
};
|
||||
let shared = pair.fade_blend == pair.steady;
|
||||
if let Some(steady) = with_stage1(&pair.steady) {
|
||||
let fade = if shared {
|
||||
Some(steady.clone())
|
||||
} else {
|
||||
with_stage1(&pair.fade_blend)
|
||||
};
|
||||
if let Some(fade) = fade {
|
||||
pair.steady = steady;
|
||||
pair.fade_blend = fade;
|
||||
}
|
||||
}
|
||||
}
|
||||
let lane = SkyMatLane::register(
|
||||
sub,
|
||||
uv,
|
||||
|
||||
@@ -313,6 +313,9 @@ pub(crate) struct SkyMatLane {
|
||||
pub(crate) uv: Option<(u16, [f32; 2], Arc<benilla_formats::UvAnim>)>,
|
||||
pub(crate) affine: Option<(u16, AffineLoops)>,
|
||||
pub(crate) tint: Option<(u16, [f32; 3], Arc<benilla_formats::RgbAnim>)>,
|
||||
/// Stage 1's translation row (absolute, no seed) and affine row.
|
||||
pub(crate) stage1_uv: Option<(u16, benilla_formats::UvAnim)>,
|
||||
pub(crate) stage1_affine: Option<(u16, AffineLoops)>,
|
||||
}
|
||||
|
||||
/// The rotation and scaling channels of a texture transform, slot 0's loops.
|
||||
@@ -383,11 +386,36 @@ impl SkyMatLane {
|
||||
lane.tint = Some((slot, seed, tint));
|
||||
}
|
||||
}
|
||||
// Stage 1: rows only where it moves; row 0 is the identity.
|
||||
if let Some(st) = &sub.stage1 {
|
||||
if let Some(uv) = st.uv_anim.clone() {
|
||||
if let Some(slot) = table.alloc() {
|
||||
write(materials, &|m| m.extension.stage1.z = f32::from(slot));
|
||||
lane.stage1_uv = Some((slot, uv));
|
||||
}
|
||||
}
|
||||
if st.uv_rot.is_some() || st.uv_scale.is_some() {
|
||||
if let Some(slot) = table.alloc() {
|
||||
write(materials, &|m| m.extension.stage1.w = f32::from(slot));
|
||||
lane.stage1_affine = Some((
|
||||
slot,
|
||||
AffineLoops {
|
||||
rot: st.uv_rot.clone(),
|
||||
scale: st.uv_scale.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
lane
|
||||
}
|
||||
|
||||
pub(crate) fn any(&self) -> bool {
|
||||
self.uv.is_some() || self.affine.is_some() || self.tint.is_some()
|
||||
self.uv.is_some()
|
||||
|| self.affine.is_some()
|
||||
|| self.tint.is_some()
|
||||
|| self.stage1_uv.is_some()
|
||||
|| self.stage1_affine.is_some()
|
||||
}
|
||||
|
||||
/// Write this frame's rows: the delta from each seed, quantized as the shared lane does.
|
||||
@@ -420,6 +448,29 @@ impl SkyMatLane {
|
||||
.map_or([1.0, 1.0], |l| l.sample(l.clock(band_t, gseq_now)));
|
||||
table.set(*slot, crate::mat_anim_table::affine_row(q, s));
|
||||
}
|
||||
if let Some((slot, a)) = &self.stage1_uv {
|
||||
let v = a.sample(a.clock(band_t, gseq_now));
|
||||
table.set(
|
||||
*slot,
|
||||
[
|
||||
benilla_assets::quantize(v[0], 4096.0),
|
||||
benilla_assets::quantize(v[1], 4096.0),
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
);
|
||||
}
|
||||
if let Some((slot, loops)) = &self.stage1_affine {
|
||||
let q = loops
|
||||
.rot
|
||||
.as_ref()
|
||||
.map_or([0.0, 0.0, 0.0, 1.0], |l| l.sample(l.clock(band_t, gseq_now)));
|
||||
let s = loops
|
||||
.scale
|
||||
.as_ref()
|
||||
.map_or([1.0, 1.0], |l| l.sample(l.clock(band_t, gseq_now)));
|
||||
table.set(*slot, crate::mat_anim_table::affine_row(q, s));
|
||||
}
|
||||
if let Some((slot, seed, a)) = &self.tint {
|
||||
let c = benilla_assets::quant255(a.sample(a.clock(band_t, gseq_now)));
|
||||
table.set(
|
||||
|
||||
Reference in New Issue
Block a user