/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "util/memory.h" #include "util/string.h" #include "display/shader/shaderunlit.h" #include "assert/assert.h" #include "log/log.h" shaderdolphin_t *SHADER_BOUND; errorret_t shaderInitDolphin( shaderdolphin_t *shader, const shaderdefinitiondolphin_t *def ) { assertNotNull(shader, "Shader must not be null"); assertNotNull(def, "Shader definition must not be null"); memoryZero(shader, sizeof(shaderdolphin_t)); glm_mat4_identity(shader->view); glm_mat4_identity(shader->proj); glm_mat4_identity(shader->model); shader->dirtyMatrix = ( SHADER_DOLPHIN_DIRTY_MODEL | SHADER_DOLPHIN_DIRTY_PROJ | SHADER_DOLPHIN_DIRTY_VIEW ); errorOk(); } errorret_t shaderBindDolphin(shaderdolphin_t *shader) { assertNotNull(shader, "Shader must not be null"); SHADER_BOUND = shader; GX_LoadProjectionMtx( shader->matrixProjection, shader->isProjectionPerspective ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC ); GX_LoadPosMtxImm(shader->matrixModelView, GX_PNMTX0); errorOk(); } errorret_t shaderSetMatrixDolphin( shaderdolphin_t *shader, const char_t *name, mat4 mat ) { assertNotNull(shader, "Shader must not be null"); assertNotNull(name, "Uniform name must not be null"); assertStrLenMin(name, 1, "Uniform name cannot be empty"); if(stringCompare(name, SHADER_UNLIT_PROJECTION) == 0) { shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_PROJ; glm_mat4_copy(mat, shader->proj); } else if(stringCompare(name, SHADER_UNLIT_VIEW) == 0) { shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_VIEW; glm_mat4_copy(mat, shader->view); } else if(stringCompare(name, SHADER_UNLIT_MODEL) == 0) { shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_MODEL; glm_mat4_copy(mat, shader->model); } else { assertUnreachable("Cannot use a custom matrix on dolphin."); } errorOk(); } errorret_t shaderSetTextureDolphin( shaderdolphin_t *shader, const char_t *name, texture_t *texture ) { assertNotNull(shader, "Shader must not be null"); assertNotNull(name, "Uniform name must not be null"); assertStrLenMin(name, 1, "Uniform name cannot be empty"); if(texture == NULL) { // GX_SetNumChans(0); GX_SetNumChans(1); GX_SetChanCtrl( GX_COLOR0A0, GX_DISABLE, GX_SRC_REG, GX_SRC_VTX, GX_LIGHTNULL, GX_DF_NONE, GX_AF_NONE ); GX_SetChanAmbColor(GX_COLOR0A0, (GXColor){ 0, 0, 0, 0 }); GX_SetChanMatColor(GX_COLOR0A0, (GXColor){ 255, 255, 255, 255 }); GX_SetNumTexGens(0); GX_SetNumTevStages(1); GX_SetTevOrder(GX_TEVSTAGE0, GX_TEXCOORDNULL, GX_TEXMAP_NULL, GX_COLOR0A0); GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR); GX_SetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_CLEAR); GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); errorOk(); } // Add channel for vertex color GX_LoadTexObj(&texture->texObj, GX_TEXMAP0); GX_SetNumChans(1); GX_SetChanCtrl( GX_COLOR0A0,// Store in color channel 0 GX_DISABLE,// Lighting disabled GX_SRC_REG,// Ambient color? GX_SRC_VTX,// Material color? GX_LIGHTNULL,// Light Mask GX_DF_NONE,// Diffuse function GX_AF_NONE// Attenuation function ); // One set of UVs GX_SetNumTexGens(1); GX_SetTexCoordGen( GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY ); // Basically the shader setup switch(texture->format) { case TEXTURE_FORMAT_RGBA: // One TEV stage: vertex color * texture color GX_SetNumTevStages(1); GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE); GX_SetTevOrder( GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0 ); GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); break; default: assertUnreachable("Unknown texture format in meshDraw"); break; } errorOk(); } errorret_t shaderDisposeDolphin(shaderdolphin_t *shader) { assertNotNull(shader, "Shader must not be null"); SHADER_BOUND = NULL; errorOk(); } errorret_t shaderUpdateMVPDolphin() { assertNotNull(SHADER_BOUND, "Shader must not be null"); // Any changes? if(SHADER_BOUND->dirtyMatrix == 0) errorOk(); // Need to update projection? if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_PROJ) != 0) { shaderMat4ToMtx44(SHADER_BOUND->proj, SHADER_BOUND->matrixProjection); // Fix projection Z mapping between GLM and GX. float A = SHADER_BOUND->matrixProjection[2][2]; float B = SHADER_BOUND->matrixProjection[2][3]; SHADER_BOUND->matrixProjection[2][2] = 0.5f * (A + 1.0f); SHADER_BOUND->matrixProjection[2][3] = 0.5f * B; // Is this perspective or ortho originally? Dolphin cares for some reason. const float_t epsilon = 0.0001f; SHADER_BOUND->isProjectionPerspective = ( fabsf(SHADER_BOUND->proj[3][2]) > epsilon && fabsf(SHADER_BOUND->proj[3][3]) < epsilon ); GX_LoadProjectionMtx( SHADER_BOUND->matrixProjection, SHADER_BOUND->isProjectionPerspective ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC ); } // Need to update view or model? bool_t mvDirt = false; if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_VIEW) != 0) { shaderMat4ToMtx(SHADER_BOUND->view, SHADER_BOUND->matrixView); mvDirt = true; } if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_MODEL) != 0) { shaderMat4ToMtx(SHADER_BOUND->model, SHADER_BOUND->matrixModel); mvDirt = true; } // Set Model/View Matrix if(mvDirt) { guMtxConcat( SHADER_BOUND->matrixView, SHADER_BOUND->matrixModel, SHADER_BOUND->matrixModelView ); GX_LoadPosMtxImm(SHADER_BOUND->matrixModelView, GX_PNMTX0); } SHADER_BOUND->dirtyMatrix = 0; errorOk(); } void shaderMat4ToMtx44(const mat4 inGlmMatrix, Mtx44 outGXMatrix) { assertNotNull(inGlmMatrix, "Input matrix cannot be null"); assertNotNull(outGXMatrix, "Output matrix cannot be null"); for(int row = 0; row < 4; ++row) { for(int col = 0; col < 4; ++col) { outGXMatrix[row][col] = inGlmMatrix[col][row]; } } } void shaderMat4ToMtx(const mat4 inGlmMatrix, Mtx outGXMatrix) { assertNotNull(inGlmMatrix, "Input matrix cannot be null"); assertNotNull(outGXMatrix, "Output matrix cannot be null"); for(int row = 0; row < 3; ++row) { for(int col = 0; col < 4; ++col) { outGXMatrix[row][col] = inGlmMatrix[col][row]; } } }