Skip to content

feat: add alpha channel blending support - #7

Open
tslmy wants to merge 6 commits into
jserv:mainfrom
tslmy:alpha
Open

tslmy wants to merge 6 commits into
jserv:mainfrom
tslmy:alpha

Conversation

@tslmy

@tslmy tslmy commented May 20, 2026 •

Copy link
Copy Markdown
  • Add a field to ZBufferPoint (8.16 fixed-point, same as r,g,b)
  • Propagate vertex alpha from glColor4f through the vertex pipeline
  • Add GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA blend factors to TGL_BLEND_FUNC and TGL_BLEND_FUNC_RGB macros
  • Use TGL_ALPHA_MUL helper with (factor + factor>>7) correction to map [0,255] -> [0,256], ensuring alpha=255 is identity and alpha=0 produces exactly zero (no darkening artifacts)
  • Preserve texture alpha through RGB_MIX_FUNC in 32-bit mode
  • Add INTERP_A support to ztriangle.h for per-vertex alpha interpolation
  • Update smooth-shaded blend triangles to interpolate and use alpha
  • Update flat-shaded blend triangles to pack vertex alpha into pixel
  • Update line/point blend paths to pass alpha

Added tests, for both textured and untextured models, to ensure that alpha blending works correctly.

Also added a compilable demo, examples/raw/alpha.c, to illustrate how transparency may look like. It renders a scene like the image below:

alpha_demo

closes #6

Disclaimer: Coding agent (GitHub Copilot + Claude Sonnet 4.6) is heavily used in this PR.

tslmy added 4 commits May 19, 2026 19:06
- Add 'a' field to ZBufferPoint (8.16 fixed-point, same as r,g,b)
- Propagate vertex alpha from glColor4f through the vertex pipeline
- Add GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA blend factors to
  TGL_BLEND_FUNC and TGL_BLEND_FUNC_RGB macros
- Use TGL_ALPHA_MUL helper with (factor + factor>>7) correction to
  map [0,255] -> [0,256], ensuring alpha=255 is identity and alpha=0
  produces exactly zero (no darkening artifacts)
- Preserve texture alpha through RGB_MIX_FUNC in 32-bit mode
- Add INTERP_A support to ztriangle.h for per-vertex alpha interpolation
- Update smooth-shaded blend triangles to interpolate and use alpha
- Update flat-shaded blend triangles to pack vertex alpha into pixel
- Update line/point blend paths to pass alpha
- Add gl_convertRGBA_to_8A8R8G8B() that preserves the alpha channel
  when converting from RGBA byte data to internal 0xAARRGGBB format
- Add gl_resizeImageNoInterpolate4() for resizing 4-channel textures
- Update glopTexImage2D to accept GL_RGBA with components=4
- Texture alpha is preserved through TGL_BLEND_FUNC via the pixel's
  high byte, enabling proper alpha-blended textured geometry
Add 7 tests covering alpha channel rendering correctness:

Untextured:
- alpha_zero_transparent: alpha=0 produces fully transparent output
- alpha_full_opaque: alpha=1 produces fully opaque output
- alpha_half_blend: alpha=0.5 produces correct 50/50 blend
- alpha_smooth_interp: GL_SMOOTH interpolates alpha per-vertex

Textured (GL_RGBA):
- alpha_texture_transparent: RGBA texture with alpha=0 is invisible
- alpha_texture_opaque: RGBA texture with alpha=255 is fully visible
- alpha_texture_half_blend: RGBA texture with alpha=128 blends correctly

All tests use direct framebuffer readback and verify pixel values
with a small tolerance for fixed-point rounding.
@jserv

jserv commented May 20, 2026

Copy link
Copy Markdown
Owner

Please provide sample programs to illustrate the use of newly-introduced APIs.

@tslmy
tslmy marked this pull request as ready for review May 20, 2026 14:46
@tslmy

tslmy commented May 20, 2026

Copy link
Copy Markdown
Author

@jserv good idea. Added a examples/raw/alpha.c file that renders a PNG. Also attached the PNG to a new README as well as the description of this PR.

@jserv

jserv commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Disclaimer: Coding agent (GitHub Copilot + Claude Sonnet 4.6) is heavily used in this PR.

You don't have to state that. As long as you ensure that a human remains in the loop, their use is perfectly acceptable and even encouraged.

Comment thread include/zbuffer.h
#if TGL_HAS(LIT_TEXTURES)
#if TGL_FEATURE_RENDER_BITS == 32
/* Preserve alpha from the texture pixel through the color mix */
#define RGB_MIX_FUNC(rr, gg, bb, tpix) \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 32-bit branch pulls source alpha from tpix & 0xFF000000, but gl_convertRGB_to_8A8R8G8B in src/image_util.c leaves the high byte at zero, so every plain GL_RGB texture carries alpha 0. With glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) those textures become fully transparent and vanish. OpenGL defines alpha as 1.0 for a GL_RGB internal format, so that converter should write 0xFF000000 into the pixel. The new tests only upload GL_RGBA, so this path is uncovered.

