R/render.R

Defines functions render_triangles render_options render_scene render_mesh

Documented in render_mesh render_options render_scene render_triangles

#' Render a 3D mesh to an image
#'
#' Renders a single mesh using the scimesh software renderer.
#' The mesh can be specified either as separate
#' \code{vertices}/\code{triangles} matrices, as a scimesh mesh
#' descriptor list, or as an rgl \code{tmesh3d}-style list (with
#' \code{vb}/\code{it} components).  rgl meshes are transparently
#' converted via \code{\link{mesh_from_rgl}()}.
#'
#' @param vertices Either an Nx3 numeric matrix of vertex positions,
#'   or a scimesh mesh descriptor list (with \code{vertices} and
#'   \code{triangles} components), or an rgl-style list (with
#'   \code{vb} and \code{it} components).
#' @param triangles Mx3 integer matrix of triangle indices (1-based).
#'   Ignored when \code{vertices} is a list.
#' @param colors Optional Nx4 numeric matrix of RGBA vertex colors (0-1).
#'   Use \code{face_colors} (Mx4) for per-triangle colours instead.
#' @param face_colors Optional Mx4 numeric matrix of per-face RGBA colors,
#'   one row per triangle.  When present, all three vertices of a triangle
#'   use the same colour.  Takes precedence over vertex \code{colors}.
#' @param normals Optional Nx3 numeric matrix of vertex normals.
#' @param uv Optional Nx2 numeric matrix of texture coordinates (0-1).
#' @param texture Optional texture image as a 3D array (H x W x 3 or 4)
#'   with values in \code{[0, 1]}, e.g. from \code{png::readPNG()}.
#' @param camera A camera list from \code{camera()} or \code{camera_auto()}.
#' @param options A render options list from \code{render_options()}.
#' @return A list with components \code{width}, \code{height}, and
#'   \code{pixels} (raw vector of RGBA values).
#'
#' @examples
#' # Render a simple colored triangle
#' verts <- matrix(c(0, 0, 0,  1, 0, 0,  0.5, 1, 0), ncol = 3, byrow = TRUE)
#' tris  <- matrix(1L, nrow = 1, ncol = 3)
#' cols  <- matrix(c(1, 0, 0, 1,  0, 1, 0, 1,  0, 0, 1, 1), ncol = 4, byrow = TRUE)
#' img <- render_mesh(verts, tris, colors = cols)
#' tmp_file <- tempfile(fileext = ".png")
#' write_png(img, tmp_file)
#'
#' # Render from a mesh descriptor list (scimesh format)
#' mesh_desc <- list(vertices = verts, triangles = tris, colors = cols)
#' img <- render_mesh(mesh_desc)
#'
#' @export
render_mesh <- function(vertices, triangles = NULL, colors = NULL,
                        face_colors = NULL, normals = NULL, uv = NULL,
                        texture = NULL,
                        camera = NULL,
                        options = render_options()) {
    # Transparently accept rgl or scimesh mesh descriptors
    if (is.list(vertices) && !is.matrix(vertices)) {
        converted <- as_scimesh_mesh(vertices)
        if (!is.null(converted$colors) && is.null(colors)) {
            colors <- converted$colors
        }
        if (!is.null(converted$face_colors) && is.null(face_colors)) {
            face_colors <- converted$face_colors
        }
        if (!is.null(converted$normals) && is.null(normals)) {
            normals <- converted$normals
        }
        vertices  <- converted$vertices
        triangles <- converted$triangles
    }

    if (!is.matrix(vertices) || ncol(vertices) != 3L) {
        stop("vertices must be an Nx3 numeric matrix")
    }
    if (!is.matrix(triangles) || ncol(triangles) != 3L) {
        stop("triangles must be an Mx3 integer matrix")
    }

    if (is.null(camera)) {
        camera <- camera_auto(vertices)
    }

    mesh <- list(
        vertices = vertices,
        triangles = triangles
    )
    if (!is.null(colors)) {
        if (!is.matrix(colors) || ncol(colors) < 3L || ncol(colors) > 4L) {
            stop("colors must be an Nx3 or Nx4 numeric matrix")
        }
        mesh$colors <- colors
    }
    if (!is.null(face_colors)) {
        if (!is.matrix(face_colors) || ncol(face_colors) < 3L || ncol(face_colors) > 4L) {
            stop("face_colors must be an Mx3 or Mx4 numeric matrix")
        }
        mesh$face_colors <- face_colors
    }
    if (!is.null(normals)) {
        if (!is.matrix(normals) || ncol(normals) != 3L) {
            stop("normals must be an Nx3 numeric matrix")
        }
        mesh$normals <- normals
    }
    if (!is.null(uv)) {
        if (!is.matrix(uv) || ncol(uv) != 2L) {
            stop("uv must be an Nx2 numeric matrix")
        }
        mesh$uv <- uv
    }
    if (!is.null(texture)) {
        mesh$texture <- texture
    }

    scimesh_render_mesh(mesh, camera, options)
}

