Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/danielitoCode/Spatial/llms.txt

Use this file to discover all available pages before exploring further.

By the end of this guide you will have a fully working @Composable function that renders a Cube, a Sphere, and a ground Plane in a 3D scene, wired to a smooth orbit camera that responds to touch gestures. No OpenGL setup, no render loop management — just a composable tree that Spatial turns into a GPU-rendered frame.
1
Add dependencies
2
Spatial is distributed as local Gradle submodules. Open your app-level build.gradle.kts and declare the three public modules as implementation dependencies:
3
// app/build.gradle.kts
dependencies {
    implementation(project(":spatial-compose"))
    implementation(project(":spatial-compose-runtime-adapter"))
    implementation(project(":spatial-units"))
}
4
  • :spatial-compose — the declarative Compose API (Scene, Element, Modifier3D, rememberCameraState, Gestures)
  • :spatial-compose-runtime-adapter — provides DefaultSceneRenderHostFactory, which wires the Compose layer to the real OpenGL renderer
  • :spatial-units — typed unit extensions (meters, cm, deg) used in modifier chains
  • 5
    Create a CameraState
    6
    Use rememberCameraState to create and remember a camera positioned in your scene:
    7
    val cameraState = rememberCameraState(
        yaw   = 20f.deg,
        pitch = (-12f).deg,
        zoom  = 1.25f,
    )
    
    8
  • yaw — horizontal rotation around the world Y-axis, in degrees. Positive values rotate the camera to the right of the scene.
  • pitch — vertical tilt of the camera, in degrees. Negative values tilt the camera downward, so the scene is viewed slightly from above.
  • zoom — a unitless multiplier controlling how close the camera sits to the orbit target. Values below 1f zoom out; values above 1f zoom in.
  • 9
    rememberCameraState is backed by Compose’s remember, so the state survives recomposition. It also exposes animateTo for programmatic, spring-animated camera transitions.
    10
    Compose the Scene
    11
    Wrap your elements inside the Scene composable, passing the camera state, the render host factory, and your chosen gesture mode:
    12
    @Composable
    fun CoreOneScene(modifier: Modifier = Modifier) {
        val cameraState = rememberCameraState(
            yaw   = 20f.deg,
            pitch = (-12f).deg,
            zoom  = 1.25f,
        )
    
        Scene(
            modifier          = modifier.fillMaxSize(),
            renderHostFactory = DefaultSceneRenderHostFactory,
            cameraState       = cameraState,
            gestures          = Gestures.orbit(),
        ) {
            Element.Cube(
                modifier = Modifier3D.Default
                    .rotateY(35f.deg)
                    .rotateZ(18f.deg)
                    .size(1.4f.meters)
                    .position(0f.meters, 0f.meters, (-4f).meters),
            )
            Element.Sphere(
                modifier = Modifier3D.Default
                    .size(1f.meters)
                    .position(2f.meters, 0f.meters, (-6f).meters),
            )
            Element.Plane(
                modifier = Modifier3D.Default
                    .size(8f.meters, 0.1f.meters, 8f.meters)
                    .position(0f.meters, (-1.2f).meters, (-5f).meters),
            )
        }
    }
    
    13
    The Modifier3D chain works similarly to Compose’s Modifier:
    14
    MethodDescription.size(all: Distance)Uniform scale — sets width, height, and depth to the same value.size(width, height, depth)Non-uniform scale along each axis.position(x, y, z)World-space translation, typed in Distance units.rotateX/Y/Z(angle)Rotation around a single axis, typed in Angle units
    15
    DefaultSceneRenderHostFactory is the bridge between Spatial’s declarative Compose layer and the real OpenGL ES 3.0 backend. It implements SceneRenderHostFactory, creates an AndroidView-hosted GL surface, and manages the render loop lifecycle. You should always pass it as the renderHostFactory argument unless you are supplying a custom test double.
    16
    Replace Gestures.orbit() with Gestures.orbitAndZoom() to also enable pinch-to-zoom:
    gestures = Gestures.orbitAndZoom()
    
    Both accept an optional sensitivity parameter (GestureSensitivity.Adaptive by default) if you need to tune orbit or zoom response.
    17
    Run the playground
    18
    Connect an Android device or start an emulator with OpenGL ES 3.0 support, then run the :app configuration. The playground renders your scene at 60 FPS and prints the live camera yaw, pitch, and zoom values to the screen — useful for tuning the initial camera position.

    Complete Example

    Here is the full, self-contained composable with all required imports:
    CoreOneScene.kt
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import com.elitec.spatial_compose.Element
    import com.elitec.spatial_compose.Gestures
    import com.elitec.spatial_compose.Modifier3D
    import com.elitec.spatial_compose.Scene
    import com.elitec.spatial_compose_runtime_adapter.DefaultSceneRenderHostFactory
    import com.elitec.spatial_compose.rememberCameraState
    import com.elitec.spatial_units.deg
    import com.elitec.spatial_units.meters
    
    @Composable
    fun CoreOneScene(modifier: Modifier = Modifier) {
        val cameraState = rememberCameraState(
            yaw   = 20f.deg,
            pitch = (-12f).deg,
            zoom  = 1.25f,
        )
    
        Scene(
            modifier          = modifier.fillMaxSize(),
            renderHostFactory = DefaultSceneRenderHostFactory,
            cameraState       = cameraState,
            gestures          = Gestures.orbit(),
        ) {
            Element.Cube(
                modifier = Modifier3D.Default
                    .rotateY(35f.deg)
                    .rotateZ(18f.deg)
                    .size(1.4f.meters)
                    .position(0f.meters, 0f.meters, (-4f).meters),
            )
            Element.Sphere(
                modifier = Modifier3D.Default
                    .size(1f.meters)
                    .position(2f.meters, 0f.meters, (-6f).meters),
            )
            Element.Plane(
                modifier = Modifier3D.Default
                    .size(8f.meters, 0.1f.meters, 8f.meters)
                    .position(0f.meters, (-1.2f).meters, (-5f).meters),
            )
        }
    }
    

    Build docs developers (and LLMs) love