| name | opengl-rendering |
| description | Core guidelines, coordinate transformation mathematics, high-performance drawing techniques, and state recovery patterns for OpenGL rendering in Starsector Ship Editor. |
OpenGL Rendering in Starsector Ship Editor
This skill provides comprehensive instructions for agents modifying or adding drawing capabilities using the custom LWJGL 3 OpenGL pipeline.
Skill Directory Structure
This skill is organized as follows:
SKILL.md: Main instructions (this file).
resources/: Documents, assets, and guides.
- rendering_migration_guide.md: The full details of the OpenGL migration, coordinate spaces, shaders, and quads.
- rendering_pipeline.md: Comprehensive documentation of the rendering pipeline, PaintOrderController, SpriteRenderer, ShapeRenderer, and Transform Math.
examples/: Code references.
scripts/: Tooling.
Rendering Architecture Overview
The rendering context is integrated within Swing components:
PrimaryViewer.java: Host container enclosing the AWTGLCanvas. Sets up the viewport, projection, and view matrices, then triggers repaints.
PaintOrderController.java: Orchestrates the draw order (background -> grid axes -> layers -> guides -> hotkeys).
SpriteRenderer.java: Handles textured sprite quads.
ShapeRenderer.java: Handles UI, overlays, grids, lines, rectangles, and circle geometry.
Coordinate & Transformation Math
Always ensure correct coordinate mapping between World Space and Screen/NDC spaces:
- Viewport/Projection: OpenGL Y-coordinates increase upwards, while Swing/AWT increases downwards. An orthographic projection maps NDC space to screen pixels:
projectionMatrix.setOrtho(0.0f, getWidth(), getHeight(), 0.0f, -1.0f, 1.0f);
- World-to-Screen: Camera translation/zoom are tracked as
AffineTransform and mapped to Matrix4f using column-major mapping (translations mapped to column 3).
- Rotations: To rotate a sprite around a custom anchor point in world coordinates:
$$M = T(\text{rotAnchor}) \cdot R(\theta) \cdot T(\text{position} - \text{rotAnchor}) \cdot S(\text{size})$$
Best Practices for High-Performance Rendering
To maintain a smooth 60 FPS viewport, adhere to the following rules:
- Trig-Free Render Loops: Never compute
Math.cos or Math.sin inside render calls. Utilize pre-calculated unit-circle coordinates:
private static final int CIRCLE_SEGMENTS = 64;
private static final float[] UNIT_CIRCLE_COS = new float[CIRCLE_SEGMENTS];
private static final float[] UNIT_CIRCLE_SIN = new float[CIRCLE_SEGMENTS];
static {
for (int i = 0; i < CIRCLE_SEGMENTS; i++) {
double theta = 2.0 * Math.PI * i / CIRCLE_SEGMENTS;
UNIT_CIRCLE_COS[i] = (float) Math.cos(theta);
UNIT_CIRCLE_SIN[i] = (float) Math.sin(theta);
}
}
- Allocation-Free Drawing: Avoid allocating memory (like direct arrays or FloatBuffers) dynamically during rendering frames. Instead, allocate a persistent native buffer at initialization and reuse it:
private final java.nio.FloatBuffer circleBuffer = org.lwjgl.system.MemoryUtil.memAllocFloat(CIRCLE_SEGMENTS * 3 * 2);
Remember to free the memory in the cleanup() method of the renderer.
- Double-Pass Opacity Rendering: For UI elements like collision/shield bubbles, perform rendering in two passes for high visual fidelity:
- Pass 1 (Interior Fill): Use low opacity (e.g.
parentPainter.getPaintOpacity()).
- Pass 2 (Boundary Ring Outline): Draw the outline with a distinct line width (e.g.
glLineWidth(3.0f)) and higher opacity (e.g. 0.5f).
Robust Drawing State & Error Recovery
Any Swing paint error or unhandled runtime exception can break the begin()/end() drawing state block. To prevent cascading rendering crashes (IllegalStateException loops), ShapeRenderer utilizes automatic recovery:
Coordinate Systems & Legacy Java2D Porting
The original Ship-Editor repository (by ontheheaven) used Java2D Graphics2D rendering. In that architecture, mathematical points (like ShipCenterPoint or WeaponSlotPoint) were never rotated. Instead, PaintOrderController rotated the entire global canvas AffineTransform matrix, and Java2D drew the unrotated points at a rotated angle on the screen.
When porting to OpenGL, the global view matrix rotation was intentionally dropped from PaintOrderController in favor of component-level transformations. Because of this architectural shift:
- Explicit Point Rotation: You must explicitly rotate the physical points (using
AffineTransform mathematics) when a layer or module is rotated, otherwise the points will remain at 0 degrees while the sprite rotates beneath them.
- Stable Pivot Calculations: When manually rotating points, be extremely careful about pivot anchor calculations (like
getRotationAnchor()). If a pivot calculation relies on a point (like ShipCenterPoint) that is being physically moved during rotation, the pivot will become unstable. You must ensure that pivot logic calculates the anchor before points are moved (using the old angle) or mathematically rotates the offsets to construct a perfectly stationary pivot. Failure to do so will cause sprite coordinates to desync and fly apart.