#' Render multiple meshes to an image
#'
#' Renders a list of meshes as a single scene using the scimesh
#' software renderer.  Each element can be a scimesh mesh descriptor
#' or an rgl-style mesh (with \code{vb}/\code{it}); rgl meshes are
#' transparently converted.
#'
#' @param meshes A list of mesh descriptors. Each element is a list
#'   with components \code{vertices} (Nx3 matrix), \code{triangles}
#'   (Mx3 integer matrix), and optionally \code{colors}, \code{face_colors},
#'   \code{normals}, and \code{default_color}.
#'   Elements may also be rgl-style lists (with \code{vb} and \code{it}),
#'   which are converted automatically.
#' @param camera A camera list from \code{camera()} or \code{camera_auto()}.
#' @param options A render options list from \code{render_options()}.
#' @return A list with components \code{width}, \code{height}, and
#'   \code{pixels} (raw vector of RGBA values).
#'
#' @examples
#' # Render two cubes side by side
#' cube1 <- generate_cuboid(c(-1.5, 0, 0), c(0.8, 0.8, 0.8), c(1, 0, 0, 1))
#' cube2 <- generate_cuboid(c( 1.5, 0, 0), c(0.8, 0.8, 0.8), c(0, 0, 1, 1))
#' cam <- camera_auto(list(cube1, cube2), direction = c(1, 1, 1))
#' img <- render_scene(list(cube1, cube2), cam,
#'     render_options(width = 800, height = 600, background_color = c(1, 1, 1, 1)))
#' tmp_file <- tempfile(fileext = ".png")
#' write_png(img, tmp_file)
#'
#' # Render multiple meshes together
#' scimesh_cube <- generate_cuboid(c(-1, 0, 0), c(0.5, 0.5, 0.5))
#' sphere <- generate_sphere(c(1, 0, 0), radius = 0.5, color = c(0, 1, 0, 1))
#' cam <- camera_auto(list(scimesh_cube, sphere), direction = c(1, 1, 1))
#' img <- render_scene(list(scimesh_cube, sphere), cam,
#'     render_options(width = 400, height = 300, background_color = c(1, 1, 1, 1)))
#'
#' @export
render_scene <- function(meshes, camera, options = render_options()) {
    if (!is.list(meshes)) {
        stop("meshes must be a list of mesh descriptors")
    }
    for (i in seq_along(meshes)) {
        meshes[[i]] <- as_scimesh_mesh(meshes[[i]])
    }
    for (i in seq_along(meshes)) {
        m <- meshes[[i]]
        if (!is.list(m) || is.null(m$vertices) || is.null(m$triangles)) {
            stop("each mesh must be a list with 'vertices' and 'triangles'")
        }
    }

    scimesh_render_scene(meshes, camera, options)
}

