v0.18.0
===
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:
- Fixed Foveated Rendering
- OpenXR Compositor Layers
- Stylus Input
- BMFont
- Filesystem Watching
- Shader Code Improvements
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:
- `filter` lets you filter collisions with Lua code. It can be used when the tag system isn't
powerful enough to express when objects should collide.
- `enter` is fired when 2 objects start touching, useful for playing sounds or spawning particles.
- `exit` is fired when 2 objects stop touching.
- `contact` is fired every frame while 2 objects are touching. This can be useful to adjust the
friction dynamically or break contact if some condition is met.
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:
- `WeldJoint` glues two colliders together, restricting all movement between them.
- `ConeJoint` allows two colliders to rotate a fixed angle away from each other, good for ropes.
#### 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:
- Quality: Layers look MUCH better than textures rendered in the 3D scene. This is because
the system renders the layer content after lens distortion is applied to the 3D scene, so the
layer pixels are sampled more accurately. The layer can also use a higher resolution than the
main display texture. All of this combined means text is especially more legible when drawn to a
layer.
- Stability: The system is responsible for rendering the layer, and this happens much later
in the composition process. Because of this, the system is able to use a more accurate head pose
to position the layer, and this makes them feel much more stable, further improving text
legibility.
Layers can also be curved. Pictured below is the [`lovr-neovim`](https://github.com/bjornbytes/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
---
- There are unit tests now! Just run `lovr test` in the repository.
- There is a changelog now! See `CHANGES.md` in the repository.
- There are new `x86_64` Android CI builds for Magic Leap 2.
- immortalx made [ARKANOID EVO](https://github.com/immortalx74/arkanoid_evo), [freecell](https://github.com/immortalx74/freecell), and [sudoku](https://github.com/immortalx74/sudoku)
- josip pushed a major update to [chui](https://github.com/jmiskovic/chui), a diegetic UI library.
- micouay wrote 2 blog posts: [Drag & drop in VR with
LÖVR](https://micouy.github.io/drag-and-drop-in-vr-with-lovr/) and [My LÖVR workflow](https://micouy.github.io/my-lovr-workflow/).
- Udinanon made a gaussian splat viewer: [Gaussian Splatting viewer in OpenGL, using LOVR](https://martinoshelf.neocities.org/gaussiansplatting-in-lovr)
- Udinanon made a spherical harmonics viewer: [Spherical Harmonics and surfaces, an attempt](https://martinoshelf.neocities.org/spherical-harmonics-lovr)
- xiejiangzhi released [imgui bindings](https://github.com/xiejiangzhi/cimgui-love/tree/lovr) and a [tracy integration](https://github.com/xiejiangzhi/luajit_tracy).
Changelog
---
### Add
#### General
- Add support for declaring objects as to-be-closed variables in Lua 5.4.
#### Filesystem
- Add `--watch` CLI flag, `lovr.filechanged` event, and `lovr.filesystem.watch/unwatch`.
- Add `File` object and `lovr.filesystem.newFile`.
- Add `lovr.filesystem.getBundlePath` (for internal boot code).
- Add `lovr.filesystem.setSource` (for internal boot code).
#### Graphics
- Add `Pass:polygon`.
- Add `Shader:hasVariable`.
- Add support for BMFont in `Font` and `Rasterizer`.
- Add support for `uniform` variables in shader code.
- Add support for cubemap array textures.
- Add support for transfer operations on texture views.
- Add support for nesting texture views (creating a view of a view).
- Add `sn10x3` `DataType`.
- Add `border` `WrapMode`.
- Add support for loading glTF models with 8 bit indices.
- Add support for `d24` texture format.
- Add support for `SampleID`, `SampleMaskIn`, `SampleMask`, and `SamplePosition` in pixel shaders.
- Add support for `layout(scalar)` buffers and `packedBuffers` graphics feature.
- Add `raw` flag to `lovr.graphics.newShader`.
- Add `Texture:getLabel`, `Shader:getLabel`, and `Pass:getLabel`.
- Add `Model:resetBlendShapes`.
#### Headset
- Add `Layer` object, `lovr.headset.newLayer`, and `lovr.headset.get/setLayers`.
- Add `lovr.headset.setBackground`.
- Add `stylus` Device, `nib` DeviceButton, and `nib` DeviceAxis.
- Add support for Logitech MX Ink input.
- Add `lovr.headset.get/setFoveation`.
- Add `t.headset.controllerskeleton` to control how controllers return hand tracking data.
- Add `controller` field to the table returned by `lovr.headset.getSkeleton`.
- Add `t.headset.mask`.
- Add back `lovr.headset.isMounted` and the `lovr.mount` callback.
- Add `lovr.headset.stop`, `lovr.headset.isActive`, and `t.headset.start`.
- Add `lovr.headset.getFeatures`.
- Add `t.headset.debug` to enable additional messages from the VR runtime.
- Add `--simulator` CLI flag to force use of simulator headset driver.
- Add `lovr.headset.getHandles`.
#### Math
- Add `Quat:get/setEuler`.
#### Physics
- Add variant of `lovr.physics.newWorld` that takes a table of settings.
- Add `World:interpolate`.
- Add `World:get/setCallbacks` and `Contact` object.
- Add `World:getColliderCount`.
- Add `World:getJointCount` and `World:getJoints`.
- Add `World:shapecast`.
- Add `World:overlapShape`.
- Add `Collider:get/setGravityScale`.
- Add `Collider:is/setContinuous`.
- Add `Collider:get/setDegreesOfFreedom`.
- Add `Collider:applyLinearImpulse` and `Collider:applyAngularImpulse`.
- Add `Collider:moveKinematic`.
- Add `Collider:getShape`.
- Add `Collider:is/setSensor` (replaces `Shape:is/setSensor`).
- Add `Collider:get/setInertia`.
- Add `Collider:get/setCenterOfMass`.
- Add `Collider:get/setAutomaticMass`.
- Add `Collider:resetMassData`.
- Add `Collider:is/setEnabled`.
- Add `ConvexShape`.
- Add `WeldJoint`.
- Add `ConeJoint`.
- Add `Joint:getForce` and `Joint:getTorque`.
- Add `Joint:get/setPriority`.
- Add `Joint:isDestroyed`.
- Add `DistanceJoint:get/setLimits`.
- Add `:get/setSpring` to `DistanceJoint`, `HingeJoint`, and `SliderJoint`.
- Add `HingeJoint:get/setFriction` and `SliderJoint:get/setFriction`.
- Add `SliderJoint:getAnchors`.
- Add `Shape:raycast` and `Shape:containsPoint`.
- Add `Shape:get/setDensity`.
- Add `Shape:getMass/Volume/Inertia/CenterOfMass`.
- Add `Shape:get/setOffset`.
- Add motor support to `HingeJoint` and `SliderJoint`.
- Add support for creating a `MeshShape` from a `ModelData`.
#### System
- Add `lovr.system.isWindowVisible` and `lovr.system.isWindowFocused`.
- Add `lovr.system.wasMousePressed` and `lovr.system.wasMouseReleased`.
- Add `lovr.system.get/setClipboardText`.
- Add `lovr.system.openConsole` (for internal Lua code).
- Add `KeyCode`s for numpad keys.
#### Thread
- Add table support to `Channel:push`.
- Add `lovr.thread.newChannel`.
- Add `t.thread.workers` to configure number of worker threads.
### Change
- Change nogame screen to be bundled as a fused zip archive.
- Change `Mesh:setMaterial` to also take a `Texture`.
- Change shader syntax to no longer require set/binding numbers for buffer/texture variables.
- Change `Texture:getFormat` to also return whether the texture is linear or sRGB.
- Change `Texture:setPixels` to allow copying between textures with different formats.
- Change `Readback:getImage` and `Texture:getPixels` to return sRGB images when the source Texture is sRGB.
- Change `lovr.graphics.newTexture` to use a layer count of 6 when type is `cube` and only width/height is given.
- Change `lovr.graphics.newTexture` to default mipmap count to 1 when an Image is given with a non-blittable format.
- Change `lovr.graphics.newTexture` to error if mipmaps are requested and an Image is given with a non-blittable format.
- Change `TextureFeature` to merge `sample` with `filter`, `render` with `blend`, and `blitsrc`/`blitdst` into `blit`.
- Change headset simulator movement to slow down when holding the control key.
- Change headset simulator to use `t.headset.supersample`.
- Change `lovr.graphics.compileShader` to take/return multiple stages.
- Change maximum number of physics tags from 16 to 31.
- Change `TerrainShape` to require square dimensions.
- Change `MeshShape` constructors to accept a `MeshShape` to reuse data from.
- Change `MeshShape` constructors to take an optional scale to apply to the vertices.
- Change `Buffer:setData` to use more consistent rules to read data from tables.
- Change `World:queryBox/querySphere` to perform coarse AABB collision detection (use `World:overlapShape` for an exact test).
- Change `World:queryBox/querySphere` to take an optional set of tags to include/exclude.
- Change `World:queryBox/querySphere` to return the first collider detected, when the callback is nil.
- Change `World:queryBox/querySphere` to return nil when a callback is given.
- Change `World:raycast` to take a set of tags to allow/ignore.
- Change `World:raycast` callback to be optional (if nil, the closest hit will be returned).
- Change `World:raycast` to also return the triangle that was hit on MeshShapes.
- Change physics queries to report colliders in addition to shapes.
- Change `tostring` on objects to also include their pointers.
- Change `desktop` HeadsetDriver to be named `simulator`.
- Change `--graphics-debug` CLI flag to be named `--debug`.
- Change `--version` and `lovr.getVersion` to also return the git commit hash.
- Change `Pass:setViewport/Scissor` to be per-draw state instead of per-pass.
- Change `Image:get/set/mapPixel` to support `r16f`, `rg16f`, and `rgba16f`.
- Change `Image:getPixel` to return 1 for alpha when the format doesn't have an alpha component.
- Change stack size of `state` stack (used with `Pass:push/pop`) from 4 to 8.
- Change `lovr.focus` and `lovr.visible` to also get called for window events.
- Change `lovr.focus` and `lovr.visible` to have an extra parameter for the display type.
### Fix
- Fix `t.headset.submitdepth` to actually submit depth.
- Fix depth write when depth testing is disabled.
- Fix "morgue overflow" error when creating or destroying large amounts of textures at once.
- Fix `Texture:getType` when used with texture views.
- Fix possible negative `dt` in lovr.update when restarting with the simulator.
- Fix issue where `Collider`/`Shape`/`Joint` userdata wouldn't get garbage collected.
- Fix OBJ triangulation for faces with more than 4 vertices.
- Fix possible crash when using vectors in multiple threads.
- Fix possible crash with `Blob:getName`.
- Fix issue when sampling from depth-stencil textures.
- Fix bug when rendering meshes from `Model:getMesh`.
- Fix bug with `Curve:slice` when curve has more than 4 points.
- Fix bug with `hand/*/pinch` and `hand/*/poke` device poses.
- Fix bug when loading glTF models that use the `KHR_texture_transform` extension.
### Deprecate
- Deprecate `World:get/setTightness` (use `stabilization` option when creating World).
- Deprecate `World:get/setLinearDamping` (use `Collider:get/setLinearDamping`).
- Deprecate `World:get/setAngularDamping` (use `Collider:get/setAngularDamping`).
- Deprecate `World:is/setSleepingAllowed` (use `allowSleep` option when creating World).
- Deprecate `World:get/setStepCount` (use `positionSteps`/`velocitySteps` option when creating World).
- Deprecate `Collider:is/setGravityIgnored` (use `Collider:get/setGravityScale`).
### Remove
- Remove `lovr.headset.getOriginType` (use `lovr.headset.isSeated`).
- Remove `lovr.headset.getDisplayFrequency` (renamed to `lovr.headset.getRefreshRate`).
- Remove `lovr.headset.getDisplayFrequencies` (renamed to `lovr.headset.getRefreshRates`).
- Remove `lovr.headset.setDisplayFrequency` (renamed to `lovr.headset.setRefreshRate`).
- Remove `lovr.graphics.getBuffer` (use `lovr.graphics.newBuffer`).
- Remove variant of `lovr.graphics.newBuffer` that takes length first (format is first now).
- Remove `location` key for Buffer fields (use `name`).
- Remove `Buffer:isTemporary`.
- Remove `Buffer:getPointer` (renamed to `Buffer:mapData`).
- Remove `lovr.graphics.getPass` (use `lovr.graphics.newPass`).
- Remove `Pass:getType` (passes support both render and compute now).
- Remove `Pass:getTarget` (renamed to `Pass:getCanvas`).
- Remove `Pass:getSampleCount` (`Pass:getCanvas` returns sample count).
- Remove variant of `Pass:send` that takes a binding number instead of a variable name.
- Remove `Texture:newView` (renamed to `lovr.graphics.newTextureView`).
- Remove `Texture:isView` and `Texture:getParent`.
- Remove `Shape:setPosition`, `Shape:setOrientation`, and `Shape:setPose` (use `Shape:setOffset`).
- Remove `Shape:is/setSensor` (use `Collider:is/setSensor`).
- Remove `World:overlaps/computeOverlaps/collide/getContacts` (use `World:setCallbacks`).
- Remove `BallJoint:setAnchor`.
- Remove `DistanceJoint:setAnchors`.
- Remove `HingeJoint:setAnchor`.
- Remove `BallJoint:get/setResponseTime` and `BallJoint:get/setTightness`.
- Remove `DistanceJoint:get/setResponseTime` and `DistanceJoint:get/setTightness` (use `:setSpring`).
- Remove `DistanceJoint:get/setDistance` (renamed to `:get/setLimits`).
- Remove `HingeJoint:get/setUpper/LowerLimit` (use `:get/setLimits`).
- Remove `SliderJoint:get/setUpper/LowerLimit` (use `:get/setLimits`).
- Remove `Collider:getLocalCenter` (renamed to `Collider:getCenterOfMass`).
- Remove `Shape:getMassData` (split into separate getters).
- Remove `shaderConstantSize` limit from `lovr.graphics.getLimits`.
- Remove `Pass:getViewport` and `Pass:getScissor`.