v0.17.0

Edit

LÖVR v0.17.0, codename Tritium Gourmet, was released on October 14th, 2023. This version has been 364 days in the making, with 555 commits from 9 authors.

This release includes tons of bugfixes and usability improvements for the new graphics module, along with the following new features:

This is a massive release with lots to chew on, so let's break things down into atomic, bite-sized pieces!

Passthrough

With passthrough, you can now layer a view of the real world underneath whatever your project renders. This is great for mixed reality experiences, or just as a way to avoid tripping over furniture and punching walls. To enable it, just call lovr.headset.setPassthrough(true).

LÖVR also sets OpenXR blend modes now, which means projects will render properly on AR devices like the Magic Leap and HoloLens.

Blend Shapes

Model now supports blend shapes! These are often used for facial animation or other types of mesh squishing, which is difficult to implement with skeletal animation. Model:setBlendShapeWeight sets the weight of a blend shape, and weights can be animated with keyframe animations as well.

They also use compute shaders instead of vertex shaders. This means:

Roundrect

Thick rounded rectangles are a very common shape to use for UI in VR. Previously we all had to generate meshes, import them as models, use SDF shaders, or piece them together with cylinders and boxes. Now, Pass:roundrect is built in!

A virtual keyboard made of rounded rectangles

Pictured above is chui, a UI library made entirely of these rounded rectangles.

TerrainShape

TerrainShape is a new physics shape that lets you add heightfields to physics simulations. Terrain can be provided as an Image, or as a Lua function for procedural terrain.

Also, if you need a simple ground plane, you can just pass a size to get flat terrain. It's much more convenient than messing around with a box collider.

Frustum Culling

Frustum culling is an optimization that skips rendering objects that are out of view. For 3D scenes with content surrounding the player, this is a quick way to reduce GPU overhead, especially when objects have lots of vertices.

Frustum culling can be enabled using Pass:setViewCull. Any object with a bounding box will be culled against the cameras, including Model objects and most shape primitives. Mesh objects can compute their bounding boxes with Mesh:computeBoundingBox.

Plugins

There are 2 new builtin plugins and a new module:

The cool part about having these as plugins is that they are 100% optional -- you can delete the library files if you don't need them and LÖVR will still work fine.

Universal APK

Previously, APK downloads were only compatible with the Oculus Quest. In v0.17.0 LÖVR switched to a "universal" APK system where multiple OpenXR loaders are bundled in a single APK. As a result, APKs will now work on most if not all Android-based headsets. This does increase file size a bit, but it should be alleviated as more vendors converge on the standard OpenXR loader.

Buffer Format Improvements

There's a handy new Shader:getBufferFormat method which will parse the format of a buffer from shader code. This means you don't need to type out the buffer's format again in Lua and keep it in sync with the shader code:

format = shader:getBufferFormat('mybuffer')
buffer = lovr.graphics.newBuffer(format, data)

Buffer formats and shader constants now support nested structure and array types. Buffer fields can also have names, and buffer data can be given as key-value pairs instead of only lists of numbers.

Finally, you can also send a table directly to a uniform buffer variable instead of needing to create a buffer first.

pass:send('lightData', { position = vec3(x, y, z), color = 0xffffee })

Graphics Improvements

The graphics module has been streamlined a bit as we shake out the new Vulkan renderer.

Temporary Objects

Temporary Buffer/Pass objects were really tricky due to the way they got invalidated whenever lovr.graphics.submit was called:

pass = lovr.graphics.getPass('render', canvas)
-- do stuff with pass
lovr.graphics.submit(pass)
pass:cube() --> Error!  Can't use the pass after it's submitted!

This version, lovr.graphics.getBuffer and lovr.graphics.getPass have been deprecated and replaced by lovr.graphics.newBuffer and lovr.graphics.newPass. These "permanent" types behave like all other objects, and you can call lovr.graphics.submit without messing them up.

For passes, instead of getting a new one every frame, you can create it once and call Pass:reset at the beginning of a frame to reset it to a fresh state. There's also the option of recording its draws once and submitting it over and over again, to reduce the Lua overhead of recording draws.

Pass Types

Passes no longer have a "type" that defines what commands can be recorded on them. Instead, all Pass objects can receive both graphics and compute work, with computes running before the draws whenever the pass is submitted.