#' Create render options
#'
#' @param width Output image width in pixels.
#' @param height Output image height in pixels.
#' @param shading Shading mode: \code{"smooth"} or \code{"flat"}.
#' @param backface_culling Whether to cull back-facing triangles.
#' @param background_color Background RGBA color as numeric vector of
#'   length 4 (values 0-1).
#' @param default_color Default vertex color when no colors are provided.
#' @param invert_normals Whether to invert surface normals.
#' @param wireframe Whether to render in wireframe mode.
#' @param wireframe_color RGBA color for wireframe edges (0-1
#'   scale).  Default \code{c(0, 0, 0, 1)} (black).
#' @param projection Projection type: \code{"perspective"} (default)
#'   or \code{"orthographic"}.  Orthographic gives a parallel
#'   projection (no perspective foreshortening), matching rgl's
#'   \code{view3d(fov=0)} convention.
#' @param specular_color Specular highlight color (0-1 scale).  When
#'   \code{shininess > 0}, a Blinn-Phong highlight in this colour is
#'   added where the surface faces the camera.  Default
#'   \code{c(0, 0, 0, 0)} (off).
#' @param shininess Specular exponent controlling highlight sharpness.
#'   Higher values produce a tighter spot.  Typical values: 32 (soft
#'   plastic), 64 (shiny), 128 (glass).  Default \code{0} (off).
#' @param lights A list of light descriptors, each a list with
#'   \code{position} (length-3 direction vector or point position),
#'   \code{color} (length-4 RGBA, 0-1 scale), \code{intensity}
#'   (numeric, default 1), and \code{directional} (logical, default
#'   \code{TRUE}).  When empty or \code{NULL}, a single headlight at
#'   \code{c(0, 0, 1)} is used (the original behaviour).
#' @param ambient Ambient light contribution (0-1).  Default 0.3.
#' @param contrast Contrast adjustment applied after shading, before
#'   uint8_t conversion.  Default 1.0 (no change).  Values > 1.0 produce
#'   darker darks and lighter highlights (S-curve).  Formula:
#'   \code{(value - 0.5) * contrast + 0.5}, clamped to \code{[0, 1]}.
#' @param fog_enabled Enable depth cueing (fog).  Default \code{FALSE}.
#' @param fog_start Z-depth where fog begins (0 = near plane, 1 = far
#'   plane).  Default 0.
#' @param fog_end Z-depth where fog is fully opaque.  Default 1.
#' @param fog_color RGBA fog colour (0-1 scale).  Defaults to
#'   \code{background_color}.
#' @param threads Number of render threads.  0 = auto-detect (use all
#'   cores), 1 = single-threaded (deterministic).  Default 0.
#'   Requires OpenMP at compile time.
#' @param clip_planes A list of clip plane descriptors, each a list
#'   with \code{normal} (length-3 vector) and \code{offset} (numeric).
#'   Points satisfying \code{dot(normal, position) + offset >= 0} are
#'   kept.  Default \code{NULL} (no clipping).
#' @param ssao_enabled Enable screen-space ambient occlusion.
#'   Default \code{FALSE}.
#' @param ssao_radius Screen-space sample radius in pixels.  Default 16.
#' @param ssao_intensity Occlusion strength (0-1).  Default 0.8.
#' @param aa_samples Anti-aliasing supersampling factor.  Renders
#'   internally at \code{width * aa_samples} x
#'   \code{height * aa_samples}, then downsamples to the requested
#'   size via box averaging.  Default \code{1} (no AA), \code{2} for
#'   2x2 SSAA, \code{4} for 4x4.
#' @return A render options list for use with \code{render_mesh()} or
#'   \code{render_scene()}.
#'
#' @examples
#' # Default options
#' opts <- render_options()
#'
#' # High-resolution with anti-aliasing and specular highlights
#' opts <- render_options(width = 1200, height = 900,
#'     aa_samples = 2L,
#'     specular_color = c(0.4, 0.4, 0.4, 1),
#'     shininess = 64)
#'
#' # Wireframe with transparent background
#' opts <- render_options(wireframe = TRUE,
#'     wireframe_color = c(0, 0, 0, 1),
#'     background_color = c(0, 0, 0, 0))
#'
#' @export
render_options <- function(width = 800L, height = 600L,
                           shading = c("smooth", "flat"),
                           backface_culling = TRUE,
                           background_color = c(0, 0, 0, 0),
                           default_color = c(0.7, 0.7, 0.7, 1),
                           invert_normals = FALSE,
                           wireframe = FALSE,
                           wireframe_color = c(0, 0, 0, 1),
                           projection = c("perspective", "orthographic"),
                           specular_color = c(0, 0, 0, 0),
                           shininess = 0,
                           lights = NULL,
                            ambient = 0.3,
                            contrast = 1.0,
                            fog_enabled = FALSE,
                           fog_start = 0,
                           fog_end = 1,
                           fog_color = c(0, 0, 0, 0),
                           threads = 0L,
                           clip_planes = NULL,
                           ssao_enabled = FALSE,
                           ssao_radius = 16,
                           ssao_intensity = 0.8,
                           aa_samples = 1L) {
    shading <- match.arg(shading)
    structure(list(
        width = as.integer(width),
        height = as.integer(height),
        shading = shading,
        backface_culling = isTRUE(backface_culling),
        background_color = as.numeric(background_color),
        default_color = as.numeric(default_color),
        invert_normals = isTRUE(invert_normals),
        wireframe = isTRUE(wireframe),
        wireframe_color = as.numeric(wireframe_color),
        projection = match.arg(projection),
        specular_color = as.numeric(specular_color),
        shininess = as.numeric(shininess),
        lights = lights,
        ambient = as.numeric(ambient),
        contrast = as.numeric(contrast),
        fog_enabled = isTRUE(fog_enabled),
        fog_start = as.numeric(fog_start),
        fog_end = as.numeric(fog_end),
        fog_color = as.numeric(fog_color),
        threads = as.integer(threads),
        clip_planes = clip_planes,
        ssao_enabled = isTRUE(ssao_enabled),
        ssao_radius = as.numeric(ssao_radius),
        ssao_intensity = as.numeric(ssao_intensity),
        aa_samples = as.integer(aa_samples)
    ), class = "scimesh_options")
}

