feat(math,gui): add full euler rotation and developer camera tools
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 23 Mar 2026 18:30:37 +0000 (20:30 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 23 Mar 2026 18:30:37 +0000 (20:30 +0200)
Add yaw/pitch/roll Euler angle support to Quaternion and Transform,
enabling complete 3-axis camera orientation. The developer tools panel
now displays real-time camera position and rotation with a copy button
for saving viewpoints to source code.

Fix mouse drag to initialize from current camera orientation, preventing
jumps when camera was programmatically positioned with non-default
rotation.

Optimize alpha blending in rendering loops by replacing division with
bit-shift and pre-multiplying source colors. Add bounds clamping and
Arrays.fill optimization to TextureBitmap.fillRect.

12 files changed:
TODO.org
doc/perspective-correct-textures/index.org
src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java

index a186422..e5c386d 100644 (file)
--- a/TODO.org
+++ b/TODO.org
@@ -43,7 +43,6 @@ By default, suggest using half of the available CPU cores.
 Extend it to display all available primitive shapes with labels,
 documenting each shape and its parameters.
 
-** Use shaded polygons for valumetric actree demo
 * Performance
 :PROPERTIES:
 :CUSTOM_ID: performance
@@ -74,10 +73,19 @@ between threads.
 :CUSTOM_ID: investigate-performance-optimizations
 :END:
 Focus on critical pixel fill loops:
-- Textured polygon
-- Flat polygon
-- Line
-- Billboard
+
+
+| Status | Class            | Hot Method(s)                                                                        | Buffer Type  |
+|--------+------------------+--------------------------------------------------------------------------------------+--------------|
+| DONE   | TextureBitmap    | drawPixel(), fillColor()                                                             | int[] (ARGB) |
+| TODO   | SolidPolygon     | drawHorizontalLine() (scanline inner loop)                                           | int[]        |
+| TODO   | TexturedPolygon  | drawHorizontalLine() (texture sampling inner loop)                                   | int[]        |
+| TODO   | Line             | drawHorizontalLine(), drawSinglePixelHorizontalLine(), drawSinglePixelVerticalLine() | int[]        |
+| TODO   | Billboard        | paint() (nested loop over pixels)                                                    | int[]        |
+| TODO   | GlowingPoint     | createTexture()                                                                      | int[]        |
+| TODO   | Texture          | downscaleBitmap(), upscaleBitmap(), avg2(), avg4()                                   | int[]        |
+| TODO   | RenderingContext | Constructor (extracts pixels from DataBufferInt)                                     | int[]        |
+
 
 ** Dynamically resize horizontal per-CPU core slices based on their complexity
 
@@ -100,10 +108,6 @@ Focus on critical pixel fill loops:
 :PROPERTIES:
 :CUSTOM_ID: add-polygon-reduction-lod
 :END:
-** Make it easy to copy current camera position in developer tools
-It would be nice to fly around and choose good viewpoint and then copy
-camera view position into application source code, so that next time
-application starts already at that pre-chosen location
 ** Add object fading based on view distance
 :PROPERTIES:
 :CUSTOM_ID: add-object-fading-view-distance
index 59d0760..35f2cce 100644 (file)
@@ -42,7 +42,7 @@
 </style>
 #+end_export
 
-[[file:index.org][Back to main documentation]]
+[[file:../index.org][Back to main documentation]]
 
 * The problem
 :PROPERTIES:
index 8c1dd37..88a4e56 100644 (file)
@@ -4,10 +4,14 @@
  */
 package eu.svjatoslav.sixth.e3d.gui;
 
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Quaternion;
+
 import javax.swing.*;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import java.awt.*;
+import java.awt.datatransfer.StringSelection;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.WindowAdapter;
@@ -20,6 +24,7 @@ import java.util.List;
  * <p>Opens as a popup window when F12 is pressed. Provides:</p>
  * <ul>
  *   <li>Checkboxes to toggle debug settings</li>
+ *   <li>Camera position display with copy button</li>
  *   <li>A scrollable log viewer showing captured debug output</li>
  *   <li>A button to clear the log buffer</li>
  *   <li>Resizable window with native maximize support</li>
@@ -30,39 +35,52 @@ import java.util.List;
  */
 public class DeveloperToolsPanel extends JFrame {
 
-    private static final int LOG_UPDATE_INTERVAL_MS = 500;
+    private static final int UPDATE_INTERVAL_MS = 200;
 
+    /** The view panel whose camera is being displayed. */
+    private final ViewPanel viewPanel;
     /** The developer tools being controlled. */
     private final DeveloperTools developerTools;
     /** The log buffer being displayed. */
     private final DebugLogBuffer debugLogBuffer;
     /** The text area showing log messages. */
     private final JTextArea logArea;
-    /** Timer for periodic log updates. */
+    /** The label showing camera position. */
+    private final JLabel cameraLabel;
+    /** Timer for periodic updates. */
     private final Timer updateTimer;
-    /** Flag to prevent concurrent log updates. */
+    /** Flag to prevent concurrent updates. */
     private volatile boolean updating = false;
 
     /**
      * Creates and displays a developer tools panel.
      *
      * @param parent           the parent frame (for centering)
+     * @param viewPanel        the view panel whose camera to display
      * @param developerTools   the developer tools to control
      * @param debugLogBuffer   the log buffer to display
      */
-    public DeveloperToolsPanel(final Frame parent, final DeveloperTools developerTools,
-                       final DebugLogBuffer debugLogBuffer) {
+    public DeveloperToolsPanel(final Frame parent, final ViewPanel viewPanel,
+                               final DeveloperTools developerTools,
+                               final DebugLogBuffer debugLogBuffer) {
         super("Developer Tools");
+        this.viewPanel = viewPanel;
         this.developerTools = developerTools;
         this.debugLogBuffer = debugLogBuffer;
 
         setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
         setLayout(new BorderLayout(8, 8));
 
-        final JPanel settingsPanel = createSettingsPanel();
-        add(settingsPanel, BorderLayout.NORTH);
+        cameraLabel = new JLabel(" ");
+        cameraLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+
+        final JPanel topPanel = new JPanel();
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+        topPanel.add(createSettingsPanel());
+        topPanel.add(createCameraPanel());
+        add(topPanel, BorderLayout.NORTH);
 
-        logArea = new JTextArea(20, 60);
+        logArea = new JTextArea(15, 60);
         logArea.setEditable(false);
         logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
         logArea.setBackground(Color.BLACK);
@@ -77,17 +95,17 @@ public class DeveloperToolsPanel extends JFrame {
         pack();
         setLocationRelativeTo(parent);
 
-        updateTimer = new Timer(LOG_UPDATE_INTERVAL_MS, new ActionListener() {
+        updateTimer = new Timer(UPDATE_INTERVAL_MS, new ActionListener() {
             @Override
             public void actionPerformed(final ActionEvent e) {
-                updateLogDisplay();
+                updateDisplay();
             }
         });
 
         addWindowListener(new WindowAdapter() {
             @Override
             public void windowOpened(final WindowEvent e) {
-                updateLogDisplay();
+                updateDisplay();
                 updateTimer.start();
             }
 
@@ -101,7 +119,7 @@ public class DeveloperToolsPanel extends JFrame {
     private JPanel createSettingsPanel() {
         final JPanel panel = new JPanel();
         panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
-        panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+        panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8));
 
         final JCheckBox showBordersCheckbox = new JCheckBox("Show polygon borders");
         showBordersCheckbox.setSelected(developerTools.showPolygonBorders);
@@ -127,6 +145,32 @@ public class DeveloperToolsPanel extends JFrame {
         return panel;
     }
 
+    private JPanel createCameraPanel() {
+        final JPanel panel = new JPanel(new BorderLayout(4, 4));
+        panel.setBorder(BorderFactory.createCompoundBorder(
+                BorderFactory.createEmptyBorder(8, 8, 8, 8),
+                BorderFactory.createTitledBorder("Camera (x, y, z, yaw, pitch, roll)")
+        ));
+
+        panel.add(cameraLabel, BorderLayout.CENTER);
+
+        final JButton copyButton = new JButton("Copy");
+        copyButton.setToolTipText("Copy camera position to clipboard");
+        copyButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(final ActionEvent e) {
+                final String text = cameraLabel.getText();
+                if (text != null && !text.trim().isEmpty()) {
+                    final StringSelection sel = new StringSelection(text);
+                    Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, null);
+                }
+            }
+        });
+        panel.add(copyButton, BorderLayout.EAST);
+
+        return panel;
+    }
+
     private JPanel createButtonPanel() {
         final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
 
@@ -144,24 +188,42 @@ public class DeveloperToolsPanel extends JFrame {
         return panel;
     }
 
-    private void updateLogDisplay() {
+    private void updateDisplay() {
         if (updating) {
             return;
         }
         updating = true;
         try {
-            final List<String> entries = debugLogBuffer.getEntries();
-            final StringBuilder sb = new StringBuilder();
-            for (final String entry : entries) {
-                sb.append(entry).append('\n');
-            }
-            logArea.setText(sb.toString());
-
-            final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent())
-                    .getVerticalScrollBar();
-            vertical.setValue(vertical.getMaximum());
+            updateCameraLabel();
+            updateLogDisplay();
         } finally {
             updating = false;
         }
     }
+
+    private void updateCameraLabel() {
+        if (viewPanel == null) {
+            return;
+        }
+
+        final Camera camera = viewPanel.getCamera();
+        final Point3D pos = camera.getTransform().getTranslation();
+        final double[] angles = camera.getTransform().getRotation().toAngles();
+
+        cameraLabel.setText(String.format("%.2f, %.2f, %.2f, %.2f, %.2f, %.2f",
+                pos.x, pos.y, pos.z, angles[0], angles[1], angles[2]));
+    }
+
+    private void updateLogDisplay() {
+        final List<String> entries = debugLogBuffer.getEntries();
+        final StringBuilder sb = new StringBuilder();
+        for (final String entry : entries) {
+            sb.append(entry).append('\n');
+        }
+        logArea.setText(sb.toString());
+
+        final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent())
+                .getVerticalScrollBar();
+        vertical.setValue(vertical.getMaximum());
+    }
 }
\ No newline at end of file
index 4a78009..2ba5637 100755 (executable)
@@ -86,9 +86,6 @@ public class ViewFrame extends JFrame implements WindowListener {
         setVisible(true);
         validate();
 
-        // Render thread will be started by ViewPanel's componentShown/componentResized listeners
-        // after all frame listeners have been registered by the caller
-
         addResizeListener();
         addWindowListener(this);
     }
index 3377919..165d15d 100755 (executable)
@@ -167,27 +167,22 @@ public class ViewPanel extends Canvas {
             @Override
             public void componentResized(final ComponentEvent e) {
                 viewRepaintNeeded = true;
+                startRenderThreadIfReady();
             }
 
             @Override
             public void componentShown(final ComponentEvent e) {
                 viewRepaintNeeded = true;
+                startRenderThreadIfReady();
             }
         });
     }
 
-    private void maybeStartRenderThread() {
+    private void startRenderThreadIfReady() {
         if (isShowing() && getWidth() > 0 && getHeight() > 0)
             startRenderThread();
     }
 
-    /**
-     * Ensures the render thread is started. Called by ViewFrame after the window is visible.
-     */
-    public void ensureRenderThreadStarted() {
-        maybeStartRenderThread();
-    }
-
     /**
      * Returns the camera representing the viewer's position and orientation.
      *
@@ -320,7 +315,7 @@ public class ViewPanel extends Canvas {
             parent = parent.getParent();
         }
 
-        developerToolsPanel = new DeveloperToolsPanel(parentFrame, developerTools, debugLogBuffer);
+        developerToolsPanel = new DeveloperToolsPanel(parentFrame, this, developerTools, debugLogBuffer);
         developerToolsPanel.setVisible(true);
     }
 
index 2d11012..68cffda 100644 (file)
@@ -44,6 +44,7 @@ public class InputManager implements
     private boolean mouseWithinWindow = false;
     private double cameraYaw = 0;
     private double cameraPitch = 0;
+    private double cameraRoll = 0;
 
     /**
      * Creates an input manager attached to the given view panel.
@@ -243,6 +244,14 @@ public class InputManager implements
 
     @Override
     public void mousePressed(final java.awt.event.MouseEvent e) {
+        // Initialize camera rotation state from current camera orientation.
+        // This prevents a jump when the camera was programmatically positioned
+        // with a non-default rotation before the user started dragging.
+        final Camera camera = viewPanel.getCamera();
+        final double[] angles = camera.getTransform().getRotation().toAngles();
+        cameraYaw = angles[0];
+        cameraPitch = angles[1];
+        cameraRoll = angles[2];
     }
 
     @Override
@@ -277,7 +286,8 @@ public class InputManager implements
                       Math.min( Math.PI / 2 - 0.001, cameraPitch));
 
         final Camera camera = viewPanel.getCamera();
-        camera.getTransform().getRotation().set(Quaternion.fromAngles(cameraYaw, cameraPitch));
+        camera.getTransform().getRotation().set(
+                Quaternion.fromAngles(cameraYaw, cameraPitch, cameraRoll));
 
         mouseDelta.zero();
         return true;
index 42bab87..18f7361 100644 (file)
@@ -127,16 +127,36 @@ public class Quaternion {
      * Creates a quaternion from XZ (yaw) and YZ (pitch) Euler angles.
      *
      * <p>The rotation is composed as yaw (around Y axis) followed by
-     * pitch (around X axis).</p>
+     * pitch (around X axis). No roll rotation is applied.</p>
+     *
+     * <p>For full 3-axis rotation, use {@link #fromAngles(double, double, double)}.</p>
      *
      * @param angleXZ the angle around the XZ axis (yaw) in radians
      * @param angleYZ the angle around the YZ axis (pitch) in radians
      * @return a quaternion representing the combined rotation
      */
     public static Quaternion fromAngles(final double angleXZ, final double angleYZ) {
-        final Quaternion yaw = fromAxisAngle(new Point3D(0, 1, 0), angleXZ);
-        final Quaternion pitch = fromAxisAngle(new Point3D(1, 0, 0), -angleYZ);
-        return pitch.multiply(yaw);
+        return fromAngles(angleXZ, angleYZ, 0);
+    }
+
+    /**
+     * Creates a quaternion from full Euler angles (yaw, pitch, roll).
+     *
+     * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+     * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+     *
+     * @param yaw   rotation around Y axis (horizontal heading) in radians
+     * @param pitch rotation around X axis (vertical tilt) in radians;
+     *              positive values tilt upward
+     * @param roll  rotation around Z axis (bank/tilt) in radians;
+     *              positive values rotate clockwise when looking along +Z
+     * @return a quaternion representing the combined rotation
+     */
+    public static Quaternion fromAngles(final double yaw, final double pitch, final double roll) {
+        final Quaternion qYaw   = fromAxisAngle(new Point3D(0, 1, 0), yaw);
+        final Quaternion qPitch = fromAxisAngle(new Point3D(1, 0, 0), -pitch);
+        final Quaternion qRoll  = fromAxisAngle(new Point3D(0, 0, 1), roll);
+        return qRoll.multiply(qPitch).multiply(qYaw);
     }
 
     /**
@@ -215,4 +235,27 @@ public class Quaternion {
         return toMatrix3x3();
     }
 
+    /**
+     * Extracts Euler angles (yaw, pitch, roll) from this quaternion.
+     *
+     * <p>This is the inverse of {@link #fromAngles(double, double, double)}.
+     * Returns angles in the Y-X-Z Euler order used by this engine.</p>
+     *
+     * @return array of {yaw, pitch, roll} in radians
+     */
+    public double[] toAngles() {
+        final Matrix3x3 m = toMatrix3x3();
+
+        // For Rz(roll) * Rx(-pitch) * Ry(yaw) rotation order:
+        // m21 = sin(pitch)
+        // m20 = cos(pitch)*sin(yaw), m22 = cos(pitch)*cos(yaw)
+        // m01 = -sin(roll)*cos(pitch), m11 = cos(roll)*cos(pitch)
+
+        final double pitch = -Math.asin(Math.max(-1, Math.min(1, m.m21)));
+        final double yaw = -Math.atan2(m.m20, m.m22);
+        final double roll = -Math.atan2(m.m01, m.m11);
+
+        return new double[]{yaw, pitch, roll};
+    }
+
 }
\ No newline at end of file
index 9a75ece..46c628e 100755 (executable)
@@ -48,13 +48,32 @@ public class Transform implements Cloneable {
      * Creates a transform with the specified translation and rotation from Euler angles.
      *
      * @param translation the translation
-     * @param angleXZ     the angle around the XZ axis (yaw) in radians
-     * @param angleYZ     the angle around the YZ axis (pitch) in radians
+     * @param yaw         the angle around the Y axis (horizontal heading) in radians
+     * @param pitch       the angle around the X axis (vertical tilt) in radians
      * @return a new transform with the specified translation and rotation
      */
-    public static Transform fromAngles(final Point3D translation, final double angleXZ, final double angleYZ) {
-        final Transform t = new Transform(translation);
-        t.rotation.set(Quaternion.fromAngles(angleXZ, angleYZ));
+    public static Transform fromAngles(final Point3D translation, final double yaw, final double pitch) {
+        return fromAngles(translation.x, translation.y, translation.z, yaw, pitch, 0);
+    }
+
+    /**
+     * Creates a transform with translation and full Euler rotation.
+     *
+     * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+     * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+     *
+     * @param x     translation X coordinate
+     * @param y     translation Y coordinate
+     * @param z     translation Z coordinate
+     * @param yaw   rotation around Y axis (horizontal heading) in radians
+     * @param pitch rotation around X axis (vertical tilt) in radians
+     * @param roll  rotation around Z axis (bank/tilt) in radians
+     * @return a new transform with the specified translation and rotation
+     */
+    public static Transform fromAngles(final double x, final double y, final double z,
+                                       final double yaw, final double pitch, final double roll) {
+        final Transform t = new Transform(new Point3D(x, y, z));
+        t.rotation.set(Quaternion.fromAngles(yaw, pitch, roll));
         return t;
     }
 
@@ -118,4 +137,27 @@ public class Transform implements Cloneable {
         this.translation.z = translation.z;
     }
 
+    /**
+     * Sets both translation and rotation from Euler angles.
+     *
+     * <p>Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard
+     * Y-X-Z Euler order commonly used for object placement in 3D scenes.</p>
+     *
+     * @param x     translation X coordinate
+     * @param y     translation Y coordinate
+     * @param z     translation Z coordinate
+     * @param yaw   rotation around Y axis (horizontal heading) in radians
+     * @param pitch rotation around X axis (vertical tilt) in radians
+     * @param roll  rotation around Z axis (bank/tilt) in radians
+     * @return this transform for chaining
+     */
+    public Transform set(final double x, final double y, final double z,
+                         final double yaw, final double pitch, final double roll) {
+        translation.x = x;
+        translation.y = y;
+        translation.z = z;
+        rotation.set(Quaternion.fromAngles(yaw, pitch, roll));
+        return this;
+    }
+
 }
\ No newline at end of file
index 83acbb0..72c38fa 100644 (file)
@@ -143,9 +143,9 @@ public class Line extends AbstractCoordinateShape {
             final int destG = (dest >> 8) & 0xff;
             final int destB = dest & 0xff;
 
-            final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) / 256;
-            final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) / 256;
-            final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) / 256;
+            final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) >> 8;
+            final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) >> 8;
+            final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) >> 8;
 
             pixels[offset++] = (newR << 16) | (newG << 8) | newB;
 
