Skip to content

Commit

Permalink
Wind Waker: Fix camera execution ordering problem
Browse files Browse the repository at this point in the history
This was causing single frame glitches in demos when the camera was moved. Previously, the camera logic was executing before the demo manager. The demo would set the camera position, as well an actor position that was previously visible. The camera was expected to be moved that frame, allowing the actor to also move without issue.

But the actor logic was running after demo manager, so the actor would move off screen this frame while the camera would not. On the next frame the camera would pick up the position change.

The order needs to be such:
- Demo manager executes
- Camera executes
- Camera draws (final matrices are set)
- Everything else draws (using the new camera matrices)

This change makes dCamera_c a leafdraw process and adds it to the proper queues. One thing worth noting is that because of the way that the layer system has been simplified in Noclip, the camera must be created before d_s_play in order to be drawn first.
  • Loading branch information
themikelester committed Dec 28, 2024
1 parent d301767 commit 72dfd6b
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 18 deletions.
53 changes: 35 additions & 18 deletions src/ZeldaWindWaker/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { ResType, dRes_control_c } from './d_resorce.js';
import { dStage_dt_c_roomLoader, dStage_dt_c_roomReLoader, dStage_dt_c_stageInitLoader, dStage_dt_c_stageLoader, dStage_roomControl_c, dStage_roomStatus_c, dStage_stageDt_c } from './d_stage.js';
import { WoodPacket } from './d_wood.js';
import { fopAcM_create, fopAcM_searchFromName, fopAc_ac_c } from './f_op_actor.js';
import { cPhs__Status, fGlobals, fopDw_Draw, fopScn, fpcCt_Handler, fpcLy_SetCurrentLayer, fpcM_Management, fpcPf__Register, fpcSCtRq_Request, fpc_pc__ProfileList } from './framework.js';
import { cPhs__Status, fGlobals, fopDw_Draw, fopScn, fpcCt_Handler, fpcLy_SetCurrentLayer, fpcM_Management, fpcPf__Register, fpcSCtRq_Request, fpc_pc__ProfileList, leafdraw_class } from './framework.js';
import { J2DGrafContext } from '../Common/JSYSTEM/J2Dv1.js';