#' Render raw triangles without index buffer
#'
#' Renders triangle geometry where positions and colours are given
#' as flat arrays with 3 vertices per triangle (no index buffer).
#' Useful for voxel renderings, misc3d isosurfaces, and other
#' dynamically generated geometry.
#'
#' @param positions Nx3 numeric matrix of vertex positions, where N
#'   is a multiple of 3 (3 per triangle).
#' @param colors Nx4 numeric matrix of RGBA colours (0-1 scale).
#' @param camera A camera list from \code{camera()} or
#'   \code{camera_auto()}.
#' @param options Render options from \code{render_options()}.
#' @return An image list with \code{width}, \code{height},
#'   \code{pixels}.
#'
#' @examples
#' # Render a single red triangle from raw vertices
#' positions <- matrix(c(0, 0, 0, 1, 0, 0, 0.5, 1, 0), ncol = 3, byrow = TRUE)
#' colors <- matrix(c(1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1), ncol = 4, byrow = TRUE)
#' cam <- camera_auto(positions)
#' img <- render_triangles(positions, colors, cam)
#'
#' @export
render_triangles <- function(positions, colors, camera,
                             options = render_options()) {
    if (!is.matrix(positions) || ncol(positions) != 3L) {
        stop("positions must be an Nx3 numeric matrix")
    }
    n <- nrow(positions)
    if (n %% 3L != 0L) {
        stop("positions must have a multiple of 3 rows")
    }
    if (!is.matrix(colors) || nrow(colors) != n || ncol(colors) < 3L) {
        stop("colors must be an Nx4 numeric matrix matching positions")
    }
    scimesh_render_triangles_raw(positions, colors, camera, options)
}

Try the scimesh package in your browser

Any scripts or data that you put into this service are public.

scimesh documentation built on Aug. 9, 2026, 9:07 a.m.