- Consider integrating with FriCAS or similar CAS software so that
formula parsing and computation happens there.
-* Study and apply where applicable
++ Study and apply where applicable
:PROPERTIES:
:CUSTOM_ID: study-and-apply
:END:
http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html
+ Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/
+
+* Sixth 3D Demos
+
+** Text editors demo
+
++ Improve focus handling:
+ + Perhaps add shortcut to navigate world without exiting entire
+ stack of focus.
+ + Possibility to retain and reuse recently focused elements.
+ + Store user location in the world and view direction with the
+ focused window. So that when returning focus to far away object,
+ user is redirected also to proper location in the world.
++ Possibility to store recently visited locations in the world and
+ return to them.
+
+** Math graphs demo
+
++ Instead of projecting 2D visualizations onto 3D space, visualize
+ some formula using all 3 dimensions available.
* The label showing tessellated polygon count.
*/
private final JLabel tessellationPolygonCountLabel;
+ /**
+ * The label showing current render thread count.
+ */
+ private final JLabel renderThreadsLabel;
/**
* Timer for periodic updates.
*/
tessellationPolygonCountLabel = new JLabel("0");
tessellationPolygonCountLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ // Initialize render threads label
+ renderThreadsLabel = new JLabel(String.valueOf(viewPanel.getNumRenderThreads()));
+ renderThreadsLabel.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());
topPanel.add(createCullingPanel());
topPanel.add(createTessellationPanel());
+ topPanel.add(createRenderThreadsPanel());
add(topPanel, BorderLayout.NORTH);
logArea = new JTextArea(15, 60);
return panel;
}
+ private JPanel createRenderThreadsPanel() {
+ final JPanel panel = new JPanel();
+ panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+ panel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(0, 8, 8, 8),
+ BorderFactory.createTitledBorder("Render Threads")
+ ));
+
+ final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2));
+ statsRow.add(new JLabel("Active:"));
+ statsRow.add(renderThreadsLabel);
+ statsRow.add(new JLabel(" Available cores:"));
+ statsRow.add(new JLabel(String.valueOf(Runtime.getRuntime().availableProcessors())));
+
+ panel.add(statsRow);
+
+ return panel;
+ }
+
private JPanel createButtonPanel() {
final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
updateCameraLabel();
updateCullingStatistics();
updateTessellationStatistics();
+ updateRenderThreadsLabel();
updateLogDisplay();
} finally {
updating = false;
tessellationPolygonCountLabel.setText(String.valueOf(controller.getLastPolygonCount()));
}
+ private void updateRenderThreadsLabel() {
+ if (viewPanel == null) {
+ return;
+ }
+ renderThreadsLabel.setText(String.valueOf(viewPanel.getNumRenderThreads()));
+ }
+
private void updateLogDisplay() {
final List<String> entries = debugLogBuffer.getEntries();
final StringBuilder sb = new StringBuilder();
/**
* Number of horizontal segments for parallel rendering.
* Each segment is rendered by a separate thread.
+ * Configurable at runtime via {@link ViewPanel#setNumRenderThreads(int)}.
*/
- public static final int NUM_RENDER_SEGMENTS = 8;
+ public final int numRenderSegments;
/**
* Java2D graphics context for drawing text, anti-aliased shapes, and other
/**
* Creates a new rendering context for full-screen rendering.
*
- * <p>Equivalent to {@code RenderingContext(width, height, 0, height)}.</p>
+ * <p>Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.</p>
*
- * @param width the rendering area width in pixels
- * @param height the rendering area height in pixels
+ * @param width the rendering area width in pixels
+ * @param height the rendering area height in pixels
+ * @param numRenderSegments number of parallel render segments (threads)
*/
- public RenderingContext(final int width, final int height) {
- this(width, height, 0, height);
+ public RenderingContext(final int width, final int height, final int numRenderSegments) {
+ this(width, height, 0, height, numRenderSegments);
}
/**
* <p>Initializes the offscreen image buffer, extracts the raw pixel byte array,
* and configures anti-aliasing on the Graphics2D context.</p>
*
- * @param width the rendering area width in pixels
- * @param height the rendering area height in pixels
- * @param renderMinY minimum Y coordinate (inclusive) to render
- * @param renderMaxY maximum Y coordinate (exclusive) to render
+ * @param width the rendering area width in pixels
+ * @param height the rendering area height in pixels
+ * @param renderMinY minimum Y coordinate (inclusive) to render
+ * @param renderMaxY maximum Y coordinate (exclusive) to render
+ * @param numRenderSegments number of parallel render segments (threads)
*/
public RenderingContext(final int width, final int height,
- final int renderMinY, final int renderMaxY) {
+ final int renderMinY, final int renderMaxY,
+ final int numRenderSegments) {
this.width = width;
this.height = height;
this.renderMinY = renderMinY;
this.renderMaxY = renderMaxY;
+ this.numRenderSegments = numRenderSegments;
this.centerCoordinate = new Point2D(width / 2d, height / 2d);
this.projectionScale = width / 3d;
this.height = parent.height;
this.renderMinY = renderMinY;
this.renderMaxY = renderMaxY;
+ this.numRenderSegments = parent.numRenderSegments;
this.centerCoordinate = parent.centerCoordinate;
this.projectionScale = parent.projectionScale;
this.bufferedImage = parent.bufferedImage;
* @return array of Graphics2D objects, one per segment
*/
private Graphics2D[] createSegmentGraphics() {
- final Graphics2D[] contexts = new Graphics2D[NUM_RENDER_SEGMENTS];
- final int segmentHeight = height / NUM_RENDER_SEGMENTS;
+ final Graphics2D[] contexts = new Graphics2D[numRenderSegments];
+ final int segmentHeight = height / numRenderSegments;
- for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) {
+ for (int i = 0; i < numRenderSegments; i++) {
final int minY = i * segmentHeight;
- final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
+ final int maxY = (i == numRenderSegments - 1) ? height : (i + 1) * segmentHeight;
final Graphics2D g = bufferedImage.createGraphics();
g.setClip(0, minY, width, maxY - minY);
* Returns the Graphics2D context for a specific render segment.
* Each segment's Graphics2D is pre-clipped to its Y bounds.
*
- * @param segmentIndex the segment index (0 to NUM_RENDER_SEGMENTS-1)
+ * @param segmentIndex the segment index (0 to numRenderSegments-1)
* @return the Graphics2D for that segment
* @throws NullPointerException if called on a segment view (not the main context)
*/
* @param parent the parent rendering context to delegate to
* @param renderMinY minimum Y coordinate (inclusive) for this segment
* @param renderMaxY maximum Y coordinate (exclusive) for this segment
- * @param segmentIndex the index of this segment (0 to NUM_RENDER_SEGMENTS-1)
+ * @param segmentIndex the index of this segment (0 to numRenderSegments-1)
*/
public SegmentRenderingContext(final RenderingContext parent,
final int renderMinY, final int renderMaxY,
/** The set of frame listeners notified before each frame. */
private final Set<FrameListener> frameListeners = ConcurrentHashMap.newKeySet();
/** The executor service for parallel rendering. */
- private final ExecutorService renderExecutor = Executors.newFixedThreadPool(RenderingContext.NUM_RENDER_SEGMENTS);
+ private ExecutorService renderExecutor = Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors(), 4));
+ /** Number of render threads. Can be changed at runtime via {@link #setNumRenderThreads(int)}. */
+ private volatile int numRenderThreads = Math.min(Runtime.getRuntime().availableProcessors(), 4);
/** The background color of the view. */
public Color backgroundColor = Color.BLACK;
private void renderFrame() {
ensureBufferStrategy();
+ ensureExecutorMatchesThreadCount();
if (bufferStrategy == null || renderingContext == null) {
debugLogBuffer.log("[VIEWPANEL] renderFrame ABORT: bufferStrategy=" + bufferStrategy + ", renderingContext=" + renderingContext);
rootShapeCollection.sortShapes();
// Phase 4: Clear and paint segments in parallel
+ final int segments = renderingContext.numRenderSegments;
final int height = renderingContext.height;
- final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS;
- final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[RenderingContext.NUM_RENDER_SEGMENTS];
- final CountDownLatch latch = new CountDownLatch(RenderingContext.NUM_RENDER_SEGMENTS);
+ final int segmentHeight = height / segments;
+ final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[segments];
+ final CountDownLatch latch = new CountDownLatch(segments);
- for (int i = 0; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) {
+ for (int i = 0; i < segments; i++) {
final int segmentIndex = i;
final int minY = i * segmentHeight;
- final int maxY = (i == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
+ final int maxY = (i == segments - 1) ? height : (i + 1) * segmentHeight;
segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY, segmentIndex);
final int[] pixels = renderingContext.pixels;
final int width = renderingContext.width;
final int red = (255 << 16); // Red in RGB format: R=255, G=0, B=0
- for (int i = 1; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) {
+ for (int i = 1; i < segments; i++) {
final int offset = i * segmentHeight * width;
Arrays.fill(pixels, offset, offset + width, red);
}
targetFPS = frameRate;
}
+ /**
+ * Returns the current number of render threads.
+ *
+ * @return the number of render threads
+ */
+ public int getNumRenderThreads() {
+ return numRenderThreads;
+ }
+
+ /**
+ * Sets the number of render threads. Takes effect on the next frame.
+ * The executor service is recreated lazily when the render loop detects the change.
+ *
+ * @param count number of render threads (must be at least 1)
+ */
+ public void setNumRenderThreads(final int count) {
+ if (count < 1)
+ throw new IllegalArgumentException("Render thread count must be at least 1, got: " + count);
+ numRenderThreads = count;
+ viewRepaintNeeded = true;
+ }
+
+ /**
+ * Recreates the executor service if the thread count has changed since last creation.
+ * Called from the render thread only (no synchronization needed).
+ */
+ private void ensureExecutorMatchesThreadCount() {
+ if (renderExecutor == null || renderExecutor.isShutdown()) {
+ renderExecutor = Executors.newFixedThreadPool(numRenderThreads);
+ return;
+ }
+ if (renderExecutor instanceof java.util.concurrent.ThreadPoolExecutor) {
+ final java.util.concurrent.ThreadPoolExecutor tpe = (java.util.concurrent.ThreadPoolExecutor) renderExecutor;
+ if (tpe.getCorePoolSize() != numRenderThreads) {
+ tpe.shutdown();
+ renderExecutor = Executors.newFixedThreadPool(numRenderThreads);
+ }
+ }
+ }
+
/**
* Stops rendering of this view.
*/
return;
}
- // create new rendering context if window size has changed
+ // create new rendering context if window size has changed OR thread count has changed
if ((renderingContext == null)
|| (renderingContext.width != panelWidth)
- || (renderingContext.height != panelHeight)) {
+ || (renderingContext.height != panelHeight)
+ || (renderingContext.numRenderSegments != numRenderThreads)) {
if (renderingContext != null) {
renderingContext.dispose();
}
- renderingContext = new RenderingContext(panelWidth, panelHeight);
+ renderingContext = new RenderingContext(panelWidth, panelHeight, numRenderThreads);
renderingContext.developerTools = developerTools;
renderingContext.debugLogBuffer = debugLogBuffer;
renderingContext.lightingManager = lightingManager;