Class Viewport3D

java.lang.Object
limn.scene.Widget<Viewport3D>
limn.components.Viewport3D

public class Viewport3D extends Widget<Viewport3D>
A viewport that renders 3D content into an offscreen 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.

  • Constructor Details

  • Method Details

    • setRenderer

      public Viewport3D setRenderer(@Nullable Viewport3D.Renderer renderer)
      Sets the render callback; without one, the built-in demo cube is shown.
    • setAnimated

      public Viewport3D setAnimated(boolean animated)
      Whether the viewport drives a continuous repaint while showing (default true: right for content that moves every frame; timeSeconds keeps advancing). Set false for static content: it then re-renders only when invalidated: camera controllers already invalidate on drag/zoom; call Widget.invalidate() after mutating the scene. An idle static viewport costs zero CPU/GPU, like the rest of the toolkit.
    • observeDispose

      public Subscription observeDispose(Runnable cleanup)
      Observes this viewport being detached, for GPU resources owned by the renderer, typically a retained scene's Scene3D::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

      public Viewport3D setController(@Nullable CameraController controller)
      Installs a camera controller (e.g. new OrbitController(viewport.camera())).
    • captureNext

      public Viewport3D captureNext(Consumer<Image> sink)
      Hands the next rendered frame of this viewport to sink as an Image, 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; see ReadableSurface.

      sink runs 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 - if sink is null
    • onClick

      public Viewport3D onClick(@Nullable Consumer<Ray> listener)
      The application's response to a click (no drag): called with the world-space ray to feed to Picker. 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 as INVOKED.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this viewport
      Throws:
      IllegalStateException - if a handler is already registered
    • setRenderScale

      public Viewport3D setRenderScale(float scale)
      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

      public Viewport3D frameContent(Aabb box)
      Points the camera at box and 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

      public Viewport3D 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. Values below 1 crop it.
    • rayAt

      public Ray rayAt(float localX, float localY)
      The world-space ray through a viewport-local pixel (for picking).

      localX is 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

      protected void onMouseEvent(MouseEvent event)
      Description copied from class: Widget
      Mouse events (bubbling); call event.consume() when handled.
      Overrides:
      onMouseEvent in class Widget<Viewport3D>
    • setCamera

      public Viewport3D setCamera(Camera camera)
      Replaces the camera. The instance is kept, so a controller can keep mutating it.
    • camera

      public Camera camera()
      The camera this viewport renders with: mutable, and mutated by any controller.
    • setPreferredSize

      public Viewport3D setPreferredSize(float width, float height)
      The size this viewport asks for, in logical points. The layout may still override it.
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. 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 ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget<Viewport3D>
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget<Viewport3D>
    • onPaint

      protected void onPaint(Canvas canvas)
      Description copied from class: Widget
      Widget's own background/content, in local coordinates.
      Overrides:
      onPaint in class Widget<Viewport3D>
    • onDetached

      protected void onDetached()
      Description copied from class: Widget
      Called when this widget leaves the scene: detached from the tree, moved into another scene (told this before the Widget.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 in Widget.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 becomes null once 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 on Widget.setVisible(boolean). Default no-op.

      Overrides:
      onDetached in class Widget<Viewport3D>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      What a viewport is to an assistive technology: one Accessible.Role.CANVAS node 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 surface setAnimated(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, and paintsDecoration() 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 onPaint would 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:
      onAccessibility in class Widget<Viewport3D>
      Parameters:
      a - the node being described