@@ -210,9 +210,9 @@ public class Line extends AbstractCoordinateShape {
                             final int destG = (dest >> 8) & 0xff;
                             final int destB = dest & 0xff;
 
-                            final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
-                            final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
-                            final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+                            final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+                            final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+                            final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
 
                             pixels[offset] = (newR << 16) | (newG << 8) | newB;
                         }
@@ -278,9 +278,9 @@ public class Line extends AbstractCoordinateShape {
                             final int destG = (dest >> 8) & 0xff;
                             final int destB = dest & 0xff;
 
-                            final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
-                            final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
-                            final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+                            final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+                            final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+                            final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
 
                             pixels[offset] = (newR << 16) | (newG << 8) | newB;
                         }
index 164ae54..fb729c6 100644 (file)
@@ -115,9 +115,9 @@ public class SolidPolygon extends AbstractCoordinateShape {
                 final int destG = (dest >> 8) & 0xff;
                 final int destB = dest & 0xff;
 
-                final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
-                final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
-                final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
+                final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+                final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+                final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
 
                 pixels[offset++] = (newR << 16) | (newG << 8) | newB;
             }
index 094ec42..477c30b 100644 (file)
@@ -161,18 +161,18 @@ public class TexturedPolygon extends AbstractCoordinateShape {
                 } else {
                     final int backgroundAlpha = 255 - srcAlpha;
 
-                    final int srcR = (srcPixel >> 16) & 0xff;
-                    final int srcG = (srcPixel >> 8) & 0xff;
-                    final int srcB = srcPixel & 0xff;
+                    final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha;
+                    final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha;
+                    final int srcB = (srcPixel & 0xff) * srcAlpha;
 
                     final int destPixel = renderBufferPixels[renderBufferOffset];
                     final int destR = (destPixel >> 16) & 0xff;
                     final int destG = (destPixel >> 8) & 0xff;
                     final int destB = destPixel & 0xff;
 
-                    final int r = ((destR * backgroundAlpha) + (srcR * srcAlpha)) / 256;
-                    final int g = ((destG * backgroundAlpha) + (srcG * srcAlpha)) / 256;
-                    final int b = ((destB * backgroundAlpha) + (srcB * srcAlpha)) / 256;
+                    final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+                    final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+                    final int b = ((destB * backgroundAlpha) + srcB) >> 8;
 
                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
                 }
