Crates.io | luminance |
lib.rs | luminance |
version | 0.47.0 |
source | src |
created_at | 2016-04-29 09:39:20.928784 |
updated_at | 2022-04-19 10:14:16.892705 |
description | Stateless and type-safe graphics framework |
homepage | https://github.com/phaazon/luminance-rs |
repository | https://github.com/phaazon/luminance-rs |
max_upload_size | |
id | 4901 |
size | 371,439 |
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.
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:
luminance is written to be fairly simple. There are several ways to learn how to use luminance:
luminance has been originally designed around the OpenGL 3.3 and OpenGL 4.5 APIs. However, it has mutated a lot 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:
"derive"
feature flag. That crate allows to implement various important traits of 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 abstract over
the actual implementation type — as a convention, the type variable B
(for backend) is
used.
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, Something<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 something without specifying a concrete type, you will have to
write something along the lines of:
use luminance::backend::something::Something as SomethingBackend;
use luminance::something::Something;
fn work<B>(b: &Something<B, u8>) where B: SomethingBackend<u8> {
todo!();
}
This kind of code is intended for people writing libraries with luminance. For the more usual case of using the luminance-front crate, you will end up writing something like:
use luminance_front::something::Something;
fn work(b: &Something<u8>) {
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.
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.
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 event features, which have nothing to do with our initial purpose.
A platform 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. However, platform crates are not supposed to be a replacement for the underlying platform system; you will typically still have to depend it as well.
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.
The Vertex
trait must be implemented if you want to use a type as vertex (consumed by 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 isunsafe
, which means that all of its implementors must be as well. This is due to the fact that vertex formats include information about the structure of the data that will be sent to the backend, and a bad implementation can lead to undefined behaviors.
You can derive the Vertex
trait if your type follows these conditions:
struct
with named fields. This is just a temporary limitation that will get
dropped as soon as the crate is stable enough.VertexAttrib
. This is mandatory so that the
backend knows enough about the types used in the structure to correctly align memory, pick
the right types, etc.HasSemantics
as well. This trait is just a
type family that associates a single constant (i.e. the semantics) that the vertex attribute
uses.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 implementSemantics
for anenum
of your choice, but also generate field types you can use when defining your vertex type.
The syntax is the following:
use luminance::{Vertex, Semantics};
// 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)]
#[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 ofHasSemantics
trait.
Besides the Semantics
-related code, this will:
MyVertex
, a struct that will hold a single vertex.Vertex for MyVertex
.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:
use luminance::{shader::{types::Vec4, Uniform}, UniformInterface};
#[derive(Debug, UniformInterface)]
struct MyIface {
time: Uniform<f32>,
resolution: Uniform<Vec4<f32>>,
}
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:
#[derive(Debug, UniformInterface)]
struct MyIface {
#[uniform(unbound)]
time: Uniform<f32>, // if this field cannot be mapped, it’ll be ignored
resolution: Uniform<Vec4<f32>>,
}
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:
#[derive(Debug, UniformInterface)]
struct MyIface {
time: Uniform<f32>,
#[uniform(name = "res")]
resolution: Uniform<Vec4<f32>>, // 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:
#[derive(Debug, UniformInterface)]
struct MyIface {
time: Uniform<f32>,
#[uniform(name = "res", unbound)]
resolution: Uniform<Vec4<f32>>,
}