partial scrolling texture support

This commit is contained in:
Brandon Johnson
2026-01-28 05:12:46 -05:00
parent 0da5eea45e
commit 0faf6d3b84
8 changed files with 104 additions and 7 deletions

View File

@@ -29,6 +29,7 @@ export const TevLayerFlags = {
TypeAlphaBlend: 1 << 13,
TypeWorldSpecular: 1 << 15,
Unk16: 1 << 16,
EnableUvScroll: 1 << 17,
};
export const ModelFlags = {

View File

@@ -355,6 +355,12 @@ export class AnimGroup {
const rp = scratchRenderParams;
rp.reset();
rp.lighting = state.lighting;
const textureScroll = this.agData.textureScroll;
if (textureScroll && (this.stageData.gameSource === "smb2" || this.stageData.gameSource === "mb2ws")) {
const timeSeconds = state.time.getAnimTimeSeconds();
vec3.set(scratchVec3b, textureScroll.speed[0] * timeSeconds, textureScroll.speed[1] * timeSeconds, 0);
mat4.fromTranslation(rp.texMtx, scratchVec3b);
}
const viewFromAnimGroup = scratchMat4a;
mat4.mul(viewFromAnimGroup, viewFromWorld, this.worldFromAg);

View File

@@ -114,6 +114,18 @@ export class BgObjectInst {
if (texMtx !== undefined) {
mat4.copy(renderParams.texMtx, texMtx);
}
const textureScroll = this.bgObjectData.textureScroll;
if (textureScroll) {
const timeSeconds = state.time.getAnimTimeSeconds();
const scroll = scratchVec3c;
vec3.set(scroll, textureScroll.speed[0] * timeSeconds, textureScroll.speed[1] * timeSeconds, 0);
if (texMtx !== undefined) {
mat4.fromTranslation(scratchMat4a, scroll);
mat4.mul(renderParams.texMtx, renderParams.texMtx, scratchMat4a);
} else {
mat4.fromTranslation(renderParams.texMtx, scroll);
}
}
mat4.mul(renderParams.viewFromModel, viewMatrix, this.worldFromModel);

View File

@@ -117,6 +117,7 @@ export enum TevLayerFlags {
TypeWorldSpecular = 1 << 15,
Unk16 = 1 << 16,
EnableUvScroll = 1 << 17,
}
export type TevLayer = {

View File

@@ -77,10 +77,16 @@ function fillWorldSpecularMapping(renderCache: GfxRenderCache, mapping: Material
mapping.height = WORLD_SPECULAR_TEX_HEIGHT;
}
function buildDiffuseLayer(mb: GXMaterialBuilder, state: BuildState, colorIn: GX.CC, alphaIn: GX.CA) {
function buildDiffuseLayer(
mb: GXMaterialBuilder,
state: BuildState,
colorIn: GX.CC,
alphaIn: GX.CA,
texGenMatrix: GX.TexGenMatrix
) {
mb.setTevDirect(state.stage);
mb.setTevSwapMode(state.stage, SWAP_TABLES[0], SWAP_TABLES[0]);
mb.setTexCoordGen(state.texCoord, GX.TexGenType.MTX2x4, state.texGenSrc, GX.TexGenMatrix.TEXMTX1);
mb.setTexCoordGen(state.texCoord, GX.TexGenType.MTX2x4, state.texGenSrc, texGenMatrix);
mb.setTevOrder(state.stage, state.texCoord, state.texMap, GX.RasColorChannelID.COLOR0A0);
mb.setTevColorIn(state.stage, GX.CC.ZERO, GX.CC.TEXC, colorIn, GX.CC.ZERO);
@@ -162,10 +168,16 @@ function buildWorldSpecularLayer(mb: GXMaterialBuilder, state: BuildState, color
state.texGenSrc++;
}
function buildAlphaBlendLayer(mb: GXMaterialBuilder, state: BuildState, colorIn: GX.CC, alphaIn: GX.CA) {
function buildAlphaBlendLayer(
mb: GXMaterialBuilder,
state: BuildState,
colorIn: GX.CC,
alphaIn: GX.CA,
texGenMatrix: GX.TexGenMatrix
) {
mb.setTevDirect(state.stage);
mb.setTevSwapMode(state.stage, SWAP_TABLES[0], SWAP_TABLES[1]);
mb.setTexCoordGen(state.texCoord, GX.TexGenType.MTX2x4, state.texGenSrc, GX.TexGenMatrix.TEXMTX1);
mb.setTexCoordGen(state.texCoord, GX.TexGenType.MTX2x4, state.texGenSrc, texGenMatrix);
mb.setTevOrder(state.stage, state.texCoord, state.texMap, GX.RasColorChannelID.COLOR0A0);
mb.setTevColorIn(state.stage, GX.CC.ZERO, GX.CC.ZERO, GX.CC.ZERO, colorIn);
@@ -397,15 +409,19 @@ export class MaterialInst {
} else {
for (let layerIdx = 0; layerIdx < this.tevLayers.length; layerIdx++) {
const layer = this.tevLayers[layerIdx];
const texGenMatrix =
layer.tevLayerData.flags & Gma.TevLayerFlags.EnableUvScroll
? GX.TexGenMatrix.TEXMTX1
: GX.TexGenMatrix.TEXMTX2;
const layerTypeFlags =
layer.tevLayerData.flags &
(Gma.TevLayerFlags.TypeAlphaBlend |
Gma.TevLayerFlags.TypeViewSpecular |
Gma.TevLayerFlags.TypeWorldSpecular);
if (layerTypeFlags === 0) {
buildDiffuseLayer(mb, buildState, colorIn, alphaIn);
buildDiffuseLayer(mb, buildState, colorIn, alphaIn, texGenMatrix);
} else if (layerTypeFlags & Gma.TevLayerFlags.TypeAlphaBlend) {
buildAlphaBlendLayer(mb, buildState, colorIn, alphaIn);
buildAlphaBlendLayer(mb, buildState, colorIn, alphaIn, texGenMatrix);
} else if (layerTypeFlags & Gma.TevLayerFlags.TypeViewSpecular) {
buildViewSpecularLayer(mb, buildState, colorIn, alphaIn);
} else if (layerTypeFlags & Gma.TevLayerFlags.TypeWorldSpecular) {
@@ -497,6 +513,7 @@ export class MaterialInst {
materialColor.a *= colorMul.a;
mat4.copy(materialParams.u_TexMtx[1], renderParams.texMtx);
mat4.identity(materialParams.u_TexMtx[2]);
ambientColor.r *= colorMul.r;
ambientColor.g *= colorMul.g;

View File

@@ -117,6 +117,11 @@ export type BgObject = {
translucency: number;
anim: BgAnim | null;
flipbookAnims: FlipbookAnims | null;
textureScroll?: TextureScroll;
};
export type TextureScroll = {
speed: vec2;
};
// export type TextureScroll = {
@@ -201,6 +206,7 @@ export type AnimGroup = {
originRot: vec3;
animType: AnimType;
anim: AnimGroupAnim | null;
textureScroll?: TextureScroll;
// conveyorVel: vec3;
coliTris: ColiTri[];

View File

@@ -658,6 +658,7 @@ function convertBgObject(obj: any): BgObject {
translucency: obj.translucency ?? 0,
anim: convertBgAnim(obj.anim),
flipbookAnims: convertFlipbookAnims(obj.flipbooks),
textureScroll: obj.textureScroll ? { speed: toVec2(obj.textureScroll.speed) } : undefined,
};
}
@@ -713,6 +714,7 @@ export function convertSmb2StageDef(stage: any): Stage {
posZKeyframes: keyframesOrEmpty(group.anim.posZKeyframes),
}
: null,
textureScroll: group.textureScroll ? { speed: toVec2(group.textureScroll.speed) } : undefined,
coliTris: group.triangles.map((tri: any) => ({
pos: toVec3(tri.pos),
normal: toVec3(tri.normal),

View File

@@ -48,6 +48,7 @@ const STAGE_CYLINDER_SIZE = 0x1c;
const STAGE_ANIM_GROUP_MODEL_SIZE = 0x0c;
const STAGE_FALLOUT_BOX_SIZE = 0x20;
const STAGE_BG_OBJECT_SIZE = 0x38;
const STAGE2_BG_OBJECT_SIZE = 0x38;
const STAGE_BG_ANIM_SIZE = 0x60;
const STAGE_FLIPBOOK_SIZE = 0x10;
const STAGE_NIGHT_WINDOW_SIZE = 0x14;
@@ -208,6 +209,16 @@ class StageParser {
return TEXT_DECODER.decode(this.data.subarray(offset, end));
}
parseTextureScroll(offset) {
if (offset === null) {
return null;
}
if (offset < 0 || offset + 8 > this.data.length) {
return null;
}
return { speed: this.readVec2(offset) };
}
readStringList(offset) {
if (offset === null) {
return [];
@@ -275,6 +286,9 @@ class StageParser {
if (offset === null) {
return null;
}
if (offset < 0 || offset + STAGE_BG_ANIM_SIZE > this.data.length) {
return null;
}
const anim = {
loopStartSeconds: this.readF32(offset),
loopEndSeconds: this.readF32(offset + 0x04),
@@ -323,6 +337,9 @@ class StageParser {
if (offset === null) {
return null;
}
if (offset < 0 || offset + STAGE_FLIPBOOK_SIZE > this.data.length) {
return null;
}
const nightWindowAnimCount = this.readS32(offset);
const nightWindowAnimsPtr = this.readPtr(offset + 0x04);
const stormFireAnimCount = this.readS32(offset + 0x08);
@@ -888,6 +905,40 @@ class StageParserSmb2 extends StageParser {
return names;
}
parseBgObjects(offset, count) {
if (offset === null || count <= 0) {
return [];
}
if (offset < 0 || offset >= this.data.length) {
return [];
}
const maxCount = Math.floor((this.data.length - offset) / STAGE2_BG_OBJECT_SIZE);
const safeCount = Math.min(count, maxCount);
const objs = new Array(safeCount);
for (let i = 0; i < safeCount; i += 1) {
const base = offset + i * STAGE2_BG_OBJECT_SIZE;
const namePtr = this.readPtr(base + 0x04);
const effectHeaderPtr = this.readPtr(base + 0x34);
const textureScroll = effectHeaderPtr !== null
? this.parseTextureScroll(this.readPtr(effectHeaderPtr + 0x10))
: null;
objs[i] = {
flags: this.readU32(base),
name: namePtr ? this.readString(namePtr) : '',
pos: this.readVec3(base + 0x0c),
rotX: this.readS16(base + 0x18),
rotY: this.readS16(base + 0x1a),
rotZ: this.readS16(base + 0x1c),
scale: this.readVec3(base + 0x20),
translucency: this.readF32(base + 0x2c),
anim: this.parseBgAnim(this.readPtr(base + 0x30)),
flipbooks: this.parseFlipbooks(this.readPtr(base + 0x34)),
textureScroll,
};
}
return objs;
}
parseAnimGroup(offset) {
const origin = this.readVec3(offset);
const initRot = this.readS16Vec(offset + 0x0c);
@@ -936,7 +987,8 @@ class StageParserSmb2 extends StageParser {
const initialPlaybackState = this.readS32(offset + 0xcc);
const loopStartSeconds = this.readF32(offset + 0xd0);
const loopEndSeconds = this.readF32(offset + 0xd4);
const textureScroll = this.readPtr(offset + 0xd8);
const textureScrollPtr = this.readPtr(offset + 0xd8);
const textureScroll = this.parseTextureScroll(textureScrollPtr);
const gridData = this.parseGridCellTris(gridCellTrisPtr, gridCellCountX, gridCellCountZ);
const triangleCount = gridData.maxIndex + 1;