v0.18.0

Edit

LÖVR v0.18.0, codename Dream Eater, was released on February 14th, 2025. This version has been 490 days in the making, with 899 commits from 8 authors.

The main highlight of this release is a brand new physics engine, Jolt Physics! In addition to tons of new physics features, this version also added:

Grab a snack and get comfortable, hopefully the rest of this doesn't put you to sleep!

Jolt Physics

This version LÖVR switched from using the venerable ODE to Jolt Physics. Jolt is faster, more stable, multithreaded, and deterministic. There were also lots of new features and improvements added to the lovr.physics API as part of the switch.

Shapecasts

Shapecasts are similar to raycasts, except instead of detecting collision along a line they sweep a whole shape through the scene and find anything it touches. They're very helpful for player locomotion and projectile hit tests.

Continuous Collision Detection

Normally the physics engine will compute a new position for a collider, and then detect and resolve any collisions at the new location. This usually works fine, but fast-moving objects can pass through walls if they move fully through through them during one physics step:

CCD (continuous collision detection) is a technique that solves this problem by checking for collisions along the object's whole path, instead of just checking the final position. It's great for fast moving projectiles. Just call Collider:setContinuous on any colliders that need CCD.

Axis Locking

It's now possible to lock translation and rotation of a Collider on specific axes with Collider:setDegreesOfFreedom. Previously this required hacks like setting very high damping. It's nice for keeping objects upright, or for an object that only needs to move in one direction, like elevators.

Collision Callbacks

There is a new World:setCallbacks function that lets you set callbacks when various collision events occur. There are 4 different callbacks:

The enter and contact callbacks receive a Contact object which has lots of useful information about the collision, including the exact Shapes that are touching, how much they're overlapping, the contact points, and the contact normal.

These callbacks replace the older method that used a callback in World:update, along with World:overlaps and World:collide. World:getContacts is also superseded by the more powerful Contact object.

MeshShape Improvements

MeshShape has received a lot of improvements. First, it's been split into 2 shapes! Now, MeshShape is a kinematic-only shape like TerrainShape, intended to be used for static level geometry.

For dynamic objects with arbitrary shapes, ConvexShape is a new shape that represents a "convex hull" of a triangle mesh. ConvexShape can be created from all the same things as MeshShape can, but it won't tank the performance of the simulation like MeshShape used to do.

Also, it's possible to create copies of MeshShape and ConvexShape. The copies will reuse the mesh data from the original shape, and they can have a different scale. This makes it possible to stamp out copies of objects with complex collision meshes without any frame hitches.

Raycast Triangles

World:raycast now returns the triangle that was hit for MeshShapes. This can be used to look up other vertex data like UVs, which lets you do things like paint on a Model!

Raycast Filters

All physics queries like raycasts and shapecasts now take a list of tags to filter by. Narrowing down the set of objects that the physics engine needs to check like this can give a big speedup when doing lots of collision queries on a dense scene!

-- Only check if we hit an enemy or a wall, skip everything else
world:raycast(from, to, 'enemy wall')

-- Ignore hits on sensors
world:raycast(from, to, '~sensor')

Joints

There are 2 new types of joints:

Breakable Joints

Breakable joints are now supported! Joint:getForce and Joint:getTorque return the amount of force/torque the physics engine is using to enforce the joint's constraint, allowing the joint to be disabled or destroyed when the force gets too high.

Springs

DistanceJoint, HingeJoint, and SliderJoint now have :setSpring methods, which make their limits "soft" and bouncy. Hinge and slider joints also have :setFriction which causes them to require some extra force to move.

Motors

Hinge and slider joints have motors now. The motor can be used to move the joint back to a desired position or rotation, or to get it to move at a target speed. The motor has a maximum amount of force it can apply, and has its own springiness. Motors are useful when designing robots or other physics-based animation rigs.

Fixed Timestep

A new World:interpolate function makes it much easier to implement fixed-timestep physics simulation, where the physics loop is run at a different rate than the normal frame loop.

Above, both simulations are running at 15Hz, but the version on the right uses World:interpolate to smooth out the collider poses between the last 2 physics steps.

Layers

The headset module now supports composition layers, exposed as a new Layer object. Layers are 2D "panels" placed in 3D space. This might seem redundant, since you can already draw a texture in 3D space with Pass:draw, but layers have several important benefits:

Layers can also be curved. Pictured below is the lovr-neovim library that renders a neovim editor on a curved panel in front of you:

