crates.io License

A simple, type-safe and opinionated graphics crate

luminance is an effort to make graphics rendering simple and elegant. It is a low-level and opinionated graphics API, highly typed (type-level computations, refined types, etc.) which aims to be simple and performant. Instead of providing users with as many low-level features as possible, luminance provides you with some ways to do rendering. That has both advantages and drawbacks:

A note on safety: here, safety is not used as with the Rust definiton, but most in terms of undefined behavior and unwanted behavior. If something can lead to a weird behavior, a crash, a panic or a black screen, it’s considered unsafe. That definition obviously includes the Rust definiton of safety — memory safety.

Feature flags

None so far.

What’s included?

luminance is a rendering crate, not a 3D engine nor a video game framework. As so, it doesn’t include specific concepts, such as lights, materials, asset management nor scene description. It only provides a rendering library you can plug in whatever you want to.

There are several so-called 3D-engines out there on crates.io. Feel free to have a look around.

However, luminance comes with several interesting features:

How to dig in?

luminance is written to be fairly simple. There are several ways to learn how to use luminance:

Implementation and architecture

luminance has been originally designed around the OpenGL 3.3 and OpenGL 4.5 APIs. However, it has mutated to adapt to new technologies and modern graphics programming. Even though its API is not meant to converge towards something like Vulkan, it’s changing over time to meet better design decisions and performance implications.

The current state of luminance comprises several crates:

The core crate

The luminance crate gathers all the logic and rendering abstractions necessary to write code over various graphics technologies. It contains parametric types and functions that depend on the actual implementation type — as a convention, the type variable B (for backend) is used. For instance, the type Buffer<B, u8> is an 8-bit unsigned integer buffer for which the implementation is provided via the B type.

Backend types — i.e. B — are not provided by [luminance] directly. They are typically provided by crates containing the name of the technology as suffix, such as luminance-gl, luminance-webgl, luminance-vk, etc. The interface between those backend crates and luminance is specified in [luminance::backend].

On a general note, Buffer<ConcreteType, u8> is a monomorphic type that will be usable only with code working over the ConcreteType backend. If you want to write a function that accepts an 8-bit integer buffer without specifying a concrete type, you will have to write something along the lines of:

```rust use luminance::backend::buffer::Buffer as BufferBackend; use luminance::buffer::Buffer;

fn work(b: &Buffer) where B: BufferBackend { todo!(); } ```

This kind of code is intented for people writing libraries with luminance. For the special case of using the [luminance-front] crate, you will end up writing something like:

```rust use luminance_front::buffer::Buffer;

fn work(b: &Buffer) { todo()!; } ```

In [luminance-front], the backend type is selected at compile and link time. This is often what people want, but keep in mind that [luminance-front] doesn’t allow to have several backend types at the same time, which might be something you would like to use, too.

Backend implementations

Backends implement the [luminance::backend] traits and provide, mostly, a single type for each implementation. It’s important to understand that a backend crate can provide several backends (for instance, [luminance-gl] can provide one backend — so one type — for each supported OpenGL version). That backend type will be used throughout the rest of the ecosystem to deduce subsequent implementors and associated types.

If you want to implement a backend, you don’t have to push any code to any luminance crate. luminance-* crates are official ones, but you can write your own backend as well. The interface is highly unsafe, though, and based mostly on unsafe impl on unsafe trait. For more information, feel free to read the documentation of the [luminance::backend] module.

Windowing

luminance doesn’t know anything about the context it executes in. That means that it doesn’t know whether it’s used within SDL, GLFW, glutin, Qt, a web canvas or an embedded specific hardware such as the Nintendo Switch. That is actually powerful, because it allows luminance to be completely agnostic of the execution platform it’s running on: one problem less. However, there is an important point to take into account: a single backend type can be used with several windowing crates / implementations. That allows to re-use a backend with several windowing implementations. The backend will typically explain what are the conditions to create it (like, in OpenGL, the windowing crate must set some specific flags when creating the OpenGL context).

