v0.18.0.md 24 KB

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 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

  • 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 KeyCodes 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.