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;