Comment thread include/zbuffer.h
sr = GET_REDDER(temp); \
sg = GET_GREENER(temp); \
sb = GET_BLUEER(temp); \
sa = (temp >> 24) & 0xFF; \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sa is read from bits 24..31 of source, which only exist when PIXEL is 32 bits. In a TGL_FEATURE_16_BITS build the textured paths feed this macro the result of RGB_MIX_FUNC, whose non-32-bit branch carries no alpha, so sa is always 0 and every textured primitive disappears under GL_SRC_ALPHA. The flat untextured path escapes this only because color is a GLuint with alpha packed above the pixel width. Either hand alpha to the textured path out of band, or reject the two new factors when TGL_FEATURE_RENDER_BITS == 16.

Comment thread include/zbuffer.h
case GL_ONE: \
default: \
break; \
case GL_SRC_ALPHA: \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GL_SRC_ALPHA is handled only as a source factor and GL_ONE_MINUS_SRC_ALPHA only as a destination factor, so a legal call like glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA) falls through default and silently degrades to GL_ONE, GL_ONE. Both factors are valid in either slot. Mirror the cases into the other switch, here and in TGL_BLEND_FUNC_RGB.

Comment thread include/zbuffer.h
}

#define TGL_BLEND_FUNC_RGB(rr, gg, bb, dest) \
#define TGL_BLEND_FUNC_RGB(rr, gg, bb, aa, dest) \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TGL_BLEND_FUNC_RGB lives in the installed header and goes from four arguments to five, in this definition and in the TGL_FEATURE_BLEND == 0 one further down, so any out-of-tree caller stops compiling. Keep the four-argument form as a wrapper that passes opaque alpha and give the new form its own name, TGL_BLEND_FUNC_RGBA.

Comment thread src/texture.c
im->ysize = height;
#if TGL_FEATURE_RENDER_BITS == 32
gl_convertRGB_to_8A8R8G8B(im->pixmap, pixels1, width, height);
if (is_rgba)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch covers only TGL_FEATURE_RENDER_BITS == 32. The 16-bit #elif below still calls gl_convertRGB_to_5R6G5B, which advances three bytes per texel, while pixels1 now holds four bytes per texel when is_rgba. Texel 1 onward decode across byte boundaries and the whole texture is scrambled in any 16-bit build. The parameter check above accepts GL_RGBA regardless of render depth, so nothing stops it. Add a four-byte-stride conversion, or reject GL_RGBA unless the render depth is 32.

Comment thread src/vertex.c
(GLint) (v->color.v[2] * COLOR_CORRECTED_MULT_MASK + COLOR_MIN_MULT) &
COLOR_MASK;

/* Propagate alpha in the same 8.16 fixed-point format as r,g,b */

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/init.c still sets c->current_color.W = 0.0, so an application that never calls glColor* now propagates vertex alpha 0 through here. Enable glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) and all of its geometry renders fully transparent. OpenGL's initial current color is (1, 1, 1, 1), so current_color.W should be 1.0.

Comment thread src/vertex.c

/* Propagate alpha in the same 8.16 fixed-point format as r,g,b */
v->zp.a =
(GLint) (v->color.v[3] * COLOR_CORRECTED_MULT_MASK + COLOR_MIN_MULT) &

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clipped counterpart of this function, gl_transform_to_viewport_clip_c in src/clip.c, still writes only zp.r, zp.g and zp.b, and the vertices it transforms come from GLinterpolate (loop bound i < 3) and updateTmp (three explicit assignments), neither of which touches color.v[3]. Any primitive crossing a clip plane therefore rasterizes with a stale zp.a, so clipped triangles and lines get arbitrary opacity. Interpolate the fourth component in both helpers and set zp.a in the clip transform.

Comment thread src/zline.c
*pp = RGB_TO_PIXEL(p->r, p->g, p->b);
else
TGL_BLEND_FUNC_RGB(p->r, p->g, p->b, (*pp))
TGL_BLEND_FUNC_RGB(p->r, p->g, p->b, p->a, (*pp))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ZB_plot, the point path. Line rasterization goes through RGBPIXEL in src/zline.h, which writes *pp = RGB_TO_PIXEL(...) without consulting zb->enable_blend, so GL_LINES, line strips and line loops stay opaque with GL_BLEND enabled. Either route those pixels through the blend macro as follow-up work, or drop the line claim from the description.

Comment thread src/ztriangle.c
/* Variant DT0_DW0 */
#undef INTERP_Z
#undef INTERP_RGB
#undef INTERP_A

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INTERP_A is left undefined for every ZB_fillTriangleMappingPerspective_* variant, so interpolated vertex alpha never reaches the blend on textured geometry and source alpha comes purely from the texel. Under the default GL_MODULATE environment a vertex alpha of 0.5 over a texel alpha of 0.5 should give 0.25, and glColor4f alpha on a textured primitive is ignored outright. Defining INTERP_A here and folding oa1 into the packed source alpha is the follow-up this needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: alpha channel emulation

2 participants