Class Viewport3D
RenderTarget (with
MSAA) and composites it as a normal 2D layer, proving that GPU-rendered content
respects the scene's layering: overlays, dialogs, tooltips and clipping all
apply to it because it draws as one quad in the paint order.
For this milestone it shows the backend's built-in demo scene (a spinning,
depth-tested cube). It drives a continuous repaint while showing (like the
animation system) and pauses automatically when hidden; static content should
opt out via setAnimated(false) to repaint only on
invalidation. Its render target is released on detach via
Scene.disposeLater(limn.graphics.GpuSurface) (deferred to a frame with the owning GL context
current).
Size axis: this widget does not participate. Its content is a GPU render
target sized in device pixels by setPreferredSize(float, float) and the layout, not a
control laid out from a metric row, so no ControlSize step
changes what the 3D scene looks like. The one thing that is chrome (the
"no GPU backend" placeholder frame and its message) reads the row resolved on this
widget like every other component, so a viewport dropped into an XSMALL panel does
not fall back with a MEDIUM corner radius and MEDIUM body type.
Direction axis: this widget does not participate either, and for a sharper reason
than the size one. A rendered scene is content, so a right-to-left window shows the same world
from the same side; and rayAt(float, float) takes viewport-local pixels, which are where the pointer
physically is, so picking, orbiting and the camera's normalised device coordinates all stay
physical. Reflecting any of them would put the pick on the far side of the scene from the
cursor. The chrome carve-out is the same one: the placeholder message is shaped for the
direction resolved on this widget, and stays centred, because a centre does not move.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceDraws the 3D scene into a pass each frame;timeSecondsadvances while showing. -
Constructor Summary
ConstructorsConstructorDescriptionAn empty viewport; give it something to draw withsetRenderer(limn.components.Viewport3D.Renderer). -
Method Summary
Modifier and TypeMethodDescriptioncamera()The camera this viewport renders with: mutable, and mutated by any controller.captureNext(Consumer<Image> sink) Hands the next rendered frame of this viewport tosinkas anImage, then forgets it: one capture per call, not a subscription.frameContent(Aabb box) Points the camera atboxand backs it off far enough to hold the whole of it, keeping the current viewing direction and field of view, and moving the clip planes to bracket what it framed.frameContent(Aabb box, float margin) frameContent(Aabb)with an explicit margin, the factor the fitted distance is multiplied by, so 1 touches the edges and 1.1 leaves a tenth of the frame around the content.observeDispose(Runnable cleanup) Observes this viewport being detached, for GPU resources owned by the renderer, typically a retained scene'sScene3D::dispose.protected voidWhat a viewport is to an assistive technology: oneAccessible.Role.CANVASnode over the box the layout gave it, described by the "no GPU backend" message on the frames that paint one and by nothing at all on the frames that render.The application's response to a click (no drag): called with the world-space ray to feed toPicker.protected voidCalled when this widget leaves the scene: detached from the tree, moved into another scene (told this before theWidget.onAttached()of the scene it joins), or still in it when the scene's window closes, which detaches every widget the scene has.protected voidonLayout()Containers position children here (measure +Widget.layoutBox(float, float, float, float)per child).protected SizeonMeasure(Constraints constraints) Reports the size this widget wants withinconstraints.protected voidonMouseEvent(MouseEvent event) Mouse events (bubbling); callevent.consume()when handled.protected voidWidget's own background/content, in local coordinates.rayAt(float localX, float localY) The world-space ray through a viewport-local pixel (for picking).floatThe fraction of device resolution the 3D content is rendered at; 1 by default.setAnimated(boolean animated) Whether the viewport drives a continuous repaint while showing (defaulttrue: right for content that moves every frame;timeSecondskeeps advancing).Replaces the camera.setController(@Nullable CameraController controller) Installs a camera controller (e.g.setPreferredSize(float width, float height) The size this viewport asks for, in logical points.setRenderer(@Nullable Viewport3D.Renderer renderer) Sets the render callback; without one, the built-in demo cube is shown.setRenderScale(float scale) Renders the 3D content at a fraction of the viewport's device resolution and upsamples it into the widget's box.Methods inherited from class limn.scene.Widget
acceptsTextInput, accessibleLabelAccessKey, accessibleLabelTarget, accessibleLabelText, add, add, announceChange, backdropReach, baselineOffset, baselineOffsetOf, caretRect, children, clipboard, clipHeight, clipsChildren, clipWidth, clipX, clipY, controlSize, cursor, declaredControlSize, declaredLayoutDirection, declaredLocale, focusArrivedBackward, focusArrivedByTraversal, handleUserChange, height, hitTest, hitTestChild, id, imageCursor, inheritanceHost, invalidate, invalidate, invalidateAccessible, isEnabled, isFocusable, isFocused, isRightToLeft, isSelfEnabled, isShowing, isVisible, isVisibleByItsOwnFlag, layoutBox, layoutDirection, locale, localToScene, localToSceneBounds, markNeedsContainedLayout, markNeedsLayout, markNeedsLayoutInPlace, maxHeight, maxWidth, measure, minHeight, minWidth, moveChild, moveChild, moveChild, neutralBase, notifyChange, notifyTextEdit, observeChanges, onAccessibilityAction, onAccessibilityChild, onAccessibilityChildAction, onAccessibilityChildIdentity, onAttached, onCanEdit, onCharTyped, onDragExit, onDragOver, onDragStart, onDrop, onEdit, onFocusGained, onFocusLost, onHitTest, onKeyEvent, onPaintOverlay, onPreedit, onSyntheticAction, overlayPassesPointer, paintChildren, paintOutset, paintsDecoration, paintsFromBackdrop, paintWidget, parent, remove, remove, requestFocus, requestFocus, revealInView, revealInView, runHandler, scene, sceneNanos, sceneToLocal, self, setAccessibleDescribedBy, setAccessibleDescription, setAccessibleDescription, setAccessibleIgnored, setAccessibleLabelledBy, setAccessibleName, setAccessibleName, setAccessibleRole, setControlSize, setCursor, setEnabled, setFocusable, setId, setImageCursor, setInheritanceHost, setLayoutDirection, setLocale, setMaxHeight, setMaxWidth, setMinHeight, setMinWidth, setPresentedElsewhere, setTooltip, setTooltip, setVisible, shapeText, textRuler, tooltip, tooltipSource, width, x, y
-
Constructor Details
-
Viewport3D
public Viewport3D()An empty viewport; give it something to draw withsetRenderer(limn.components.Viewport3D.Renderer).
-
-
Method Details
-
setRenderer
Sets the render callback; without one, the built-in demo cube is shown. -
setAnimated
Whether the viewport drives a continuous repaint while showing (defaulttrue: right for content that moves every frame;timeSecondskeeps advancing). Setfalsefor static content: it then re-renders only when invalidated: camera controllers already invalidate on drag/zoom; callWidget.invalidate()after mutating the scene. An idle static viewport costs zero CPU/GPU, like the rest of the toolkit. -
observeDispose
Observes this viewport being detached, for GPU resources owned by the renderer, typically a retained scene'sScene3D::dispose. Every observer runs when this viewport is detached from the tree, deferred to the owning window's next frame so the GL context is current (the same path that releases the render target). Kept across re-attachments: a renderer that rebuilds lazily should also reset its own reference in this callback.A lifecycle observer list and not a slot, because this is the one registration where a list is obviously right: a second registration on a slot silently dropped a GPU free, with no diagnostic and no test that could see it.
- Parameters:
cleanup- what to run, context-current, on detach; never null- Returns:
- a handle that unregisters; cancelling it twice is a no-op. UI thread
-
setController
Installs a camera controller (e.g.new OrbitController(viewport.camera())). -
captureNext
Hands the next rendered frame of this viewport tosinkas anImage, then forgets it: one capture per call, not a subscription. Requests a repaint, so it works on a viewport that has stopped animating.The image is display-referred: what the viewport shows, with exposure, tonemap and sRGB encode applied, straight alpha, top-down. It is ready to hand to
Images.encode(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.io.OutputStream). The render target's own scene-referred contents are a different picture and are reached through the target itself, not here; seeReadableSurface.sinkruns on the UI thread inside that frame, so the read cannot be moved off it; a large capture should encode asynchronously (Images.saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path)) rather than inside the sink.- Throws:
NullPointerException- ifsinkis null
-
onClick
The application's response to a click (no drag): called with the world-space ray to feed toPicker. The one handler in the toolkit whose payload no accessor answers, since a 3D pick is not widget state, so it stays a slot fired from the pointer path, after the click is announced to the watchers asINVOKED.- Parameters:
listener- the handler, ornullto clear the slot- Returns:
- this viewport
- Throws:
IllegalStateException- if a handler is already registered
-
setRenderScale
Renders the 3D content at a fraction of the viewport's device resolution and upsamples it into the widget's box. Clamped to[0.25, 1]; 1 (the default) renders at full resolution.This is the cost lever for a heavy 3D view. The target holds four half-float channels per pixel, so both its memory and its fill cost fall with the square of the scale: 0.5 is a quarter of both. Nothing else changes: the widget's box, the 2D layout around it and
rayAt(float, float)picking are all in logical points and do not move.What it costs is sharpness. The upsample is bilinear, so edges soften; geometry with thin features shows it first. Multisampling still applies, at the reduced resolution.
-
renderScale
public float renderScale()The fraction of device resolution the 3D content is rendered at; 1 by default. -
frameContent
Points the camera atboxand backs it off far enough to hold the whole of it, keeping the current viewing direction and field of view, and moving the clip planes to bracket what it framed.Fitting needs the viewport's aspect ratio, so a call made before the first layout takes effect on the layout pass that supplies one instead of being lost, which is how to frame content once, at construction. An empty box is ignored.
The viewport does not know what its renderer draws, so it cannot find the box itself; a scene graph reports one through
Scene3D.bounds(). -
frameContent
frameContent(Aabb)with an explicit margin, the factor the fitted distance is multiplied by, so 1 touches the edges and 1.1 leaves a tenth of the frame around the content. Values below 1 crop it. -
rayAt
The world-space ray through a viewport-local pixel (for picking).localXis physical and stays physical in a right-to-left viewport: it is a distance from the viewport's left edge, because that is what the pointer reports and what an application computing its own coordinates will pass. A layout direction is a reading order and this argument is not read; reflecting it here would return the ray through the mirror image of the pixel the caller named. -
onMouseEvent
Description copied from class:WidgetMouse events (bubbling); callevent.consume()when handled.- Overrides:
onMouseEventin classWidget<Viewport3D>
-
setCamera
Replaces the camera. The instance is kept, so a controller can keep mutating it. -
camera
The camera this viewport renders with: mutable, and mutated by any controller. -
setPreferredSize
The size this viewport asks for, in logical points. The layout may still override it. -
onMeasure
Description copied from class:WidgetReports the size this widget wants withinconstraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.Resolve the
ControlSizeand theLayoutDirectiononce each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, andI18n.locale()answers it wherever text is resolved.- Specified by:
onMeasurein classWidget<Viewport3D>
-
onLayout
protected void onLayout()Description copied from class:WidgetContainers position children here (measure +Widget.layoutBox(float, float, float, float)per child).- Overrides:
onLayoutin classWidget<Viewport3D>
-
onPaint
Description copied from class:WidgetWidget's own background/content, in local coordinates.- Overrides:
onPaintin classWidget<Viewport3D>
-
onDetached
protected void onDetached()Description copied from class:WidgetCalled when this widget leaves the scene: detached from the tree, moved into another scene (told this before theWidget.onAttached()of the scene it joins), or still in it when the scene's window closes, which detaches every widget the scene has. What was taken inWidget.onAttached()from something that outlives the window (a listener's place in a list of the process's, a resource of the system's) is given back here.Widget.scene()still answers the scene being left, and becomesnullonce this returns, so releasing something the scene owns needs no field of your own to remember it by. Fires bottom-up, so children run before their parent, and every one of them can still reach the scene.Release resources here, but GPU resources must be handed to
Scene.disposeLater(limn.graphics.GpuSurface)(disposal needs the owning GL context, which is only current inside a frame). Not called onWidget.setVisible(boolean). Default no-op.- Overrides:
onDetachedin classWidget<Viewport3D>
-
onAccessibility
What a viewport is to an assistive technology: oneAccessible.Role.CANVASnode over the box the layout gave it, described by the "no GPU backend" message on the frames that paint one and by nothing at all on the frames that render.The role is the whole reason this hook exists, and it closes two opposite defects. The constructor makes this widget focusable, so the transparency predicate never deletes it whatever it declares, and a focusable node with no role is published as an unknown control and names this class in an application's log. The other path is the one that is easier to miss:
setFocusable(false)is public and final, and a viewport used as the display-only render surfacesetAnimated(false)is documented for would then declare nothing, hold no children, be deleted by the predicate, and — because it paints — be reported as a toolkit class that draws and says nothing, with the recommendation to strike out the one thing on the screen. Declaring the picture is a picture answers both, andpaintsDecoration()answers neither: a render is information, not a wash.The missing backend is a description and not a name. On the branch that has no provider the widget fills its whole box and centres one line of text, and that line is the only thing a sighted user has; without this a reader is told "canvas" and nothing more. It cannot be a name, and not because it would win one: the walk runs this hook first and applies an application's name and a bound caption after it, both unconditionally, so a name written here is a fallback beneath either. It is the third route that would suffer — the tooltip default is the one thing gated on nothing else having named the node. A failure is a status and not an identity, so it goes where a status goes: as the description it stands beside whichever of the three names the node, and where nothing else names it the tooltip is promoted into the name and the two are read together. The one real cost is a viewport that has both a tooltip and an application name: the description slot is already taken by the failure, so the tooltip is dropped from the tree, which is the one thing a sighted user still gets on hover and a reader does not.
The branch is read here rather than remembered by the paint. The publish step runs before the paint passes of the same frame, so a flag written in
onPaintwould describe the previous frame.Graphics3D.isAvailable()is a volatile read of a static the backend installs before any window exists and clears after the last one is destroyed, so it cannot move underneath a tree being published and needs no invalidation seam of its own.Nothing here formats anything, and on this widget more than any other that is the point: an animated viewport invalidates from its ticker on every frame, so one string built in this hook would be sixty allocations a second for the life of the window, spent to conclude that nothing had moved.
No name, no state and no verb, each a decision. The class holds no string that names it, so naming it is the application's, the caption's or the tooltip's, as it is for every other content surface the toolkit cannot describe. The camera's orientation, the animated flag and the render scale are not facts about an element and publishing them would republish the tree on a setter no reader can perceive. And the three pointer gestures — orbit, zoom and pick — have no honest parameterless form: a camera controller declares two void deltas with no readable value and no bounds, so a step verb could carry no value facet to report back, and the picking click carries the pointer's own coordinates, so a press verb would have to invent a pixel and tell the application the user clicked somewhere they did not. The node keeps its bounds and every gesture keeps working; what a reader cannot reach is said plainly here rather than papered over with a verb that lies.
- Overrides:
onAccessibilityin classWidget<Viewport3D>- Parameters:
a- the node being described
-