type SymbolData = { Filename: string, SymbolName: string, Data: ArrayBufferSlice };
Expand Down Expand Up @@ -147,7 +147,7 @@ export class dGlobals {
// g_dComIfG_gameInfo.mPlay.mpPlayer.mPos
public playerPosition = vec3.create();
// g_dComIfG_gameInfo.mPlay.mCameraInfo[0].mpCamera
public camera = new dCamera_c();
public camera: dCamera_c;

public resCtrl: dRes_control_c;
// TODO(jstpierre): Remove
Expand Down Expand Up @@ -212,7 +212,9 @@ const enum CameraMode {
Cinematic
}

export class dCamera_c {
export class dCamera_c extends leafdraw_class {
public static PROCESS_NAME = dProcName_e.d_camera;

public viewFromWorldMatrix = mat4.create(); // aka viewMatrix
public worldFromViewMatrix = mat4.create(); // aka worldMatrix
public clipFromWorldMatrix = mat4.create();
Expand Down Expand Up @@ -272,8 +274,14 @@ export class dCamera_c {
this.finishSetup();
}

public execute(globals: dGlobals, viewerInput: Viewer.ViewerRenderInput) {
this.setupFromCamera(viewerInput.camera);
public override load(globals: dGlobals, userData: any): cPhs__Status {
globals.camera = this;
return cPhs__Status.Next;
}

// Executes after the demo manager and other systems that can modify the camera
public override execute(globals: dGlobals, deltaTimeFrames: number): void {
this.setupFromCamera(globals.sceneContext.viewerInput.camera);

// Near/far planes are decided by the stage data.
const stag = globals.dStage_dt.stag;
Expand All @@ -287,7 +295,7 @@ export class dCamera_c {
this.far *= 2;

// noclip modification: if we're paused, allow noclip camera control during demos
const isPaused = viewerInput.deltaTime === 0;
const isPaused = globals.sceneContext.viewerInput.deltaTime === 0;

// dCamera_c::Store() sets the camera params if the demo camera is active
const demoCam = globals.scnPlay.demo.getSystem().getCamera();
Expand All @@ -311,7 +319,7 @@ export class dCamera_c {
// Adapted from dCamera_c::CalcTrimSize()
// When switching between Cinematic and Regular camera modes (e.g. when pausing a cutscene),
// blend the camera parameters smoothly. This accounts for deltaTime, but still works when paused.
const deltaTimeFrames = clamp(viewerInput.deltaTime / 1000 * 30, 0.5, 1);
deltaTimeFrames = clamp(deltaTimeFrames, 0.5, 1);
this.cameraModeBlendVal += (this.cameraMode - this.cameraModeBlendVal) * 0.25 * deltaTimeFrames;
this.trimHeight = lerp(0, dCamera_c.trimHeightCinematic, this.cameraModeBlendVal);
this.fovY = lerp(this.fovY, this.demoFov, this.cameraModeBlendVal);
Expand All @@ -321,8 +329,8 @@ export class dCamera_c {
mat4.rotateZ(this.worldFromViewMatrix, this.worldFromViewMatrix, this.roll * MathConstants.DEG_TO_RAD);

// Keep noclip and demo cameras in sync. Ensures that when the user pauses, the camera doesn't snap to an old location
mat4.copy(viewerInput.camera.worldMatrix, this.worldFromViewMatrix);
viewerInput.camera.worldMatrixUpdated();
mat4.copy(globals.sceneContext.viewerInput.camera.worldMatrix, this.worldFromViewMatrix);
globals.sceneContext.viewerInput.camera.worldMatrixUpdated();

// Compute updated projection matrix
const nearY = Math.tan(this.fovY * 0.5) * this.near;
Expand All @@ -332,8 +340,8 @@ export class dCamera_c {
projectionMatrixConvertClipSpaceNearZ(this.clipFromViewMatrix, this.clipSpaceNearZ, GfxClipSpaceNearZ.NegativeOne);

// Scissor setup
const trimPx = (this.trimHeight / 480) * viewerInput.backbufferHeight;
vec4.set(this.scissor, 0, trimPx, viewerInput.backbufferWidth, viewerInput.backbufferHeight - 2 * trimPx);
const trimPx = (this.trimHeight / 480) * globals.sceneContext.viewerInput.backbufferHeight;
vec4.set(this.scissor, 0, trimPx, globals.sceneContext.viewerInput.backbufferWidth, globals.sceneContext.viewerInput.backbufferHeight - 2 * trimPx);

this.finishSetup();

Expand All @@ -343,6 +351,16 @@ export class dCamera_c {
}
}

// Executes before any other draw in other systems
override draw(globals: dGlobals, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput): void {
const template = renderInstManager.pushTemplate();

mat4.copy(sceneParams.u_Projection, globals.camera.clipFromViewMatrix);
sceneParams.u_SceneTextureLODBias = calcLODBias(viewerInput.backbufferWidth, viewerInput.backbufferHeight);
const d = template.allocateUniformBufferF32(GX_Program.ub_SceneParams, ub_SceneParamsBufferSize);
fillSceneParamsData(d, 0, sceneParams);
}

public applyScissor(pass: GfxRenderPass) {
if(this.enableLetterboxing) {
pass.setScissor(this.scissor[0], this.scissor[1], this.scissor[2], this.scissor[3]);
Expand Down Expand Up @@ -596,8 +614,6 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
}
}

globals.camera.execute(globals, viewerInput);

// Not sure exactly where this is ordered...
dKy_setLight(globals);

Expand All @@ -606,11 +622,6 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
if (globals.renderHacks.wireframe)
template.setMegaStateFlags({ wireframe: true });

mat4.copy(sceneParams.u_Projection, globals.camera.clipFromViewMatrix);
sceneParams.u_SceneTextureLODBias = calcLODBias(viewerInput.backbufferWidth, viewerInput.backbufferHeight);
const d = template.allocateUniformBufferF32(GX_Program.ub_SceneParams, ub_SceneParamsBufferSize);
fillSceneParamsData(d, 0, sceneParams);

this.extraTextures.prepareToRender(device);

fpcM_Management(globals.frameworkGlobals, globals, renderInstManager, viewerInput);
Expand Down Expand Up @@ -991,6 +1002,7 @@ class SceneDesc {
const f_pc_profiles = BYML.parse<fpc_pc__ProfileList>(modelCache.getFileData(`f_pc_profiles.crg1_arc`), BYML.FileType.CRG1);
const framework = new fGlobals(f_pc_profiles);

fpcPf__Register(framework, dProcName_e.d_camera, dCamera_c);
fpcPf__Register(framework, dProcName_e.d_s_play, d_s_play);
dKy__RegisterConstructors(framework);
dKyw__RegisterConstructors(framework);
Expand All @@ -1007,6 +1019,11 @@ class SceneDesc {
context.destroyablePool.push(renderer);
globals.renderer = renderer;

// NOTE: The camera must be created before d_s_play, as that determines the draw order and camera must draw first.
// TODO: Use the RCAM/CAMR data from the stage to set the initial camera position
const camId = fpcSCtRq_Request(framework, null, dProcName_e.d_camera, null);
assert(camId !== null);

const pcId = fpcSCtRq_Request(framework, null, dProcName_e.d_s_play, null);
assert(pcId !== null);

Expand Down
1 change: 1 addition & 0 deletions src/ZeldaWindWaker/d_procname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const enum dProcName_e {
d_a_vrbox2 = 0x01BB,
d_a_bg = 0x01BC,
d_a_swhit0 = 0x01C9,
d_camera = 0x01E2,
d_kyeff = 0x01E4,
d_kyeff2 = 0x01E5,
d_place_name = 0x01EE,
Expand Down

0 comments on commit 72dfd6b

Please sign in to comment.