Class Viewport3D

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

public class Viewport3D extends Widget
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(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.
    • onDispose

      public Viewport3D onDispose(Runnable cleanup)
      Registers cleanup for GPU resources owned by the renderer, typically a retained scene's Scene3D::dispose. It 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.
    • setController

      public Viewport3D setController(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(Consumer<Ray> listener)
      Fires with the world-space ray on a click (no drag); feed it to Picker.
    • 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
    • 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
    • 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
    • onPaint

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

      protected void onDetached()
      Description copied from class: Widget
      Called when this widget leaves the scene (detached from the tree).

      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