Skip to content

RENDERER: Fix animation interpolation at cycle boundaries for alias models#1101

Draft
qqshka wants to merge 1 commit intomasterfrom
fix_alias_models_lerp
Draft

RENDERER: Fix animation interpolation at cycle boundaries for alias models#1101
qqshka wants to merge 1 commit intomasterfrom
fix_alias_models_lerp

Conversation

@qqshka
Copy link
Copy Markdown
Collaborator

@qqshka qqshka commented Feb 14, 2026

Previously, animation interpolation was broken at loop boundaries (e.g.
frame 7 -> frame 0) for both MDL and MD3 models. This caused visible
snapping/popping when an animation cycled back to the beginning.

Root causes:

  1. R_AliasModelDeterminePoses set lerp=1 for non-sequential frame pairs,
    disabling interpolation at cycle boundaries.
  2. R_AliasModelPrepare checked that frame2 == expected_next_frame,
    rejecting legitimate cross-boundary lerp pairs.
  3. Classic GL shaders used precomputed direction vectors (nextFrame -
    currentFrame) baked into VBO at load time. At cycle boundaries these
    vectors point to the wrong target frame, producing garbage geometry.

Classic GL fix (macOS, RENDERER_MODERN_OPENGL=OFF):

  • Remove the non-sequential frame guard in R_AliasModelDeterminePoses
    so lerpfrac = r_framelerp for all frame pairs.
  • Remove the expected_frame check in R_AliasModelPrepare.
  • Change all classic GL alias model vertex shaders from the precomputed-
    direction formula (pos + dir * t) to mix(pos1, pos2, t), where
    gl_MultiTexCoord1 is rebound per draw call to carry pose2 positions
    instead of baked direction vectors.
  • Add GLC_BindPose2TextureCoord[_MD3]() helpers that override the
    VAO's texcoord1 binding after each state setup call (which resets the
    VAO), pointing it at the VBO region for the second pose.
  • When pose2 precedes pose1 in the VBO (cycle boundary where frame2 <
    frame1), swap the two poses and invert lerpfrac to ensure the VBO
    offset (pose2Vert - firstVert) is always non-negative. Swapping is
    mathematically equivalent: mix(a, b, t) == mix(b, a, 1-t).
  • Pass pose2 to GLC_DrawPowerupShell_Program and
    GLC_DrawAliasModelShadowDrawCall_Program (previously only received
    pose1), and apply the same rebind + swap logic there.
  • Inline GLC_DrawCachedAliasOutlineFrame at its call site so that
    GLC_BindPose2TextureCoord can be inserted between the state setup
    and GL_DrawArrays; remove the now-unused function.

Modern GL fix (Linux/Windows, RENDERER_MODERN_OPENGL=ON):

  • Add pose1Base / pose2Base fields to the per-instance uniform block
    so the vertex shader can locate the correct second-pose data.
  • Create a per-model SSBO (r_buffer_aliasmodel_vertex_ssbo) containing
    the full vertex array as raw floats (same data as the VBO).
  • In the vertex shader, use gl_VertexID to index into the SSBO and
    read pose2 position directly, replacing the pos + vboDirection * t
    formula with mix(vboPosition, pose2Position, t).
  • Bind the SSBO in GLM_PrepareAliasModelBatches before the draw.

Affected files:
src/r_aliasmodel.c - lerp guards
src/r_aliasmodel_mesh.c - SSBO creation (modern GL)
src/glc_aliasmodel.c - pose2 rebind helpers, swap logic (MDL)
src/glc_md3.c - pose2 rebind helpers, swap logic (MD3)
src/glm_aliasmodel.c - pose1Base/pose2Base uniforms, SSBO bind
src/glsl/common.glsl - pose1Base/pose2Base in AliasModel struct
src/glsl/constants.glsl - EZQ_GL_BINDINGPOINT_ALIASMODEL_SSBO = 6
src/glsl/draw_aliasmodel.vertex.glsl - SSBO read + mix() (modern GL)
src/glsl/glc/glc_aliasmodel_std.vertex.glsl - mix() formula (classic)
src/glsl/glc/glc_aliasmodel_shadow.vertex.glsl - mix() formula (classic)
src/glsl/glc/glc_aliasmodel_shell.vertex.glsl - mix() formula (classic)

…dels