index 8726bc5..ac0e1f0 100644 (file)
@@ -97,6 +97,9 @@ public class TextureBitmap {
      * <p>This texture stores pixels in ARGB format. The target is RGB format (no alpha).
      * Alpha blending is performed based on the source pixel's alpha value.</p>
      *
+     * <p><b>Performance note:</b> Uses bit-shift instead of division for alpha blending,
+     * and pre-multiplies source colors to reduce per-pixel operations.</p>
+     *
      * @param sourcePixelAddress Pixel index within current texture.
      * @param targetBitmap       Target RGB pixel array.
      * @param targetPixelAddress Pixel index within target image.
@@ -117,22 +120,71 @@ public class TextureBitmap {
 
         final int backgroundAlpha = 255 - textureAlpha;
 
-        final int srcR = (sourcePixel >> 16) & 0xff;
-        final int srcG = (sourcePixel >> 8) & 0xff;
-        final int srcB = sourcePixel & 0xff;
+        // Pre-multiply source colors by alpha to reduce operations in blend
+        final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha;
+        final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha;
+        final int srcB = (sourcePixel & 0xff) * textureAlpha;
 
         final int destPixel = targetBitmap[targetPixelAddress];
         final int destR = (destPixel >> 16) & 0xff;
         final int destG = (destPixel >> 8) & 0xff;
         final int destB = destPixel & 0xff;
 
-        final int r = ((destR * backgroundAlpha) + (srcR * textureAlpha)) / 256;
-        final int g = ((destG * backgroundAlpha) + (srcG * textureAlpha)) / 256;
-        final int b = ((destB * backgroundAlpha) + (srcB * textureAlpha)) / 256;
+        // Use bit-shift instead of division for faster blending
+        final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+        final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+        final int b = ((destB * backgroundAlpha) + srcB) >> 8;
 
         targetBitmap[targetPixelAddress] = (r << 16) | (g << 8) | b;
     }
 
+    /**
+     * Renders a scanline using pre-computed source pixel addresses.
+     *
+     * <p>This variant is optimized for cases where source addresses are computed
+     * externally (e.g., by a caller that already has the stepping logic).
+     * The sourceAddresses array must contain valid indices into {@link #pixels}.</p>
+     *
+     * @param sourceAddresses     array of source pixel addresses (indices into pixels array)
+     * @param targetBitmap        target RGB pixel array
+     * @param targetStartAddress  starting index in the target array
+     * @param pixelCount          number of pixels to render
+     */
+    public void drawScanlineWithAddresses(final int[] sourceAddresses,
+                                          final int[] targetBitmap, final int targetStartAddress,
+                                          final int pixelCount) {
+
+        int targetOffset = targetStartAddress;
+
+        for (int i = 0; i < pixelCount; i++) {
+            final int sourcePixel = pixels[sourceAddresses[i]];
+            final int textureAlpha = (sourcePixel >> 24) & 0xff;
+
+            if (textureAlpha == 255) {
+                targetBitmap[targetOffset] = sourcePixel;
+            } else if (textureAlpha != 0) {
+                final int backgroundAlpha = 255 - textureAlpha;
+
+                final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha;
+                final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha;
+                final int srcB = (sourcePixel & 0xff) * textureAlpha;
+
+                final int destPixel = targetBitmap[targetOffset];
+                final int destR = (destPixel >> 16) & 0xff;
+                final int destG = (destPixel >> 8) & 0xff;
+                final int destB = destPixel & 0xff;
+
+                final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+                final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+                final int b = ((destB * backgroundAlpha) + srcB) >> 8;
+
+                targetBitmap[targetOffset] = (r << 16) | (g << 8) | b;
+            }
+
+            targetOffset++;
+        }
+    }
+
     /**
      * Draws a single pixel at the specified coordinates using the given color.
      *
@@ -154,6 +206,9 @@ public class TextureBitmap {
      * The same applies to {@code y1} and {@code y2}. The rectangle is exclusive of the
      * right and bottom edges.</p>
      *
+     * <p><b>Performance:</b> Uses {@link java.util.Arrays#fill(int[], int, int, int)}
+     * per scanline for optimal JVM-optimized memory writes.</p>
+     *
      * @param x1    the left x coordinate
      * @param y1    the top y coordinate
      * @param x2    the right x coordinate (exclusive)
@@ -175,9 +230,23 @@ public class TextureBitmap {
             y2 = tmp;
         }
 
-        for (int y = y1; y < y2; y++)
-            for (int x = x1; x < x2; x++)
-                drawPixel(x, y, color);
+        // Clamp to bitmap bounds
+        if (x1 < 0) x1 = 0;
+        if (y1 < 0) y1 = 0;
+        if (x2 > width) x2 = width;
+        if (y2 > height) y2 = height;
+
+        final int pixel = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b;
+        final int rowWidth = x2 - x1;
+
+        if (rowWidth <= 0)
+            return;
+
+        // Fill each scanline using Arrays.fill for optimal performance
+        for (int y = y1; y < y2; y++) {
+            final int rowStart = y * width + x1;
+            java.util.Arrays.fill(pixels, rowStart, rowStart + rowWidth, pixel);
+        }
     }
 
     /**