From ab10965f8231a0a2f813fb90402124e7ee12d87f Mon Sep 17 00:00:00 2001 From: Mike Lester Date: Fri, 3 Jan 2025 17:07:22 -0700 Subject: [PATCH] Wind Waker: Particle system improvements (#739) * Wind Waker: Split dPa_control_c init into common and scene * Wind Waker: Replace Particle.ts EffectDrawGroup with ParticleGroup * JPA: Fix misplaced `workData.volumeEmitCount = this.emitCount` Now `workData.volumeEmitCount` is always valid when it should be * Wind Waker: Introducing "simple" emitters The game uses these to consolidate commonly used emitters, such as flames. This fixes a big long-standing TODO in d_particle.ts, but the main goal is to fix the HACK related to indirect/projection particles * Wind Waker: "Simple" particles can now access the framebuffer This is done almost the same as the game, but we modify the TEV settings at emitter creation time rather than at draw time (because generating the material is expensive) * Wind Waker: Fix common particles spawning once on scene creation * Formatting improvements * Wind Waker: d_a_ep now uses simple particles * Wind Waker: Small note regarding simple particles ignoring emitter translation * Wind Waker: Remove indirect particle hack Particles now keep the groupID that was assigned to them at creation. Projection particles are determined at simple emitter creation time, or if ParticleGroup.Projection is specified manually. * Wind Waker: Simple particle emitters are cleared every frame Actors such as d_a_ep must call `setSimple()` each frame * Wind Waker: Fixup d_a_ep (torch) actor. The default state now matches the game. * Wind Waker: Allow type 2 d_a_ep to render flames These should be handled by the d_a_lamp actor (e.g. in Beedle's ship shop), but that is not yet implemented. For now, let's handle them in d_a_ep * Wind Waker: Explicit return types for d_particle functions * Wind Waker: Fixed TODO - d_a_obj_flame now uses simple particle emitters * Wind Waker: Remove old `patchResData` indirect particles hack This TEV patching is now done in a more proper place, inside the the simple particle callback * Wind Waker: Add frustum culling for simple emitters * Wind Waker: Don't distance cull 2D/UI particle emitters * J2D: Add `J2DGrafContext.getFrustumForView()` Given a view matrix, return a Frustum which can be used for culling. In the original game, frustums are generated from proj matrices alone. Points were transformed by the view matrix before being tested against the frustum. But noclip typically expects viewProj frustums. * Wind Waker: Draw 2D/UI particle groups with ortho view/proj matrices * Wind Waker: Implement first 2D emitter for d_a_title * Wind Waker: Add sparkle emitter to d_a_title * Wind Waker: Add new drawlists for fore/background 2D particles This aligns with the game and fixes sorting issues with the title screen emitters * Wind Waker: Don't frustum cull 2D particles * Wind Waker: Use computeModelMatrixT to avoid list creation --- src/Common/JSYSTEM/J2Dv1.ts | 3 +- src/Common/JSYSTEM/JPA.ts | 5 +- src/ZeldaWindWaker/Main.ts | 42 +++++-- src/ZeldaWindWaker/d_a.ts | 155 +++++++++++++++++------ src/ZeldaWindWaker/d_drawlist.ts | 3 + src/ZeldaWindWaker/d_particle.ts | 208 ++++++++++++++++++++++++------- 6 files changed, 313 insertions(+), 103 deletions(-) diff --git a/src/Common/JSYSTEM/J2Dv1.ts b/src/Common/JSYSTEM/J2Dv1.ts index 1b6687f75..576fb1b5f 100644 --- a/src/Common/JSYSTEM/J2Dv1.ts +++ b/src/Common/JSYSTEM/J2Dv1.ts @@ -17,6 +17,7 @@ import { BTIData } from "./JUTTexture.js"; import { GXMaterialBuilder } from "../../gx/GXMaterialBuilder.js"; import { mat4, vec2, vec4 } from "gl-matrix"; import { GfxRenderCache } from "../../gfx/render/GfxRenderCache.js"; +import { Frustum } from "../../Geometry.js"; const materialParams = new MaterialParams(); const drawParams = new DrawParams(); @@ -83,7 +84,6 @@ export class J2DGrafContext { public sceneParams = new SceneParams(); public viewport = vec4.create(); public ortho = vec4.create(); - public near: number; public far: number; @@ -118,6 +118,7 @@ export class J2DGrafContext { projectionMatrixForCuboid(this.sceneParams.u_Projection, left, right, bottom, top, this.near, this.far); projectionMatrixConvertClipSpaceNearZ(this.sceneParams.u_Projection, this.clipSpaceNearZ, GfxClipSpaceNearZ.NegativeOne); + } public setOnRenderInst(renderInst: GfxRenderInst): void { diff --git a/src/Common/JSYSTEM/JPA.ts b/src/Common/JSYSTEM/JPA.ts index 18c120d6f..7af2c29c0 100644 --- a/src/Common/JSYSTEM/JPA.ts +++ b/src/Common/JSYSTEM/JPA.ts @@ -1711,7 +1711,7 @@ export class JPABaseEmitter { throw "whoops"; } - private createParticle(): JPABaseParticle | null { + public createParticle(): JPABaseParticle | null { if (this.emitterManager.deadParticlePool.length === 0) return null; @@ -1736,7 +1736,6 @@ export class JPABaseEmitter { this.emitCount = bem1.divNumber * bem1.divNumber * 4 + 2; else this.emitCount = bem1.divNumber; - workData.volumeEmitCount = this.emitCount; workData.volumeEmitIdx = 0; } else { // Rate @@ -1748,6 +1747,8 @@ export class JPABaseEmitter { this.emitCount = 1; } + workData.volumeEmitCount = this.emitCount; + if (!!(this.status & JPAEmitterStatus.STOP_CREATE_PARTICLE)) this.emitCount = 0; diff --git a/src/ZeldaWindWaker/Main.ts b/src/ZeldaWindWaker/Main.ts index 91a6c22e5..707c54d8a 100644 --- a/src/ZeldaWindWaker/Main.ts +++ b/src/ZeldaWindWaker/Main.ts @@ -17,7 +17,7 @@ import { J3DModelInstance } from '../Common/JSYSTEM/J3D/J3DGraphBase.js'; import * as JPA from '../Common/JSYSTEM/JPA.js'; import { BTIData } from '../Common/JSYSTEM/JUTTexture.js'; import { dfRange } from '../DebugFloaters.js'; -import { range } from '../MathHelpers.js'; +import { computeModelMatrixT, range } from '../MathHelpers.js'; import { SceneContext } from '../SceneBase.js'; import { TextureMapping } from '../TextureHolder.js'; import { setBackbufferDescSimple, standardFullClearRenderPassDescriptor } from '../gfx/helpers/RenderGraphHelpers.js'; @@ -35,7 +35,7 @@ import { EDemoMode, dDemo_manager_c } from './d_demo.js'; import { dDlst_list_Set, dDlst_list_c } from './d_drawlist.js'; import { dKankyo_create, dKy__RegisterConstructors, dKy_setLight, dScnKy_env_light_c } from './d_kankyo.js'; import { dKyw__RegisterConstructors } from './d_kankyo_wether.js'; -import { dPa_control_c } from './d_particle.js'; +import { dPa_control_c, ParticleGroup } from './d_particle.js'; import { Placename, PlacenameState, dPn__update, d_pn__RegisterConstructors } from './d_place_name.js'; import { dProcName_e } from './d_procname.js'; import { ResType, dRes_control_c } from './d_resorce.js'; @@ -403,18 +403,37 @@ export class WindWakerRenderer implements Viewer.SceneGfx { { globals.particleCtrl.calc(globals, viewerInput); - for (let group = EffectDrawGroup.Main; group <= EffectDrawGroup.Indirect; group++) { + for (let group = ParticleGroup.Normal; group <= ParticleGroup.Wind; group++) { let texPrjMtx: mat4 | null = null; - if (group === EffectDrawGroup.Indirect) { + if (group === ParticleGroup.Projection) { texPrjMtx = scratchMatrix; texProjCameraSceneTex(texPrjMtx, globals.camera.clipFromViewMatrix, 1); } globals.particleCtrl.setDrawInfo(globals.camera.viewFromWorldMatrix, globals.camera.clipFromViewMatrix, texPrjMtx, globals.camera.frustum); - renderInstManager.setCurrentList(dlst.effect[group]); + renderInstManager.setCurrentList(dlst.effect[group == ParticleGroup.Projection ? EffectDrawGroup.Indirect : EffectDrawGroup.Main]); globals.particleCtrl.draw(device, this.renderHelper.renderInstManager, group); } + + // From mDoGph_Painter(). Draw the 2D particle groups with different view/proj matrices. + { + const orthoCtx = this.globals.scnPlay.currentGrafPort; + const template = renderInstManager.pushTemplate(); + orthoCtx.setOnRenderInst(template); + + const viewMtx = scratchMatrix; + computeModelMatrixT(viewMtx, orthoCtx.aspectRatioCorrection * 320, 240, 0); + globals.particleCtrl.setDrawInfo(viewMtx, orthoCtx.sceneParams.u_Projection, null, null); + + renderInstManager.setCurrentList(dlst.particle2DBack); + globals.particleCtrl.draw(device, this.renderHelper.renderInstManager, ParticleGroup.TwoDback); + globals.particleCtrl.draw(device, this.renderHelper.renderInstManager, ParticleGroup.TwoDmenuBack); + + renderInstManager.setCurrentList(dlst.particle2DFore); + globals.particleCtrl.draw(device, this.renderHelper.renderInstManager, ParticleGroup.TwoDfore); + globals.particleCtrl.draw(device, this.renderHelper.renderInstManager, ParticleGroup.TwoDmenuFore); + } } this.renderHelper.renderInstManager.popTemplate(); @@ -481,9 +500,11 @@ export class WindWakerRenderer implements Viewer.SceneGfx { this.executeList(passRenderer, dlst.effect[EffectDrawGroup.Main]); this.executeList(passRenderer, dlst.wetherEffect); - this.executeListSet(passRenderer, dlst.ui); + this.executeList(passRenderer, dlst.particle2DBack); + this.executeListSet(passRenderer, dlst.ui); this.executeListSet(passRenderer, dlst.ui2D); + this.executeList(passRenderer, dlst.particle2DFore); }); }); @@ -823,12 +844,9 @@ class SceneDesc { renderer.extraTextures = new ZWWExtraTextures(device, ZAtoon, ZBtoonEX); - const jpac: JPA.JPAC[] = []; - for (let i = 0; i < particleArchives.length; i++) { - const jpacData = modelCache.getFileData(particleArchives[i]); - jpac.push(JPA.parse(jpacData)); - } - globals.particleCtrl = new dPa_control_c(renderer.renderCache, jpac); + globals.particleCtrl = new dPa_control_c(renderer.renderCache); + globals.particleCtrl.createCommon(globals, JPA.parse(modelCache.getFileData(particleArchives[0]))); + globals.particleCtrl.createRoomScene(globals, JPA.parse(modelCache.getFileData(particleArchives[1]))); // dStage_Create dKankyo_create(globals); diff --git a/src/ZeldaWindWaker/d_a.ts b/src/ZeldaWindWaker/d_a.ts index 180b12df7..e9d959a8d 100644 --- a/src/ZeldaWindWaker/d_a.ts +++ b/src/ZeldaWindWaker/d_a.ts @@ -30,7 +30,7 @@ import { PeekZResult } from "./d_dlst_peekZ.js"; import { dDlst_alphaModel__Type } from "./d_drawlist.js"; import { LIGHT_INFLUENCE, LightType, WAVE_INFO, dKy_change_colpat, dKy_checkEventNightStop, dKy_plight_cut, dKy_plight_set, dKy_setLight__OnMaterialParams, dKy_setLight__OnModelInstance, dKy_tevstr_c, dKy_tevstr_init, setLightTevColorType, settingTevStruct } from "./d_kankyo.js"; import { ThunderMode, dKyr_get_vectle_calc, dKyw_get_AllWind_vecpow, dKyw_get_wind_pow, dKyw_get_wind_vec, dKyw_rain_set, loadRawTexture } from "./d_kankyo_wether.js"; -import { dPa_splashEcallBack, dPa_trackEcallBack, dPa_waveEcallBack } from "./d_particle.js"; +import { dPa_splashEcallBack, dPa_trackEcallBack, dPa_waveEcallBack, ParticleGroup } from "./d_particle.js"; import { dProcName_e } from "./d_procname.js"; import { ResType, dComIfG_resLoad } from "./d_resorce.js"; import { dPath, dPath_GetRoomPath, dPath__Point, dStage_Multi_c, dStage_stagInfo_GetSTType } from "./d_stage.js"; @@ -217,6 +217,11 @@ class d_a_ep extends fopAc_ac_c { private lightPower: number = 0.0; private lightPowerTarget: number = 0.0; + private burstEmitter: JPABaseEmitter | null = null; + private burstRotY: number = 0; + private burstRotZ: number = 0; + private burstTimer = 0; + private timers = nArray(3, () => 0); private alphaModelMtx = mat4.create(); private alphaModelRotX = 0; @@ -246,20 +251,6 @@ class d_a_ep extends fopAc_ac_c { dKy_plight_set(globals.g_env_light, this.light); - // Create particle systems. - - // TODO(jstpierre): Implement the real thing. - const pa = globals.particleCtrl.set(globals, 0, 0x0001, null)!; - vec3.copy(pa.globalTranslation, this.posTop); - pa.globalTranslation[1] += -240 + 235 + 15; - if (this.type !== 2) { - const pb = globals.particleCtrl.set(globals, 0, 0x4004, null)!; - vec3.copy(pb.globalTranslation, pa.globalTranslation); - pb.globalTranslation[1] += 20; - } - const pc = globals.particleCtrl.set(globals, 0, 0x01EA, null)!; - vec3.copy(pc.globalTranslation, this.posTop); - pc.globalTranslation[1] += -240 + 235 + 8; // TODO(jstpierre): ga return cPhs__Status.Next; @@ -308,8 +299,8 @@ class d_a_ep extends fopAc_ac_c { } } - this.alphaModelAlpha = cLib_addCalc2(this.alphaModelAlpha, this.alphaModelAlphaTarget, 1.0, 1.0); - this.alphaModelScale = cLib_addCalc2(this.alphaModelScale, this.alphaModelScaleTarget, 0.4, 0.04); + this.alphaModelAlpha = cLib_addCalc2(this.alphaModelAlpha, this.alphaModelAlphaTarget, 1.0 * deltaTimeFrames, 1.0); + this.alphaModelScale = cLib_addCalc2(this.alphaModelScale, this.alphaModelScaleTarget, 0.4 * deltaTimeFrames, 0.04); MtxTrans(this.posTop, false); mDoMtx_YrotM(calc_mtx, this.alphaModelRotY); mDoMtx_XrotM(calc_mtx, this.alphaModelRotX); @@ -317,10 +308,9 @@ class d_a_ep extends fopAc_ac_c { vec3.set(scratchVec3a, scale, scale, scale); mat4.scale(calc_mtx, calc_mtx, scratchVec3a); mat4.copy(this.alphaModelMtx, calc_mtx); + this.ep_move(globals, deltaTimeFrames); this.alphaModelRotY += 0xD0 * deltaTimeFrames; this.alphaModelRotX += 0x100 * deltaTimeFrames; - - this.ep_move(); } public override delete(globals: dGlobals): void { @@ -349,18 +339,27 @@ class d_a_ep extends fopAc_ac_c { // TODO(jstpierre): ga } - private ep_move(): void { + private ep_move(globals: dGlobals, deltaTimeFrames: number): void { + + const flamePos = vec3.set(scratchVec3a, this.posTop[0], this.posTop[1] + -240 + 235 + 15, this.posTop[2]); + // tons of fun timers and such if (this.state === 0) { // check switches this.state = 3; this.lightPowerTarget = this.scale[0]; } else if (this.state === 3 || this.state === 4) { - this.lightPower = cLib_addCalc2(this.lightPower, this.lightPowerTarget, 0.5, 0.2); - if (this.type !== 2) { - // check a bunch of stuff, collision, etc. - // setSimple 0x4004 + this.lightPower = cLib_addCalc2(this.lightPower, this.lightPowerTarget, 0.5 * deltaTimeFrames, 0.2); + + // TODO: Type 2 flames should be handled by d_a_lamp, but for now lets just handle them here + if (true || this.type !== 2) { + if (this.burstTimer < 7) globals.particleCtrl.setSimple(0x0001, flamePos, 0xFF, White, White, false); + // Check for collision. If hit, set the burst timer to emit a quick burst of flame + flamePos[1] += 20; + globals.particleCtrl.setSimple(0x4004, flamePos, 0xFF, White, White, false); } + + // check a bunch of stuff, collision, etc. } vec3.copy(this.light.pos, this.posTop); @@ -370,7 +369,28 @@ class d_a_ep extends fopAc_ac_c { this.light.power = this.lightPower * 150.0; this.light.fluctuation = 250.0; - // other emitter stuff + // When hit with an attack, emit a quick burst of flame before returning to normal + if (this.burstTimer >= 0) { + if (this.burstTimer == 0x28 && !this.burstEmitter) { + const pos = vec3.set(scratchVec3a, this.posTop[0], this.posTop[1] + -240 + 235 + 8, this.posTop[2]); + this.burstEmitter = globals.particleCtrl.set(globals, 0, 0x01EA, pos)!; + } + if (this.burstEmitter) { + mDoMtx_YrotS(scratchMat4a, this.burstRotY); + const target = (this.burstTimer > 10) ? 4.0 : 0.0; + this.burstRotZ = cLib_addCalc2(this.burstRotZ, target, 1.0 * deltaTimeFrames, 0.5) + const emitterDir = vec3.set(scratchVec3b, 0.0, 1.0, this.burstRotZ); + MtxPosition(emitterDir, emitterDir, scratchMat4a); + vec3.copy(this.burstEmitter.localDirection, emitterDir); + + if (this.burstTimer <= 1.0) { + this.burstEmitter.maxFrame = -1; + this.burstEmitter.stopCreateParticle(); + this.burstEmitter = null; + } + } + this.burstTimer -= deltaTimeFrames; + } } } @@ -4430,19 +4450,38 @@ class d_a_obj_flame extends fopAc_ac_c { } private em_simple_set(globals: dGlobals): void { - /* if (this.em0State === d_a_obj_em_state.TurnOn) { vec3.copy(scratchVec3a, this.eyePos); scratchVec3a[1] += this.extraScaleY * this.eyePosY * -300.0; - globals.particleCtrl.setSimple(globals, 0x805A, scratchVec3a, 1.0, White, White, false); + globals.particleCtrl.setSimple(0x805A, scratchVec3a, 0xFF, White, White, false); } if (this.em1State === d_a_obj_em_state.TurnOn) - globals.particleCtrl.setSimple(globals, 0x805B, this.eyePos, 1.0, White, White, false); + globals.particleCtrl.setSimple(0x805B, this.eyePos, 0xFF, White, White, false); if (this.em2State === d_a_obj_em_state.TurnOn) - globals.particleCtrl.setSimple(globals, this.bubblesParticleID, this.eyePos, 1.0, White, White, false); - */ + globals.particleCtrl.setSimple(this.bubblesParticleID, this.pos, 0xFF, White, White, false); + } + + private em_simple_inv(globals: dGlobals): void { + const forceKillEm = false; + if (forceKillEm) { + if (this.em0State === d_a_obj_em_state.On) + this.em0State = d_a_obj_em_state.TurnOff; + if (this.em2State === d_a_obj_em_state.On) + this.em2State = d_a_obj_em_state.TurnOff; + } + + if (this.em0State === d_a_obj_em_state.TurnOff) { + this.em0 = null; + } + if (this.em1State === d_a_obj_em_state.TurnOff) { + this.em1 = null; + } + if (this.em2State !== d_a_obj_em_state.TurnOff) { + return; + } + this.em2 = null; } private mode_proc_call(globals: dGlobals, deltaTimeFrames: number): void { @@ -4452,11 +4491,10 @@ class d_a_obj_flame extends fopAc_ac_c { this.mode_proc_tbl[this.mode].call(this, globals); - // TODO(jstpierre): Simple particle system - if (false && this.useSimpleEm) { + if (this.useSimpleEm) { this.em_position(globals); this.em_simple_set(globals); - // this.em_simple_inv(globals); + this.em_simple_inv(globals); } else { this.em_manual_set(globals); this.em_manual_inv(globals); @@ -5759,7 +5797,7 @@ const enum TitlePane { JapanSubtitle, PressStart, Nintendo, - Effect1, + ShipParticles, Effect2, } @@ -5775,7 +5813,11 @@ class d_a_title extends fopAc_ac_c { private btkSubtitle = new mDoExt_btkAnm(); private btkShimmer = new mDoExt_btkAnm(); private screen: J2DScreen; - private panes: J2DPane[] = []; + private panes: J2DPane[] = []; + + private cloudEmitter: JPABaseEmitter | null = null; + private sparkleEmitter: JPABaseEmitter | null = null; + private sparklePos = vec3.create(); private anmFrameCounter = 0 private delayFrameCounter = 120; @@ -5804,7 +5846,7 @@ class d_a_title extends fopAc_ac_c { // TODO: mDoAud_seStart(JA_SE_TITLE_WIND); } } else { - this.calc_2d_alpha(deltaTimeFrames); + this.calc_2d_alpha(globals, deltaTimeFrames); } if (this.enterMode == 2) { @@ -5932,18 +5974,28 @@ class d_a_title extends fopAc_ac_c { mDoMtx_ZXYrotM(this.modelSubtitleShimmer.modelMatrix, [0, -0x8000, 0]); } - private calc_2d_alpha(deltaTimeFrames: number) { + private calc_2d_alpha(globals: dGlobals, deltaTimeFrames: number) { this.anmFrameCounter += deltaTimeFrames; if (this.anmFrameCounter >= 200 && this.enterMode == 0) { this.enterMode = 1; } + const puffPos = vec3.set(scratchVec3a, + ((this.panes[TitlePane.ShipParticles].data.x - 320.0) - this.shipOffsetX) + 85.0, + (this.panes[TitlePane.ShipParticles].data.y - 240.0) + 5.0, + 0.0 + ); + if (this.enterMode == 0) { if (this.shipFrameCounter < 0) { this.shipFrameCounter += deltaTimeFrames; - } + } - // TODO: Emitters + if (this.cloudEmitter === null) { + this.cloudEmitter = globals.particleCtrl.set(globals, ParticleGroup.TwoDback, 0x83F9, puffPos); + } else { + this.cloudEmitter.setGlobalTranslation(puffPos); + } if (this.anmFrameCounter <= 30) { this.panes[TitlePane.MainTitle].setAlpha(0.0); @@ -5956,7 +6008,19 @@ class d_a_title extends fopAc_ac_c { // TODO: Viewable japanese version this.panes[TitlePane.JapanSubtitle].setAlpha(0.0); - // TODO: Emitters + if (this.anmFrameCounter >= 80 && !this.sparkleEmitter) { + // if (daTitle_Kirakira_Sound_flag == true) { + // mDoAud_seStart(JA_SE_TITLE_KIRA); + // daTitle_Kirakira_Sound_flag = false; + // } + + const sparklePane = this.panes[TitlePane.ShipParticles]; + vec3.set(this.sparklePos, sparklePane.data.x - 320.0, sparklePane.data.y - 240.0, 0.0); + this.sparkleEmitter = globals.particleCtrl.set(globals, ParticleGroup.TwoDfore, 0x83FB, this.sparklePos); + } else if (this.anmFrameCounter > 80 && this.anmFrameCounter <= 115 && this.sparkleEmitter) { + this.sparklePos[0] += (this.panes[TitlePane.Effect2].data.x - this.panes[TitlePane.ShipParticles].data.x) / 35.0 * deltaTimeFrames; + this.sparkleEmitter.setGlobalTranslation(this.sparklePos); + } if (this.anmFrameCounter >= 80) { this.btkSubtitle.play(deltaTimeFrames); @@ -5978,11 +6042,20 @@ class d_a_title extends fopAc_ac_c { this.panes[TitlePane.PressStart].setAlpha(1.0); } } else { - // TODO: Emitters + if (this.cloudEmitter === null) { + this.cloudEmitter = globals.particleCtrl.set(globals, ParticleGroup.TwoDback, 0x83F9, puffPos); + } else { + this.cloudEmitter.setGlobalTranslation(puffPos); + } this.panes[TitlePane.MainTitle].setAlpha(1.0); this.panes[TitlePane.JapanSubtitle].setAlpha(0.0); + if (this.sparkleEmitter) { + this.sparkleEmitter.becomeInvalidEmitter(); + this.sparkleEmitter = null; + } + this.btkSubtitle.frameCtrl.setFrame(this.btkSubtitle.frameCtrl.endFrame); this.panes[TitlePane.Nintendo].setAlpha(1.0); if (this.blinkFrameCounter >= 100) { diff --git a/src/ZeldaWindWaker/d_drawlist.ts b/src/ZeldaWindWaker/d_drawlist.ts index 1f625413f..d5120bf13 100644 --- a/src/ZeldaWindWaker/d_drawlist.ts +++ b/src/ZeldaWindWaker/d_drawlist.ts @@ -205,6 +205,9 @@ export class dDlst_list_c { new GfxRenderInstList(gfxRenderInstCompareNone, GfxRenderInstExecutionOrder.Forwards) ]; + public particle2DBack = new GfxRenderInstList(gfxRenderInstCompareNone, GfxRenderInstExecutionOrder.Forwards); + public particle2DFore = new GfxRenderInstList(gfxRenderInstCompareNone, GfxRenderInstExecutionOrder.Forwards); + public alphaModel = new GfxRenderInstList(gfxRenderInstCompareNone, GfxRenderInstExecutionOrder.Forwards); public peekZ = new PeekZManager(128); public alphaModel0: dDlst_alphaModel_c; diff --git a/src/ZeldaWindWaker/d_particle.ts b/src/ZeldaWindWaker/d_particle.ts index 4b6e092d1..0161c261e 100644 --- a/src/ZeldaWindWaker/d_particle.ts +++ b/src/ZeldaWindWaker/d_particle.ts @@ -2,7 +2,7 @@ // particle import { mat4, ReadonlyMat4, ReadonlyVec3, vec2, vec3 } from "gl-matrix"; -import { Color, colorCopy } from "../Color.js"; +import { Color, colorCopy, colorNewCopy } from "../Color.js"; import { JPABaseEmitter, JPAEmitterManager, JPAResourceData, JPAEmitterCallBack, JPADrawInfo, JPACData, JPAC, JPAResourceRaw } from "../Common/JSYSTEM/JPA.js"; import { Frustum } from "../Geometry.js"; import { GfxDevice } from "../gfx/platform/GfxPlatform.js"; @@ -11,7 +11,7 @@ import { EFB_HEIGHT, EFB_WIDTH } from "../gx/gx_material.js"; import { computeModelMatrixR, getMatrixTranslation, saturate, transformVec3Mat4w0 } from "../MathHelpers.js"; import { TDDraw } from "../SuperMarioGalaxy/DDraw.js"; import { TextureMapping } from "../TextureHolder.js"; -import { nArray } from "../util.js"; +import { assert, nArray } from "../util.js"; import { ViewerRenderInput } from "../viewer.js"; import { dKy_get_seacolor } from "./d_kankyo.js"; import { cLib_addCalc2, cM_s2rad } from "./SComponent.js"; @@ -21,6 +21,16 @@ import { ColorKind } from "../gx/gx_render.js"; import { gfxDeviceNeedsFlipY } from "../gfx/helpers/GfxDeviceHelpers.js"; import { GfxRenderCache } from "../gfx/render/GfxRenderCache.js"; +// Simple common particles +const j_o_id: number[] = [ 0x0000, 0x0001, 0x0002, 0x0003, 0x03DA, 0x03DB, 0x03DC, 0x4004 ]; + +// Simple scene particles +const s_o_id: number[] = [ + 0x8058, 0x8059, 0x805A, 0x805B, 0x805C, 0x8221, 0x8222, 0x8060, 0x8061, 0x8062, 0x8063, 0x8064, 0x8065, 0x8066, + 0x8067, 0x8068, 0x8069, 0x81D5, 0x8240, 0x8241, 0x8306, 0x8407, 0x8408, 0x8409, 0x8443, 0x840A, 0x840B, 0x840C, + 0x840D, 0x840E, 0x840F, 0xA410, 0xA06A, 0xC06B, +]; + export abstract class dPa_levelEcallBack extends JPAEmitterCallBack { constructor(protected globals: dGlobals) { super(); @@ -30,9 +40,18 @@ export abstract class dPa_levelEcallBack extends JPAEmitterCallBack { } } -const enum EffectDrawGroup { - Main = 0, - Indirect = 1, +export const enum ParticleGroup { + Normal, + NormalP1, + Toon, + ToonP1, + Projection, + ShipTail, + Wind, + TwoDfore, + TwoDback, + TwoDmenuFore, + TwoDmenuBack, } function setTextureMappingIndirect(m: TextureMapping, flipY: boolean): void { @@ -47,23 +66,56 @@ export class dPa_control_c { private drawInfo = new JPADrawInfo(); private jpacData: JPACData[] = []; private resourceDatas = new Map(); + private flipY: boolean; + private simpleCallbacks: dPa_simpleEcallBack[] = []; - constructor(cache: GfxRenderCache, private jpac: JPAC[]) { + constructor(cache: GfxRenderCache) { const device = cache.device; - const flipY = gfxDeviceNeedsFlipY(device); + this.flipY = gfxDeviceNeedsFlipY(device); this.emitterManager = new JPAEmitterManager(cache, 6000, 300); - for (let i = 0; i < this.jpac.length; i++) { - const jpacData = new JPACData(this.jpac[i]); + } - const m = jpacData.getTextureMappingReference('AK_kagerouSwap00'); - if (m !== null) - setTextureMappingIndirect(m, flipY); + public createCommon(globals: dGlobals, commonJpac: JPAC): void { + const jpacData = new JPACData(commonJpac); + const m = jpacData.getTextureMappingReference('AK_kagerouSwap00'); + if (m !== null) + setTextureMappingIndirect(m, this.flipY); + this.jpacData.push(jpacData); + + for (let id of j_o_id) { + const resData = this.getResData(globals, id); + if (resData) { + this.newSimple(resData, id, id & 0x4000 ? ParticleGroup.Projection : ParticleGroup.Normal) + } + } + } - this.jpacData.push(jpacData); + public createRoomScene(globals: dGlobals, sceneJpac: JPAC): void { + const jpacData = new JPACData(sceneJpac); + const m = jpacData.getTextureMappingReference('AK_kagerouSwap00'); + if (m !== null) + setTextureMappingIndirect(m, this.flipY); + this.jpacData.push(jpacData); + + for (let id of s_o_id) { + const resData = this.getResData(globals, id); + if (resData) { + let groupID; + if (id & 0x4000) groupID = ParticleGroup.Projection; + else if (id & 0x2000) groupID = ParticleGroup.Toon; + else groupID = ParticleGroup.Normal; + this.newSimple(resData, id, groupID) + } } } - public setDrawInfo(posCamMtx: ReadonlyMat4, prjMtx: ReadonlyMat4, texPrjMtx: ReadonlyMat4 | null, frustum: Frustum): void { + private newSimple(resData: JPAResourceData, userID: number, groupID: number): void { + const simple = new dPa_simpleEcallBack(); + simple.create(this.emitterManager, resData, userID, groupID); + this.simpleCallbacks.push(simple); + } + + public setDrawInfo(posCamMtx: ReadonlyMat4, prjMtx: ReadonlyMat4, texPrjMtx: ReadonlyMat4 | null, frustum: Frustum | null): void { this.drawInfo.posCamMtx = posCamMtx; this.drawInfo.texPrjMtx = texPrjMtx; this.drawInfo.frustum = frustum; @@ -75,6 +127,11 @@ export class dPa_control_c { // Some hacky distance culling for emitters. for (let i = 0; i < this.emitterManager.aliveEmitters.length; i++) { const emitter = this.emitterManager.aliveEmitters[i]; + + // Don't distance cull 2D/UI emitters + if(emitter.drawGroupId >= ParticleGroup.TwoDfore) + continue; + const cullDistance = (emitter as any).cullDistance ?? 5000; if (vec3.distance(emitter.globalTranslation, globals.camera.cameraPos) > cullDistance) { emitter.stopCalcEmitter(); @@ -106,18 +163,6 @@ export class dPa_control_c { return null; } - private patchResData(globals: dGlobals, resData: JPAResourceData): void { - if (resData.resourceId & 0x4000) { - const m = resData.materialHelper.material; - m.tevStages[0].alphaInA = GX.CA.ZERO; - m.tevStages[0].alphaInB = GX.CA.ZERO; - m.tevStages[0].alphaInC = GX.CA.ZERO; - m.tevStages[0].alphaInD = GX.CA.A0; - - resData.materialHelper.materialInvalidated(); - } - } - private getResData(globals: dGlobals, userIndex: number): JPAResourceData | null { if (!this.resourceDatas.has(userIndex)) { const data = this.findResData(userIndex); @@ -125,8 +170,9 @@ export class dPa_control_c { const [jpacData, jpaResRaw] = data; const device = globals.modelCache.device, cache = globals.modelCache.cache; const resData = new JPAResourceData(device, cache, jpacData, jpaResRaw); - this.patchResData(globals, resData); this.resourceDatas.set(userIndex, resData); + } else { + return null; } } @@ -144,15 +190,6 @@ export class dPa_control_c { baseEmitter.drawGroupId = groupID; - // HACK for now - // This seems to mark it as an indirect particle (???) for simple particles. - // ref. d_paControl_c::readCommon / readRoomScene - if (!!(userID & 0x4000)) { - baseEmitter.drawGroupId = EffectDrawGroup.Indirect; - } else { - baseEmitter.drawGroupId = EffectDrawGroup.Main; - } - if (pos !== null) vec3.copy(baseEmitter.globalTranslation, pos); if (rot !== null) @@ -181,18 +218,12 @@ export class dPa_control_c { return baseEmitter; } - // TODO(jstpierre): Full simple particle system -/* - public setSimple(globals: dGlobals, userID: number, pos: ReadonlyVec3, alpha: number = 1.0, colorPrm: Color | null = null, colorEnv: Color | null = null, affectedByWind: boolean = false): boolean { - let groupID = EffectDrawGroup.Main; - - if (!!(userID & 0x4000)) - groupID = EffectDrawGroup.Indirect; - - this.set(globals, groupID, userID, pos, null, null, alpha, null, 0, colorPrm, colorEnv); - return true; + public setSimple(userID: number, pos: vec3, alpha: number, prmColor: Color, envColor: Color, isAffectedByWind: boolean): boolean { + const simple = this.simpleCallbacks.find(s => s.userID == userID); + if (!simple) + return false; + return simple.set(pos, alpha / 0xFF, prmColor, envColor, isAffectedByWind); } -*/ public destroy(device: GfxDevice): void { for (let i = 0; i < this.jpacData.length; i++) @@ -201,6 +232,89 @@ export class dPa_control_c { } } +interface dPa_simpleData_c { + pos: vec3; + prmColor: Color; + envColor: Color; + isAffectedByWind: boolean; +}; + +class dPa_simpleEcallBack extends JPAEmitterCallBack { + public userID: number; + public groupID: number; + private baseEmitter: JPABaseEmitter | null; + private datas: dPa_simpleData_c[] = []; + private emitCount: number = 0; + + public create(emitterManager: JPAEmitterManager, resData: JPAResourceData, userID: number, groupID: number) { + this.userID = userID; + this.groupID = groupID; + this.baseEmitter = emitterManager.createEmitter(resData); + if (this.baseEmitter) { + this.baseEmitter.drawGroupId = groupID; + this.baseEmitter.emitterCallBack = this; + this.baseEmitter.maxFrame = 0; + this.baseEmitter.stopCreateParticle(); + + // From dPa_simpleEcallBack::draw(). Fixup TEV settings for particles that access the framebuffer. + if (groupID == ParticleGroup.Projection) { + const m = resData.materialHelper.material; + m.tevStages[0].alphaInA = GX.CA.ZERO; + m.tevStages[0].alphaInB = GX.CA.ZERO; + m.tevStages[0].alphaInC = GX.CA.ZERO; + m.tevStages[0].alphaInD = GX.CA.A0; + resData.materialHelper.materialInvalidated(); + } + + if (userID == 0xa06a || userID == 0xa410) { + // TODO: Smoke callback + } + } + } + + public set(pos: vec3, alpha: number, prmColor: Color, envColor: Color, isAffectedByWind: boolean): boolean { + this.datas.push({ pos: vec3.clone(pos), prmColor: colorNewCopy(prmColor, alpha), envColor: colorNewCopy(envColor), isAffectedByWind }); + return true; + } + + public override executeAfter(emitter: JPABaseEmitter): void { + const workData = emitter.emitterManager.workData; + if (workData.volumeEmitCount <= 0) { + this.datas = []; + return; + } + + // The emit count is often 1 per game-frame, meaning our emit count will be ~0.5 + // So we track the emit count across frames and only actually emit when it is >1 + this.emitCount += workData.volumeEmitCount * workData.deltaTime; + const emitThisFrame = Math.floor(this.emitCount); + this.emitCount -= emitThisFrame; + + emitter.playCreateParticle(); + for (let simple of this.datas) { + if (!workData.frustum || workData.frustum.containsSphere(simple.pos, 200)) { + emitter.setGlobalTranslation(simple.pos); + colorCopy(emitter.globalColorPrm, simple.prmColor); + colorCopy(emitter.globalColorEnv, simple.envColor); + for (let i = 0; i < emitThisFrame; i++) { + const particle = emitter.createParticle(); + if (!particle) + break; + + // NOTE: Overwriting this removes the influence of the local emitter translation (bem.emitterTrs) + // I.e. all simple emitters ignore their local offsets and are fixed to the local origin. + vec3.copy(particle.offsetPosition, simple.pos); + if (simple.isAffectedByWind) { + // TODO: Wind callback + } + } + } + } + this.datas = []; + emitter.stopCreateParticle(); + } +} + export class dPa_splashEcallBack extends dPa_levelEcallBack { public emitter: JPABaseEmitter | null = null;