For transfers, these methods have been moved onto Buffer and Texture objects themselves. So if you want to change the data in a Buffer, call Buffer:setData. Uploading to a Texture is Texture:setPixels. There's no need for a dedicated transfer pass. The transfers happen in order and finish before subsequent graphics submissions.

-- Old
local pass = lovr.graphics.getPass('transfer')
pass:copy(image, texture)
lovr.graphics.submit(pass)

-- New
texture:setPixels(image)

Mesh

The Mesh object has returned! This is mostly a convenience object that holds a vertex and index buffer, a Material, draw range, and bounding box. Pass:mesh still exists as a lower-level way to draw meshes with Buffer objects.

Compute Barriers

This is a small change, but there's a new Pass:barrier function that lets you sequence multiple compute shader dispatches within a pass. Since computes within a pass all ran at the same time, you previously had to use multiple Pass objects to get computes to wait for each other, which is costly. With Pass:barrier, all computes before the barrier will finish before further compute work starts.

pass:setShader(computer)
pass:compute()
pass:compute()
pass:barrier()
pass:compute() --> this compute will wait for the first two

Headless VR

The headset module can now be used in headless mode (spooky!). This means it will still work even when the graphics module is disabled. The intended use case is for console applications that don't need to render anything but still want to use pose data. Note that this only works on certain XR runtimes -- currently monado and SteamVR are known to work.

New Headset Simulator

The headset simulator incorporated changes from the lovr-mousearm library. The virtual hand is now placed at the projected mouse position, and the scroll wheel can be used to control the hand distance. The shift key can also be used to "sprint", which is great for moving through large worlds.

Mouse Input

Mouse input has been added to lovr.system. You'll find the following new methods and callbacks:

You might not need the lovr-mouse library anymore!

Tally Changes

Recording GPU timing info is now as simple as calling lovr.graphics.setTimingEnabled. Stats will be made available via Pass:getStats, with a frame or two of delay. Timing stats are also active by default when t.graphics.debug is set.

Pixel tallies (occlusion queries) also have a new revamped API. Instead of using a Tally object and a transfer pass, Pass:beginTally and Pass:finishTally will start and stop an occlusion query. The results for tallies can be copied to a Buffer after the Pass with Pass:setTallyBuffer.

Quality of Life

There are also a bunch of small improvements worth mentioning.

Drawing a texture on a plane no longer requires a call to Pass:setMaterial. Instead, Pass:draw can take a Texture:

-- Old
pass:setMaterial(texture)
pass:plane(x, y, z, w, h)
pass:setMaterial()

-- New
pass:draw(texture, x, y, z, w)

All objects now have the Object:type method to return their type name as a string.

There's a new Image:mapPixel function which is an easier way to set all the pixels of an Image.

Shaders now support #include to load shader code from LÖVR's filesystem.

When t.graphics.debug is set, shaders include debug info now. This allows graphics debugging tools like RenderDoc to inspect variables in a shader and step through each line interactively.

Vectors have capitalized constructors to create permanent vectors, e.g. Vec3 can be used in addition to vec3.

Vectors also have named constants: vec3.up, vec4.one, etc.

Physics has World:queryBox and World:querySphere to query all the Shapes that intersect a volume.

Community

LÖVR's Slack is now deprecated because Slack held our messages hostage! We migrated all the chat history over to a Matrix homeserver hosted on #community:lovr.org and bridged everything to a new Discord server. Keeping the source of truth for chat on a self-hosted, open source platform ensures the community is hopefully a bit more resilient to future corporate monkey business.

Speaking of corporate monkey business, LÖVR has entrenched itself further into the GitHub ecosystem by adding continuous builds via GitHub Actions! This means us mere mortals can have up-to-date builds for all platforms without touching CMake or the Android SDK.

For the remaining masochists among us who choose to build LÖVR from source, there has been a change to the branching system. The dev branch is now the default branch, and master has been renamed to stable. The development workflow otherwise remains the same, where new features go into dev and bugfixes are made on stable. The following commands will update a local clone:

$ git branch -m master stable
$ git fetch origin
$ git branch -u origin/stable stable
$ git remote set-head origin -a

Changelog

Add

General

Audio

Data

Filesystem

Graphics

Headset

Math

Physics

System

Thread

Change

Fix

Deprecate

Remove