Files
F3DEX3/cpu/occlusionplane.c
2025-10-04 16:51:44 -07:00

539 lines
21 KiB
C

/*
This is a bit outdated but still generally okay. A full implementation is
present in HackerOoT, including the dynamic choice of occlusion plane, see
src/code/occlusionplanes.c and related files.
This is a working demo implementation of the occlusion plane, set up in an
OoT scene render function. Here are some rough guidelines on how to properly
implement this in your game.
1. Move the structs to some header, and the rest of the code to a source file.
Yes, all this code really is needed except for the commented-out parts for
debugging (see below)--the algorithms really are this complicated.
2. If this is an OoT codebase, see the discussion in camera.c (in this repo)
about how OoT updates the camera at the end of the frame and retroactively
changes the view matrix at the start of the frame. You should similarly insert
the gSPOcclusionPlane command into the main display list near the beginning,
e.g. after writing the camera matrices etc., and with a NULL pointer. Then
update the occlusion plane after updating the camera and write the pointer to
this occlusion plane into the existing DL command near the beginning.
3. Create a system in your game engine for dynamically choosing or creating an
occlusion plane. (See the implementation in HackerOoT.) For example, you might
have a set of pre-determined occlusion planes in the scene, and at runtime pick
the one which you think is most optimal. Some criteria to use for this include:
- whether the camera is on the correct side of the occlusion plane
- the distance from the camera to the full (infinite) plane
- how far the point of the camera projected onto the full (infinite) plane is
from the bounds of the finite plane (or if it's inside it)
- how close the camera is to looking directly at the plane (negative dot
product between plane normal and camera view direction)
- some of these things scaled by the (constant per candiate) world-space area
of the finite plane
You can get even more relevant metrics by using parts of the code here. For
example, the screen area of the occlusion plane can be computed from the clipped
screen-space polygon. However, it's probably not worth it to run that much of
the code here for every candidate occlusion plane.
4. Take a look at the commented out code using occPlaneMessage. Except for
"Offscreen" and the candidate counts, all the messages written to it represent
errors or problems with the occlusion plane setup (or bugs in these algorithms).
For example, "Edge %d now has no cands" occurs when the occlusion plane is being
viewed nearly edge-on, causing there to be too many edges oriented too similarly
to be representable by the occlusion plane equations. If you get this message,
your choice of occlusion plane was poor and the occlusion plane will be
disabled.
While it's not recommended to use occPlaneMessage besides for debugging, you
should create an "error flag" which is set if any of these messages would have
been written, and display something visually on the screen (such as an error
icon, short text string, etc.) if this happens. This is important because:
- while this code seems to work, it is not tested super thoroughly, especially
for extreme transformations of the occlusion plane (e.g. camera at a sharp
angle very close to it)
- if the occlusion plane doesn't work, you won't notice visually in the game,
it'll just not occlude anything and you'll get lower performance
Please confirm the occlusion plane is actually working in your game, and let me
know of any issues!
*/
typedef struct {
Vec3f clip;
float w;
Vec2f scrn;
u8 isScreenEdge;
} ClipVertex;
typedef struct {
s16 cScale;
s16 cOffset;
u8 edgeID;
} EdgeCandidate;
s16 FloatToS16Clamp(float f){
f = CLAMP(f, -32768.0f, 32767.0f);
return (s16)f;
}
s16 FloatMinus1To1ToS16(float f){
return FloatToS16Clamp(f * 32768.0f);
}
void ClipToScreenSpace(PlayState* play, const Vec3f* clipXYZ, float clipW, Vec2f* screen){
if(clipW < 0.001f){
// Behind camera plane
screen->x = 0x8000;
screen->y = 0x8000;
return;
}
float invW = 1.0f / clipW;
float preViewportX = clipXYZ->x * invW;
float preViewportY = clipXYZ->y * invW;
Vp* vp = &play->view.vp;
screen->x = (float)vp->vp.vscale[0] * preViewportX + (float)vp->vp.vtrans[0];
screen->y = (float)vp->vp.vscale[1] * preViewportY + (float)vp->vp.vtrans[1];
}
bool ClipPolygon(PlayState* play, ClipVertex* verts, s8* indices, s8** idxFinalStart, s8** idxFinalEnd){
// This is roughly a reimplementation of the F3D family clipping code
// (Overlay 3), except with hardcoded clip ratio of 1 (screen clipping)
s8 igen = 4; // gen vertex pointer
s32 idxSelect = 0;
ClipVertex* v3 = &verts[indices[3]];
s8* idxWrite;
for(s32 condition=4; condition>=0; --condition){
s8* idxRead = &indices[idxSelect];
idxSelect ^= 10;
idxWrite = &indices[idxSelect];
while(true){
s8 i2 = *idxRead;
if(i2 < 0) break;
ClipVertex* v2 = &verts[i2];
++idxRead;
bool v2Offscreen, v3Offscreen;
switch(condition){
case 4: // -W
v2Offscreen = v2->w <= 0.0f;
v3Offscreen = v3->w <= 0.0f;
break;
case 3: // +X
v2Offscreen = v2->clip.x >= v2->w;
v3Offscreen = v3->clip.x >= v3->w;
break;
case 2: // -X
v2Offscreen = v2->clip.x <= -v2->w;
v3Offscreen = v3->clip.x <= -v3->w;
break;
case 1: // +Y
v2Offscreen = v2->clip.y >= v2->w;
v3Offscreen = v3->clip.y >= v3->w;
break;
case 0: // -Y
v2Offscreen = v2->clip.y <= -v2->w;
v3Offscreen = v3->clip.y <= -v3->w;
break;
}
if(v2Offscreen != v3Offscreen){
// Clip this edge
ClipVertex* v19 = v2;
if(v2Offscreen){
v19 = v3;
v3 = v2;
}
// v19 is on screen, v3 is off screen
float clOnScreen, clOffScreen;
if(condition == 4){
clOnScreen = 0.0f;
clOffScreen = 0.0f;
}else if(condition <= 1){
clOnScreen = v19->clip.y;
clOffScreen = v3->clip.y;
}else{
clOnScreen = v19->clip.x;
clOffScreen = v3->clip.x;
}
float mult = (condition & 1) ? -1.0f : 1.0f;
clOnScreen += mult * v19->w;
clOffScreen += mult * v3->w;
float clBase = clOnScreen;
float clDiff = clOnScreen - clOffScreen;
float clFade1;
if(fabsf(clDiff) < 1e-6f){
clFade1 = 1.0f;
}else{
clFade1 = clBase / clDiff;
clFade1 = CLAMP(clFade1, 0.0f, 1.0f);
}
float clFade2 = 1.0f - clFade1;
if(igen >= 14){
// Too many generated vertices
return false;
}
if(idxWrite - &indices[idxSelect] >= 9){
// Polygon has too many vertices
return false;
}
verts[igen].clip.x = clFade2 * v19->clip.x + clFade1 * v3->clip.x;
verts[igen].clip.y = clFade2 * v19->clip.y + clFade1 * v3->clip.y;
verts[igen].w = clFade2 * v19->w + clFade1 * v3->w;
verts[igen].isScreenEdge = v2Offscreen || v3->isScreenEdge;
ClipToScreenSpace(play, &verts[igen].clip, verts[igen].w, &verts[igen].scrn);
*idxWrite = igen;
++idxWrite;
++igen;
}
if(!v2Offscreen){
if(idxWrite - &indices[idxSelect] >= 9){
// Polygon has too many vertices
return false;
}
*idxWrite = i2;
++idxWrite;
}
v3 = v2;
}
*idxWrite = -1;
if(idxWrite - &indices[idxSelect] < 3){
// Less than 3 verts in written polygon
return false;
}
v3 = &verts[*(idxWrite-1)];
}
*idxFinalStart = &indices[idxSelect];
*idxFinalEnd = idxWrite;
return true;
}
// For debugging only
//static char occPlaneMessage[64];
// The occlusion plane settings for "disable the occlusion plane". This is just
// stored once, and the SPOcclusionPlane DL command is set to point here if the
// occlusion plane is disabled or invalid.
static OcclusionPlane sNoOcclusionPlane = {
0x0000,
0x0000,
0x0000,
0x0000,
0x8000,
0x8000,
0x8000,
0x8000,
0x0000,
0x0000,
0x0000,
0x7FFF
};
OcclusionPlane* ComputeOcclusionPlane(PlayState* play, Vec3f* worldBounds){
//occPlaneMessage[0] = 0;
ClipVertex verts[14]; // 4 initial verts, 5 tips cut off polygon with 2 gen verts each
s8 indices[20]; // Polygon starts with 4 verts, 5 tips cut off = 9, plus 1 entry -1, times read and write
for(s32 i=0; i<4; ++i){
SkinMatrix_Vec3fMtxFMultXYZW(&play->viewProjectionMtxF,
&worldBounds[i], &verts[i].clip, &verts[i].w);
ClipToScreenSpace(play, &verts[i].clip, verts[i].w, &verts[i].scrn);
verts[i].isScreenEdge = 0;
indices[i] = i;
}
indices[4] = -1;
// Clip space plane
float kxf, kyf, kzf, kcf;
Math3D_DefPlane(&verts[0].clip, &verts[2].clip, &verts[1].clip,
&kxf, &kyf, &kzf, &kcf);
s16 kx = FloatMinus1To1ToS16(kxf);
s16 ky = FloatMinus1To1ToS16(kyf);
s16 kz = FloatMinus1To1ToS16(kzf);
s16 kc = (s16)(kcf * -0.5f);
if((kx | ky | kz) == 0){
// Degenerate plane, disable the clipping
//sprintf(occPlaneMessage, "Clip space degenerate");
return &sNoOcclusionPlane;
}
// Clip the polygon to the screen edges. Screen edges don't require an
// occlusion plane equation.
s8 *idxFinalStart, *idxFinalEnd, *idx;
if(!ClipPolygon(play, verts, indices, &idxFinalStart, &idxFinalEnd)){
// Resulting polygon is degenerate; occlusion plane is fully offscreen. No occlusion.
//sprintf(occPlaneMessage, "Offscreen");
return &sNoOcclusionPlane;
}
/*
// Visualize the clipped polygon by drawing its vertices.
OPEN_DISPS(play->state.gfxCtx);
gDPPipeSync(OVERLAY_DISP++);
gDPSetCycleType(OVERLAY_DISP++, G_CYC_FILL);
gDPSetRenderMode(OVERLAY_DISP++, G_RM_NOOP, G_RM_NOOP2);
u8 r = 0xFF, g = 0, b = 0;
gDPSetFillColor(OVERLAY_DISP++, (GPACK_RGBA5551(r, g, b, 1) << 16) | GPACK_RGBA5551(r, g, b, 1));
idx = idxFinalStart;
while(idx != idxFinalEnd){
s16 x = verts[*idx].scrn.x;
s16 y = verts[*idx].scrn.y;
++idx;
x >>= 2;
y >>= 2;
if(x < 3) x = 3;
if(x > 315) x = 315;
if(y < 1) y = 1;
if(y > 236) y = 236;
gDPScisFillRectangle(OVERLAY_DISP++, x-2, y-2, x+2, y+2);
}
gDPPipeSync(OVERLAY_DISP++);
gDPSetCycleType(OVERLAY_DISP++, G_CYC_2CYCLE);
CLOSE_DISPS(play->state.gfxCtx);
*/
// Candidates for each of the 4 equations. Up to 3 edges can be candidates for each.
EdgeCandidate cands[4][3];
u8 numCands[4];
numCands[0] = numCands[1] = numCands[2] = numCands[3] = 0;
u8 totalEdges = 0;
u8 candsForEdge[4];
// Traverse the clipped polygon. For each edge which is not a screen edge,
// see if it can be represented as each of the four equations. For any it can
// be, compute its representation as that equation and store as a candidate.
ClipVertex* vtxA;
ClipVertex* vtxB = &verts[*(idxFinalEnd-1)];
idx = idxFinalStart;
while(idx < idxFinalEnd){
vtxA = vtxB;
vtxB = &verts[*idx];
++idx;
if(vtxA->isScreenEdge) continue;
// Should only be 4 edges not along a screen edge
if(totalEdges >= 4){
//sprintf(occPlaneMessage, "Too many edges");
return &sNoOcclusionPlane;
}
u8 numCandsFit = 0;
float dx = vtxB->scrn.x - vtxA->scrn.x;
float dy = vtxB->scrn.y - vtxA->scrn.y;
for(s32 q=0; q<4; ++q){
float du, dv, uA, vA; // Equation V <> U * cScale + cOffset
if((q & 1)){
dv = dx;
du = dy;
vA = vtxA->scrn.x;
uA = vtxA->scrn.y;
}else{
du = dx;
dv = dy;
uA = vtxA->scrn.x;
vA = vtxA->scrn.y;
}
// Check side of edge using cross product. For example, if the
// equation is Y > something, which side of that line is inside /
// outside the clipped polygon depends on dx.
// Eqn 0: Y > something -> dx > 0 -> du > 0
// Eqn 1: X > something -> dy < 0 -> du < 0
// Eqn 2: Y < something -> dx < 0 -> du < 0
// Eqn 3: X < something -> dy > 0 -> du > 0
if((q == 0 || q == 3) != (du > 0.0f)) continue;
// cScale (after 1/8th scale) is limited to +/- 1. This also takes
// care of the divided by 0 case, as the left side will always be
// greater than or equal to 0.
if(fabsf(dv) >= 8.0f * fabsf(du)) continue;
float cScale = dv / du;
float cOffset = vA - uA * cScale;
cScale *= 0.125f;
cOffset *= 0.5f;
if(q >= 2){
cScale = -cScale;
}else{
cOffset = -cOffset;
}
if(fabsf(cOffset) > 32767.0f) continue;
if(numCands[q] >= 3){
// Each equation should have no more than 3 candidate edges
//sprintf(occPlaneMessage, "Eqn has too many cands");
return &sNoOcclusionPlane;
}
EdgeCandidate* cand = &cands[q][numCands[q]];
cand->cScale = FloatMinus1To1ToS16(cScale);
cand->cOffset = (s16)cOffset;
cand->edgeID = totalEdges;
++numCandsFit;
++(numCands[q]);
}
if(numCandsFit == 0){
//sprintf(occPlaneMessage, "Edge fit in no cands");
return &sNoOcclusionPlane;
}else if(numCandsFit > 3){
// Each edge should have no more than 3 candidate equations
//sprintf(occPlaneMessage, "Edge fit in too many cands");
return &sNoOcclusionPlane;
}
candsForEdge[totalEdges] = numCandsFit;
++totalEdges;
}
//sprintf(occPlaneMessage, "%de %dv %d> %d^ %d<", totalEdges,
// numCands[0], numCands[1], numCands[2], numCands[3]);
// Assign candidates to equations.
while(true){
// Check fail condition: if now there is some edge which has no candidates
for(s32 e=0; e<totalEdges; ++e){
if(candsForEdge[e] == 0){
//sprintf(occPlaneMessage, "Edge %d now has no cands", e);
return &sNoOcclusionPlane;
}
}
// Check done condition: all equations have 0 or 1 candidates
bool done = true;
for(s32 q=0; q<4; ++q){
if(numCands[q] >= 2){
done = false;
break;
}
}
if(done) break;
// Check for an equation which has more than one candidate edge, but
// one of those edges only has one candidate, so that edge has to be
// assigned to that equation.
bool madeChange = false;
for(s32 q=0; q<4 && !madeChange; ++q){
if(numCands[q] <= 1) continue;
for(s32 c=0; c<numCands[q]; ++c){
if(candsForEdge[cands[q][c].edgeID] == 1){
madeChange = true;
// Decrement num candidates for other edges in this equation
for(s32 i=0; i<numCands[q]; ++i){
if(i == c) continue;
--(candsForEdge[cands[q][i].edgeID]);
}
// Move found edge to position 0 and truncate list
cands[q][0] = cands[q][c];
numCands[q] = 1;
break;
}
}
}
if(madeChange) continue; // Restart loop
// Take the first equation which has more than one candidate edge, and
// assign the edge with smallest abs(cScale)
for(s32 q=0; q<4; ++q){
if(numCands[q] <= 1) continue;
s32 bestC = 0;
s32 bestScale = ABS((s32)cands[q][0].cScale);
for(s32 c=1; c<numCands[q]; ++c){
s32 scale = ABS((s32)cands[q][c].cScale);
if(scale < bestScale){
bestScale = scale;
bestC = c;
}
}
// Assigning equation q to edge e (edge currently as candidate bestC)
s32 e = cands[q][bestC].edgeID;
// Decrement num candidates for other edges in this equation
for(s32 i=0; i<numCands[q]; ++i){
if(i == bestC) continue;
--(candsForEdge[cands[q][i].edgeID]);
}
// Move found edge to position 0 and truncate list
cands[q][0] = cands[q][bestC];
numCands[q] = 1;
// Remove this edge from other candidate lists
for(s32 j=0; j<4; ++j){
if(q == j) continue;
s32 i;
for(i=0; i<numCands[j]; ++i){
if(cands[j][i].edgeID == e) break;
}
if(i == numCands[j]) continue;
for(; i<numCands[j] - 1; ++i){
cands[j][i] = cands[j][i+1];
}
--(numCands[j]);
--(candsForEdge[e]);
}
if(candsForEdge[e] != 1){
//sprintf(occPlaneMessage, "Internal error 2");
return &sNoOcclusionPlane;
}
madeChange = true;
break;
}
if(!madeChange){
//sprintf(occPlaneMessage, "Internal error 1");
return &sNoOcclusionPlane;
}
}
// Move equations to occlusion plane
OcclusionPlane* occ = Graph_Alloc(play->state.gfxCtx, sizeof(OcclusionPlane));
for(s32 q=0; q<4; ++q){
occ->c[q] = (numCands[q] == 0) ? 0x0000 : cands[q][0].cScale;
occ->c[q+4] = (numCands[q] == 0) ? 0x7FFF : cands[q][0].cOffset;
}
occ->o.kx = kx;
occ->o.ky = ky;
occ->o.kz = kz;
occ->o.kc = kc;
return occ;
}
void someDrawFunction(PlayState* play) {
...
// Replace this with some dynamic choice of the occulsion plane in your
// game engine. This comment is about the constraints on the four points
// defining the corner of the plane.
//
// These points must be coplanar and form a convex quadrilateral. They must
// also be in winding order, i.e. when viewed from the front (occlude things
// on the far side of the plane), the verts must be in this order:
// 1 2
// 0 3
// This can be rotated / scaled / sheared (as the camera moves). However, it
// won't work properly if it is flipped (the camera is moved to be on the
// occlusion side of it).
static Vec3f PortalBoundingPointsWorld[4] = {
{200.0f, 0.0f, -210.0f},
{200.0f, 150.0f, -210.0f},
{200.0f, 150.0f, -100.0f},
{200.0f, 0.0f, -100.0f}
};
// Compute the occlusion plane and write the pointer to it into the display
// list. Do this as early as possible in the full frame's DL, e.g. after
// setting up the camera. Depending on your framebuffer clearing strategy
// you may want to do this before or after that. There is no need to put
// this in POLY_XLU_DISP too; its state will be retained through the full
// graphics task.
gSPOcclusionPlane(POLY_OPA_DISP++,
ComputeOcclusionPlane(play, PortalBoundingPointsWorld));
/*
if(occPlaneMessage[0] != 0){
GfxPrint printer;
GfxPrint_Init(&printer);
Gfx *opaStart = POLY_OPA_DISP;
Gfx *gfx = Graph_GfxPlusOne(POLY_OPA_DISP);
gSPDisplayList(OVERLAY_DISP++, gfx);
GfxPrint_Open(&printer, gfx);
GfxPrint_SetColor(&printer, 0, 0, 255, 255);
GfxPrint_SetPos(&printer, 12, 28);
GfxPrint_Printf(&printer, "%s", occPlaneMessage);
gfx = GfxPrint_Close(&printer);
gSPEndDisplayList(gfx++);
Graph_BranchDlist(opaStart, gfx);
POLY_OPA_DISP = gfx;
}
*/
...
}