This crate is the interpreter for the raytracer's SDL (scene description language). It is a (mostly) declarative language that is responsible for describing the objects in the scene to the raytracer. It is designed to be fast and readable. It is reminiscent of POV-Ray's SDL, JSON, and maybe even shader languages like GLSL or HLSL.
To render from an SDL file,
sdl my_file.sdl
To change its output,
sdl my_file.sdl -o my_render.png
To continuously watch the SDL file for changes and rerender on all saves,
sdl --watch my_file.sdl
Optionally compile with cargo initially by changing sdl in all cases to cargo run --release -p sdl -- .
The sdl crate is capable of rendering scenes from sdl files. Some examples are in
the scenes folder in this repository.
Comments can be added anywhere with #. Everything after a comment indicator will be ignored
until the next newline.
As far as values go, there are a few primitive types:
- Numbers, which are constructed with literal numbers like
1,2.4,-5.0, ... - Strings, which are constructed with literal strings like
"hello world!","I say \"hello\"", ... - Booleans, which are constructed with the keywords
true/yesorfalse/no - Vectors, which are constructed with the syntax
<x, y, z> - Colors, which are constructed with the familiar function call syntax
color(r, g, b), where r, g, and b are numbers from 0-255 - Dictionaries, which are constructed much like JSON objects. They are wrapped in curly braces and are a collection of comma-separated key-values, like
{key: value, another_key: another_value}
There are also reference objects, which include:
- Arrays, which are constructed with
[1, 2, 3]syntax. Nested arrays are supported, e.g.[[1, 2, 3], [4, 5, 6]]. They can be index usingarray[index]syntax.
In any scope, variables can be set simply with the syntax identifer = value, like tau = PI * 2.
Local variables can be declared by prefixing the keyword let, i.e. let y = x * 2. Variables can
be updated in scopes of the same or greater depth by omitting the let keyword.
Later, the variable can be used in dictionaries as values, as function arguments, and so on.
Variables declared in nested scopes are always local. Variables declared in a nested scope will shadow variables of the same name in a higher scope.
A few constants are provided, such as
PIis piTAUis double piEis Euler's constant
There are a number of functions that can be used as values.
add(x, y)adds two values together (alternatively, use +)sub(x, y)subtracts two values from one another (alternatively, -)mul(x, y)multiplies two values together (*)div(x, y)divides two values from each other (/)mod(x, y)modulo of two values (%)
vec(x, y, z)constructs a vector from 3 numbers (alternatively use<x, y, z>)color(r, g, b)orrgb(r, g, b)constructs a color from 3 numbers (each 0-255)hsv(h, s, v)constructs a color from HSV values where H is in [0, 360], and S and V are both from 0 to 1.
sin(x),cos(y), andtan(z)are all traditional trigonometric functionsasin(x),acos(y), andatan(z)are all traditional inverse trigonometric functionsabs(x)returns the absolute value of xfloor(x)returns the floor of xceil(y)returns the ceiling of yrad(x)returns x, converted from degrees to radiansdeg(x)returns x, converted from radians to degreesrandom(x, y)returns a random floating point number betweenxandy, inclusive
normalize(v)returns a normalized vmagnitude(v)returns the magnitude of vangle(a, b)returns the angle between vectors a and b
push(a, v)pushesvintoalen(a)returns the length ofa
Users can define their own function with the following syntax:
fn identifier(param1, param2, param3) {
// function body here
}
Much like JavaScript, except the function keyword has been replaced with fn, to keep the language
in line with Rust-like syntax.
Functions can add scene objects to the scene, return values with the return keyword, and do a host
of other operations that can be done in the global scope (or in statement scopes).
An if-statement is constructed with the following syntax:
if condition {
// body
} else if other_condition {
// else-if body
} else {
// else body
}
A for loop over a definite integer range, upper-bound exclusive can be constructed with the following syntax:
for i in lower to upper {
// body, use `i` here
}
The SDL supports normal comparison and logic operators, like ==, !=, >, >=, <, <=, &&, and ||.
A value is truthy if it
- is a number and is non-zero
- is a boolean and is true
- is not the unit type
- all other cases
At the top-level of every SDL file, a number of objects can be declared. An object is in the form:
[object name] {
[properties]
}
There are a collection of valid object names, like
camera, used to define the camera transform and viewport*scene, used to define a few scene properties*skybox, used to define the scene's skybox*aabborbox, an object that is an axis-aligned bounding boxmesh, an object that can be loaded from anobjfile and is a meshplane, an object that is a planesphere, an object that is a spherepoint_light, a point lightsun, a sun light
* This object can only be defined once.
The properties of an object are a dictionary of comma-separated keys and values, like
object {
key: value,
another_key: another_value,
}
Below is an example object. Each part will be explained beneath it.
aabb {
position: <0, 0, 0>,
size: <1, 1, 1>,
material: {
texture: solid(color(255, 80, 80)),
reflectiveness: 0.6,
}
}
This syntax creates an AABB object at the origin <0, 0, 0> with size <1, 1, 1>. Its material
declaration says that it has a solid red color texture, and is 60% reflective.
Each object has its own properties. For example, the position property on aabb is not valid
for, say, camera. Read on to see what properties are valid for what objects.
camera(defined once)vw(number), the view widthvh(number), the view heightorigin(vector), the origin of the camerayaw(number), the yaw of camera rotation in radianspitch(number), the pitch of camera rotation in radiansfov(number), the field of view of the camera in degrees
scene(defined once)max_ray_depth(number), the maximum number of rays that can bounce or refract from one source rayambient(color), the ambient color of objects receiving no light in the scene
skybox(defined once)type(string), dictates what type of skybox to use"normal": use the ray direction to determine color"solid": specifycolor(a color) to determine the color"cubemap": specifyimage(a string) to determine the image filename to use as a cubemap
aabb(a scene object)position* (vector), the center of the AABBsize* (vector), the distance from one corner to the center of the AABB (radial size if you will)material(dictionary), see below
mesh(a scene object)mesh* (string), the filename of the OBJ to load from, or alternatively:verts* (array of vectors), the vertex buffer to use for the mesh, andtris* (array of numbers), an array of numbers where each 3 consecutive numbers points to 3 different vertices in thevertsarraynormals(array of vectors), pass only if specifying verts/tris, the normal buffer to use for the mesh, andnormal_indices(array of numbers), pass only if specifying verts/tris/normals, an array of numbers where each 3 consecutive numbers points to 3 different normals in thenormalsarrayposition(vector), the center of the meshscale(number), the scale factorrotate_xyz(vector), a rotation vector for each axis (all in radians), applied in XYZ orderrotate_zyx(vector), a rotation vector for each axis (all in radians), applied in ZYX ordermaterial(dictionary), see below
plane(a scene object)origin* (vector), the origin of the planenormal(vector), the normal vector of the planeuv_wrap(number), the number of units before UVs on the plane wrap aroundmaterial(dictionary), see below
sphere(a scene object)position* (vector), the position of the sphereradius* (number), the radius of the spherematerial(dictionary), see below
point_light|pointlight(a light)position* (vector), the position of the point lightcolor(color), the color of the lightintensity(number), the intensity of the lightspecular_power(number), the power to raise specular light tospecular_strength(number), the coefficient of specular lightmax_distance(number), the max distance a hit can be before this light is no longer considered
sun|sun_light|sunlight(a light)vector* (vector), the vector this sun is facing (automatically normalized)color(color), the color of the sunintensity(number), the sun's intensityspecular_power(number), the power to raise specular light tospecular_strength(number), the coefficient of specular lightshadows(boolean), whether or not this sun should draw shadowsshadow_coefficient(number), what % of normal object color ambient light should be, from 0 - 1
* This property is required.
On all scene objects, the material property can be linked to a dictionary with the following
properties:
texture, which can be one of the following:solid(color), which sets the texture to a solid color, e.g.texture: solid(color(255, 0, 0))checkerboard(color_a, color_b), which sets the texture to a 2x2 checkerboard of colorscolor_aandcolor_b, e.g.texture: checkerboard(color(0, 0, 0), color(255, 255, 255))image(filename), which sets the texture to an image loaded fromfilename, e.g.texture: image("assets/texture.png")
reflectiveness, which is a number from 0 - 1, representing how reflective the object istransparency, which is a number from 0 - 1, representing how opaque or transparent the object isior, the index of refraction
Here is an example scene that renders a fedora, from assets/fedora.obj and assets/fedora.png.
The fedora will randomly be bigger or smaller in size.
camera {
vw: 1920,
vh: 1080,
origin: <1.5, 0.6, 3>,
yaw: -0.5,
pitch: -0.3,
}
sun {
vector: <-0.8, -1, -0.3>,
intensity: 0.8,
specular_power: 64,
}
mesh {
obj: "assets/fedora.obj",
position: <0, -0.5, 0>,
scale: random(0.5, 1.5),
material: {
texture: image("assets/fedora.png"),
}
}
plane {
origin: <0, -1, 0>,
material: {
reflectiveness: 0.6,
}
}