Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clipping planes #64

Merged
merged 4 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ add_library(${TARGET} STATIC
src/arrays.h
src/call_lists.c
src/call_lists.h
src/clip.c
src/clip.h
src/debug.c
src/debug.h
src/efb.c
Expand Down
26 changes: 26 additions & 0 deletions doc/src/opengx.tex
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ \subsubsection{Lit and textured render}
The stencil test allows to discard individual fragments depending on the outcome of a comparison between the value stored in the stencil buffer for that fragment and a reference value. The compared values can also be masked with a bitmask before comparison, and the generation of the stencil buffer involves drawing primitives and performing logical and arithmetical operations on the drawn stencil pixels. The stencil test is typically used to render shadows and reflections. Unfortunately, nor the GameCube hardware or the GX APIs provide any support for the stencil test, so we have to emulate it, partially in software, partially with an additional TEV stage.

\subsubsection {Discarding fragments}
\label{sec:stencildiscard}

Let's first see how opengx discards fragments which don't pass the stencil test; for the time being, let's assume that we have managed to build a stencil buffer (in opengx it can be 4 or 8 bits wide, with 4 being the default) and focus only on how to build a TEV stage which does the comparison and discards the fragment. We will be setting up the TEV stage to operate in \emph{compare mode}, where the inputs are combined according to this formula:

Expand Down Expand Up @@ -473,6 +474,31 @@ \subsubsection{Lit and textured render}
The stencil drawing operations \lstinline{GL_INCR}, \lstinline{GL_DECR}, \lstinline{GL_INCR_WRAP}, \lstinline{GL_DECR_WRAP} and \lstinline{GL_INVERT} are not implemented. It might be possible that at least some of them could be implemented in the hardware by setting an appropriate blending mode. Another posibility is to make these functions not directly render on the stencil buffer, but on yet another offscreen buffer, and then process the results in software. But none of these solutions have been implemented so far, given how rarely these operations are used (at least in public source code indexed by GitHub).


\subsection {Clip planes}

The OpenGL \fname{glClipPlane} function can be used to define a clip plane, which will “cut” the primitives drawn while it is enabled and will show only the part that is on the desired side of the plane. A plane is defined by a linear equation

$$Ax + By + Cz + D = 0$$

and the space of the visible (unclipped) points is defined by the corresponding expression

$$Ax + By + Cz + D \geq 0$$

More than a clipping plane can be defined (OpenGL specifies that implementations should support at least 6 of them), in which case the visible area is the intersection of all these spaces.

The GX API does not offer such a functionality, but this doesn't mean that it cannot be implemented. The first idea was generating a Z-texture for each clipping plane, and then let the depth test exclude the fragments whose Z coordinate did not satisfy the requirements. Special care would have to be taken to support clipping planes intersecting the camera, because in those cases the Z-texture would have to be placed not as the plane itself, but as a semi-plane with the Z set to the maximum value in order to mask out all the unwanted fragments. But the hardest part would have been that of saving the original Z values from the EFB and then restoring them back at the end of the clipping operation, while updating the Z values of those fragments that had actually been drawn. As a solution to this problem was not found, the idea was dropped and the whole feature was deemed to be practically unfeasible.

After implementing the stencil test, though, another implementation idea was born: if we did encode the $A$, $B$, $C$, $D$ coefficient of the clipping plane as the first row of the texture coordinate generation matrix, and used the fragment's position as the input source for the texture coordinate generation, the $s$ texture coordinate we would be generating would be

$$s = Ax + By + Cy + Dw$$

which, given that $w = 1$, would effectively be the computation of the clipping equation: we would get an $s \geq 0$ for those fragments that can be drawn, and $s < 0$ for those who need to be clipped out. Sure, texture coordinates are only meaningful in the range $[0,1]$ so a negative $s$ would wrap or be clamped within this interval, but this can be solved by applying a translation of $0.5$ as the last step of the texture coordinate generation in order to have our conditions changed to $s \geq 0.5$ for the visible fragments and $s < 0.5$ for the invisible ones. At this point, we could use this $s$ coordinate on a very small texture, just two texels wide, with an alpha value of 0 on the left texel and 1 on the right one, and use a TEV stage setup similarly to that described in the \ref{sec:stencildiscard} section to drop all fragments which produce a zero alpha value. Note that we need to set the texture coordinates to be clamped (and not wrapped!), so that any value of $s$ bigger than $0.5$ renders a pixel with full alpha, and any value of $s$ smaller than $0.5$ renders a transparent (discarded) pixel.

Furthermore, we can improve this solution by encoding another clip plane in the second row of the texture coordinate generation matrix (which is responsible for generating the $t$ texture coordinate), and then we can handle two clipping planes in a single TEV stage: we just need to prepare a 2-by-2 pixel texture, having an alpha value of $0$ in three of its pixels, and $1$ in the one pixel for which both $s$ and $t$ are positive.

At the moment opengx hardcodes the maximum number of supported clip planes to 6 (which require 3 TEV stages), but this can be increased if needed. Note that only the required number of TEV stages needed to perform the clipping operations is used.


\subsection {Selection mode}

GL selection mode, also known as "picking mode", is typically used in applications to determine which objects are rendered at a certain screen position, in order to implement mouse interactions. When selection mode is active, drawing primitives do not result in any pixels (or even Z-pixels) being drawn, but instead produce a stack containing the names of the object which would have been drawn.
Expand Down
186 changes: 186 additions & 0 deletions src/clip.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*****************************************************************************
Copyright (c) 2011 David Guillen Fandos ([email protected])
Copyright (c) 2024 Alberto Mardegan ([email protected])
All rights reserved.

Attention! Contains pieces of code from others such as Mesa and GRRLib

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of copyright holders nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/

#include "clip.h"

#include "debug.h"
#include "state.h"
#include "utils.h"

#include <GL/gl.h>
#include <malloc.h>

static GXTexObj s_clip_texture;
static uint8_t s_clip_texels[32] ATTRIBUTE_ALIGN(32) = {
/* We only are about the top-left 2x2 corner, that is (given that pixels
* are 4 bits wide) the first and the fourth byte only.
* Note how the positive pixel value is set on the bottom right corner,
* since in OpenGL the y coordinate grows upwards, but the t texture
* coordinate grows downwards. */
0x00, 0x00, 0x00, 0x00,
0x0f, 0x00, 0x00, 0x00,
};

static void load_clip_texture(u8 tex_map)
{
void *tex_data = GX_GetTexObjData(&s_clip_texture);
if (!tex_data) {
GX_InitTexObj(&s_clip_texture, s_clip_texels, 2, 2,
GX_TF_I4, GX_CLAMP, GX_CLAMP, GX_FALSE);
GX_InitTexObjLOD(&s_clip_texture, GX_NEAR, GX_NEAR,
0.0f, 0.0f, 0.0f, 0, 0, GX_ANISO_1);
DCStoreRange(s_clip_texels, sizeof(s_clip_texels));
GX_InvalidateTexAll();
}

GX_LoadTexObj(&s_clip_texture, tex_map);
}

static bool setup_tev(int *stages, int *tex_coords, int tex_maps, int *tex_mtxs,
int plane_index0, int plane_index1)
{
u8 stage = GX_TEVSTAGE0 + *stages;
u8 tex_coord = GX_TEXCOORD0 + *tex_coords;
u8 tex_map = GX_TEXMAP0 + tex_maps;
u8 tex_mtx = GX_TEXMTX0 + *tex_mtxs * 3;

debug(OGX_LOG_CLIPPING, "%d TEV stages, %d tex_coords, %d tex_maps",
*stages, *tex_coords, tex_maps);

/* Set a TEV stage that draws only where the clip texture is > 0 */
GX_SetTevColorIn(stage, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_CPREV);
GX_SetTevColorOp(stage, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
/* Set a logical operation: output = d + ((a OP b) ? c:0) */
GX_SetTevAlphaIn(stage, GX_CA_TEXA, GX_CA_ZERO, GX_CA_APREV, GX_CA_ZERO);
GX_SetTevAlphaOp(stage, GX_TEV_COMP_A8_GT, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GX_SetTevOrder(stage, tex_coord, tex_map, GX_COLORNULL);

/* Setup a texture coordinate transformation that applies the vertex
* coordinates to the clip plane equation, therefore resulting in a texture
* coordinate that is >= 0 if the clip equation is satisfied, and < 0 if
* it's not. */
Mtx m;
Mtx planes;
set_gx_mtx_rowv(0, planes, glparamstate.clip_planes[plane_index0]);
if (plane_index1 >= 0) {
set_gx_mtx_rowv(1, planes, glparamstate.clip_planes[plane_index1]);
} else {
/* Add an equation which is always satisfied (in theory, a plane with
* all four coefficients set to zero is also always >=0, but with a 0
* coordinate the TEV ends up sampling the wrong texel, since we are
* just in the middle of two texels; returning a value strictly greater
* than zero ensures that we end up in the right quadrant) */
set_gx_mtx_row(1, planes, 0.0f, 0.0f, 0.0f, 1.0f);
}
guMtxConcat(planes, glparamstate.modelview_matrix, m);
/* Our texture has coordinates [0,1]x[0,1] and is made of four texels. The
* centre of our texture is (0.5, 0.5), therefore we need to map the zero
* point to that. We do that by translating the texture coordinates by 0.5.
*/
guMtxTransApply(m, m, 0.5, 0.5, 0);
GX_LoadTexMtxImm(m, tex_mtx, GX_MTX2x4);

GX_SetTexCoordGen(tex_coord, GX_TG_MTX2x4, GX_TG_POS, tex_mtx);
glparamstate.dirty.bits.dirty_texture_gen = 1;

++(*stages);
++(*tex_coords);
++(*tex_mtxs);
return true;
}

static void mtx44_multiply(const ClipPlane in, const Mtx44 m, ClipPlane out)
{
for (int i = 0; i < 4; i++) {
out[i] = in[0]*m[0][i] + in[1]*m[1][i] + in[2]*m[2][i] + in[3]*m[3][i];
}
}

void _ogx_clip_setup_tev(int *stages, int *tex_coords,
int *tex_maps, int *tex_mtxs)
{
debug(OGX_LOG_CLIPPING, "setting up clip TEV");
load_clip_texture(*tex_maps);

int plane_index0 = -1;
for (int i = 0; i < MAX_CLIP_PLANES; i++) {
if (!(glparamstate.clip_plane_mask & (1 << i))) continue;

if (plane_index0 < 0) {
plane_index0 = i;
} else {
/* We found two enabled planes, we can setup a TEV stage for them. */
setup_tev(stages, tex_coords, *tex_maps, tex_mtxs,
plane_index0, i);
plane_index0 = -1;
}
}

if (plane_index0 >= 0) {
/* We have an odd number of clip planes */
setup_tev(stages, tex_coords, *tex_maps, tex_mtxs,
plane_index0, -1);
}
++(*tex_maps);
}

void _ogx_clip_enabled(int plane)
{
glparamstate.clip_plane_mask |= 1 << plane;
glparamstate.dirty.bits.dirty_clip_planes = 1;
}

void _ogx_clip_disabled(int plane)
{
glparamstate.stencil.enabled &= ~(1 << plane);
glparamstate.dirty.bits.dirty_clip_planes = 1;
}

void glClipPlane(GLenum plane, const GLdouble *equation)
{
if (plane < GL_CLIP_PLANE0 || plane - GL_CLIP_PLANE0 >= MAX_CLIP_PLANES) {
set_error(GL_INVALID_ENUM);
return;
}

ClipPlane *p = &glparamstate.clip_planes[plane - GL_CLIP_PLANE0];
Mtx44 mv, mv_inverse;
memcpy(mv, glparamstate.modelview_matrix, sizeof(Mtx));
/* Fill in last row */
mv[3][0] = mv[3][1] = mv[3][2] = 0.0f;
mv[3][3] = 1.0f;
/* TODO: cache the inverse matrix, since planes are likely to be specified
* all at the same time */
guMtx44Inverse(mv, mv_inverse);
ClipPlane p0 = { equation[0], equation[1], equation[2], equation[3] };
mtx44_multiply(p0, mv_inverse, *p);
}
46 changes: 46 additions & 0 deletions src/clip.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*****************************************************************************
Copyright (c) 2011 David Guillen Fandos ([email protected])
Copyright (c) 2024 Alberto Mardegan ([email protected])
All rights reserved.

Attention! Contains pieces of code from others such as Mesa and GRRLib

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of copyright holders nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/

#ifndef OPENGX_CLIP_H
#define OPENGX_CLIP_H

#include <GL/gl.h>
#include <malloc.h>
#include <stdbool.h>

void _ogx_clip_enabled(int plane);
void _ogx_clip_disabled(int plane);

void _ogx_clip_setup_tev(int *stages, int *tex_coords,
int *tex_maps, int *tex_mtxs);

#endif /* OPENGX_CLIP_H */
1 change: 1 addition & 0 deletions src/debug.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ static const struct {
} s_feature_masks[] = {
{ "warning", OGX_LOG_WARNING },
{ "call-lists", OGX_LOG_CALL_LISTS },
{ "clipping", OGX_LOG_CLIPPING },
{ "lighting", OGX_LOG_LIGHTING },
{ "texture", OGX_LOG_TEXTURE },
{ "stencil", OGX_LOG_STENCIL },
Expand Down
1 change: 1 addition & 0 deletions src/debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ typedef enum {
OGX_LOG_LIGHTING = 1 << 2,
OGX_LOG_TEXTURE = 1 << 3,
OGX_LOG_STENCIL = 1 << 4,
OGX_LOG_CLIPPING = 1 << 5,
} OgxLogMask;

extern OgxLogMask _ogx_log_mask;
Expand Down
Loading