luminance does not provide a way to create windows because it’s important that it not depend on windowing libraries – so that end-users can use whatever they like. Furthermore, such libraries typically implement windowing and events features, which have nothing to do with our initial purpose.

A windowing crate supporting luminance will typically provide native types by re-exporting symbols (types, functions, etc.) from a windowing crate and the necessary code to make it compatible with luminance. That means providing a way to access a backend type, which implements the [luminance::backend] interface.

luminance-derive

If you are compiling against the "derive" feature, you get access to [luminance-derive] automatically, which provides a set of procedural macros.

Vertex

The [Vertex] derive proc-macro.

That proc-macro allows you to create custom vertex types easily without having to care about implementing the required traits for your types to be usable with the rest of the crate.

The [Vertex] trait must be implemented if you want to use a type as vertex (passed-in via slices to [Tess]). Either you can decide to implement it on your own, or you could just let this crate do the job for you.

Important: the [Vertex] trait is unsafe, which means that all of its implementors must be as well. This is due to the fact that vertex formats include information about raw-level GPU memory and a bad implementation can have undefined behaviors.

You can derive the [Vertex] trait if your type follows these conditions:

Once all those requirements are met, you can derive [Vertex] pretty easily.

Note: feel free to look at the [Semantics] proc-macro as well, that provides a way to generate semantics types in order to completely both implement [Semantics] for an enum of your choice, but also generate field types you can use when defining your vertex type.

The syntax is the following:

```rust

// visit the Semantics proc-macro documentation for further details

[derive(Clone, Copy, Debug, PartialEq, Semantics)]

pub enum Semantics { #[sem(name = "position", repr = "[f32; 3]", wrapper = "VertexPosition")] Position, #[sem(name = "color", repr = "[f32; 4]", wrapper = "VertexColor")] Color }

[derive(Clone, Copy, Debug, PartialEq, Vertex)] // just add Vertex to the list of derived traits

[vertex(sem = "Semantics")] // specify the semantics to use for this type

struct MyVertex { position: VertexPosition, color: VertexColor } ```

Note: the Semantics enum must be public because of the implementation of [HasSemantics] trait.

Besides the Semantics-related code, this will:

The proc-macro also supports an optional #[vertex(instanced = "<bool>")] struct attribute. This attribute allows you to specify whether the fields are to be instanced or not. For more about that, have a look at [VertexInstancing].

Semantics

The [Semantics] derive proc-macro.

UniformInterface

The [UniformInterface] derive proc-macro.

The procedural macro is very simple to use. You declare a struct as you would normally do:

```rust

[derive(Debug, UniformInterface)]

struct MyIface { time: Uniform, resolution: Uniform<[f32; 4]> } ```

The effect of this declaration is declaring the MyIface struct along with an effective implementation of UniformInterface that will try to get the "time" and "resolution" uniforms in the corresponding shader program. If any of the two uniforms fails to map (inactive uniform, for instance), the whole struct cannot be generated, and an error is arisen (see UniformInterface::uniform_interface’s documentation for further details).

If you don’t use a parameter in your shader, you might not want the whole interface to fail building if that parameter cannot be mapped. You can do that via the #[unbound] field attribute:

```rust

[derive(Debug, UniformInterface)]

struct MyIface { #[uniform(unbound)] time: Uniform, // if this field cannot be mapped, it’ll be ignored resolution: Uniform<[f32; 4]> } ```

You can also change the default mapping with the #[uniform(name = "string_mapping")] attribute. This changes the name that must be queried from the shader program for the mapping to be complete:

```rust

[derive(Debug, UniformInterface)]

struct MyIface { time: Uniform, #[uniform(name = "res")] resolution: Uniform<[f32; 4]> // maps "res" from the shader program } ```

Finally, you can mix both attributes if you want to change the mapping and have an unbound uniform if it cannot be mapped:

```rust

[derive(Debug, UniformInterface)]

struct MyIface { time: Uniform, #[uniform(name = "res", unbound)] resolution: Uniform<[f32; 4]> // must map "res" from the shader program and ignored otherwise } ```