In addition to the Layer object, there is also a new lovr.headset.setBackground function, which uses the same techniques but for a skybox (cubemap or equirectangular). For static backgrounds this can reduce rendering costs significantly, since the main headset pass doesn't need to render the background at all, which uses a significant amount of time on mobile GPUs.

Fixed Foveated Rendering

Fixed foveated rendering is an optimization that renders at a lower resolution around the edges of the display. The drop in quality is often imperceptible due to lens distortion, but the GPU savings can be as high as 40% for scenes with heavy pixel shading! It's really easy to enable, just call lovr.headset.setFoveation:

lovr.headset.setFoveation('medium', true)

The second parameter defaults to true and specifies whether the system is allowed to dynamically lower the foveation level based on GPU load.

Stylus Input

There is a new stylus device for pens like the Logitech MX Ink. The device exposes a 3D pose, velocity, a pressure-sensitive nib axis, and the 3 buttons on the pen.

BMFont

In addition to TTF fonts, LÖVR now supports BMFont fonts. BMFont files contain an atlas of glyph images, along with a small text file containing metadata. Compared to the existing MSDF fonts, BMFonts look better and are more efficient for 2D text, making them a great fit for rendering text on the new Layer objects. Since the image atlas can contain anything, they can also be used for icons/emoji. However, they don't support scaling like MSDF fonts do, so they can get blurry when rendering text in 3D space.

Live Reloading

LÖVR has built in filesystem monitoring now! Simply add the --watch command line argument:

lovr --watch .

When watching is enabled, the lovr.filechanged callback will be fired whenever a file changes. The default implementation restarts the project with lovr.event.restart, but you can override it to ignore certain files or perform more granular asset reloading without performing a full restart.

Under the hood this uses efficient native APIs for filesystem events, so it's more lightweight and responsive than polling file modification times with lovr.filesystem.getLastModified.

Shader Improvements

There have been a ton of tiny improvements to the shader syntax. These are all optional, so existing shaders will continue to work.

First, uniform variables no longer require the set and binding decorations:

// Old
layout(set = 2, binding = 0) uniform texture2D sparkles;

// New
uniform texture2D sparkles;

Vertex attributes no longer require location decorations:

// Old
layout(location = 0) in vec3 displacement;

// New
in vec3 displacement;

Instead of a Constants block, regular uniform variables are supported. This makes it easier to port code from other shaders.

// Old
Constants {
  vec3 direction;
  float radius;
  vec2 size;
};

// New
uniform vec3 direction;
uniform float radius;
uniform vec2 size;

Uniform and storage buffers support a scalar layout, which removes confusing padding requirements and doesn't require Buffer formats to specify a std140 or std430 layout:

layout(scalar) uniform Light {
  vec3 direction;
  vec3 color;
};

// newBuffer({{ 'direction', 'vec3' }, { 'color', 'vec3' }}) works!
// No need for { layout = 'std140' }

There is also a new raw flag in lovr.graphics.newShader which will create a completely raw shader without any of the LÖVR helpers:

lovr.graphics.newShader([[
  in vec3 position;
  void main() {
    gl_Position = vec4(position, 1);
  }
]], [[
  out vec4 pixel;
  void main() {
    pixel = vec4(1, 0, 1, 1);
  }
]], { raw = true })

Small Stuff

The lovr.headset.isMounted getter and lovr.mount callback are back! These tell you whether the headset is currently on someone's head, using the proximity sensor.

There's a new File object which can be used to do multiple file operations on a file without having to reopen it every time, or for partial reads/writes.

Quat has new methods for working with euler angles: Quat:getEuler and Quat:setEuler.

Channel:push supports Lua tables now!

Cubemap arrays are supported: cube textures can be created with any layer count as long as it's a multiple of 6.

MSAA textures can now use sample counts of 2, 8, and 16 (instead of just 1 and 4), and they can be correctly sent to multisample shader variables like texture2DMSAA. Additionally, Pass:setCanvas accepts a set of resolve attachments that can be used to do custom MSAA resolves.

Texture, Shader, and Pass can be created with debug labels, which will show in graphics debuggers like RenderDoc.

The lovr executable is now shipped as a zip archive on all platforms, with the nogame screen and command line parser packaged like a regular fused project. This makes it easy to customize the nogame screen, or extend the CLI with new options. It's even possible to put Lua libraries in the zip, which will be globally accessible to all projects run with that copy of LÖVR.

Community

Changelog

Add

General

Filesystem

Graphics

Headset

Math

Physics

System

Thread

Change

Fix

Deprecate

Remove