Previously, animation interpolation was broken at loop boundaries (e.g.
frame 7 -> frame 0) for both MDL and MD3 models. This caused visible
snapping/popping when an animation cycled back to the beginning.

Root causes:
1. `R_AliasModelDeterminePoses` set lerp=1 for non-sequential frame pairs,
   disabling interpolation at cycle boundaries.
2. `R_AliasModelPrepare` checked that frame2 == expected_next_frame,
   rejecting legitimate cross-boundary lerp pairs.
3. Classic GL shaders used precomputed direction vectors (nextFrame -
   currentFrame) baked into VBO at load time. At cycle boundaries these
   vectors point to the wrong target frame, producing garbage geometry.

Classic GL fix (macOS, RENDERER_MODERN_OPENGL=OFF):
- Remove the non-sequential frame guard in `R_AliasModelDeterminePoses`
  so `lerpfrac = r_framelerp` for all frame pairs.
- Remove the `expected_frame` check in `R_AliasModelPrepare`.
- Change all classic GL alias model vertex shaders from the precomputed-
  direction formula (`pos + dir * t`) to `mix(pos1, pos2, t)`, where
  `gl_MultiTexCoord1` is rebound per draw call to carry pose2 positions
  instead of baked direction vectors.
- Add `GLC_BindPose2TextureCoord[_MD3]()` helpers that override the
  VAO's texcoord1 binding after each state setup call (which resets the
  VAO), pointing it at the VBO region for the second pose.
- When pose2 precedes pose1 in the VBO (cycle boundary where frame2 <
  frame1), swap the two poses and invert lerpfrac to ensure the VBO
  offset `(pose2Vert - firstVert)` is always non-negative. Swapping is
  mathematically equivalent: mix(a, b, t) == mix(b, a, 1-t).
- Pass pose2 to `GLC_DrawPowerupShell_Program` and
  `GLC_DrawAliasModelShadowDrawCall_Program` (previously only received
  pose1), and apply the same rebind + swap logic there.
- Inline `GLC_DrawCachedAliasOutlineFrame` at its call site so that
  `GLC_BindPose2TextureCoord` can be inserted between the state setup
  and `GL_DrawArrays`; remove the now-unused function.

Modern GL fix (Linux/Windows, RENDERER_MODERN_OPENGL=ON):
- Add `pose1Base` / `pose2Base` fields to the per-instance uniform block
  so the vertex shader can locate the correct second-pose data.
- Create a per-model SSBO (r_buffer_aliasmodel_vertex_ssbo) containing
  the full vertex array as raw floats (same data as the VBO).
- In the vertex shader, use `gl_VertexID` to index into the SSBO and
  read pose2 position directly, replacing the `pos + vboDirection * t`
  formula with `mix(vboPosition, pose2Position, t)`.
- Bind the SSBO in `GLM_PrepareAliasModelBatches` before the draw.

Affected files:
  src/r_aliasmodel.c              - lerp guards
  src/r_aliasmodel_mesh.c         - SSBO creation (modern GL)
  src/glc_aliasmodel.c            - pose2 rebind helpers, swap logic (MDL)
  src/glc_md3.c                   - pose2 rebind helpers, swap logic (MD3)
  src/glm_aliasmodel.c            - pose1Base/pose2Base uniforms, SSBO bind
  src/glsl/common.glsl            - pose1Base/pose2Base in AliasModel struct
  src/glsl/constants.glsl         - EZQ_GL_BINDINGPOINT_ALIASMODEL_SSBO = 6
  src/glsl/draw_aliasmodel.vertex.glsl  - SSBO read + mix() (modern GL)
  src/glsl/glc/glc_aliasmodel_std.vertex.glsl    - mix() formula (classic)
  src/glsl/glc/glc_aliasmodel_shadow.vertex.glsl - mix() formula (classic)
  src/glsl/glc/glc_aliasmodel_shell.vertex.glsl  - mix() formula (classic)
@ciscon
Copy link
Copy Markdown
Collaborator

ciscon commented Feb 14, 2026

as of now i get a ton of artifacts

@qqshka
Copy link
Copy Markdown
Collaborator Author

qqshka commented Feb 14, 2026

as of now i get a ton of artifacts

yeah, as I mention in description, you probably using vid_rendrer 1, could not test it. Anyway, PR a draft for a reason.

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.

2 participants