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:
TerrainShape
, for 3D heightfields in physics simulationsThis is a massive release with lots to chew on, so let's break things down into atomic, bite-sized pieces!
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.
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:
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!
Pictured above is chui, a UI library made entirely of these rounded rectangles.
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 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
.
There are 2 new builtin plugins and a new module:
http
plugin does HTTP requests, with support for HTTPS.enet
plugin is a UDP networking library (back as a plugin).utf8
module backports the utf8
library from Lua 5.3.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.
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.
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 })
The graphics module has been streamlined a bit as we shake out the new Vulkan renderer.
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.
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)
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.
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
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.
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 has been added to lovr.system
. You'll find the following new methods and callbacks:
lovr.system.getMouseX
lovr.system.getMouseY
lovr.system.getMousePosition
lovr.system.isMouseDown
lovr.mousepressed
lovr.mousereleased
lovr.mousemoved
lovr.wheelmoved
You might not need the lovr-mouse
library anymore!
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
.
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.
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
enet
plugin.http
plugin.utf8
module.:type
method to all objects and vectors.--graphics-debug
command line option, which sets t.graphics.debug
to true.lovr.audio.getDevice
.Image:mapPixel
.Blob:getString
that takes a byte range.Blob:getI8/getU8/getI16/getU16/getI32/getU32/getF32/getF64
.lovr.filesystem.load
to restrict chunks to text/binary.Pass:roundrect
.Pass:cone
that takes two vec3
endpoints.Pass:draw
that takes a Texture
.Pass:setViewCull
to enable frustum culling.Model:getBlendShapeWeight
, and Model:setBlendShapeWeight
.Model(Data):getBlendShapeCount
, Model(Data):getBlendShapeName
.Shader:getBufferFormat
.Mesh
object (sorry!).Model:clone
.materials
flag to lovr.graphics.newModel
.lovr.graphics.isInitialized
(mostly internal).depthResolve
feature to lovr.graphics.getFeatures
.Texture:newView
that creates a 2D slice of a layer/mipmap of a texture.Pass:send
that takes tables for uniform buffers.Buffer:getData
and Buffer:newReadback
.Texture:getPixels/setPixels/clear/newReadback/generateMipmaps
.t.graphics.debug
is set.lovr.graphics.is/setTimingEnabled
to record GPU durations for each Pass object.lovr.graphics.newPass
.Pass:setCanvas
and Pass:setClear
.Pass:getStats
.Pass:reset
.Pass:getScissor
and Pass:getViewport
.Pass:barrier
.Pass:beginTally/finishTally
and Pass:get/setTallyBuffer
.#include
in shader code.lovr.headset.getPassthrough/setPassthrough/getPassthroughModes
.hand/*/pinch
, hand/*/poke
, and hand/*/grip
Devices.lovr.headset
when lovr.graphics
is disabled, on supported runtimes.elbow/left
and elbow/right
poses on Ultraleap hand tracking.lovr.headset.getDirection
.lovr.headset.isVisible
and lovr.visible
callback.lovr.recenter
callback.floor
Device.t.headset.seated
and lovr.headset.isSeated
.lovr.headset.stopVibration
.radius
fields to joint tables returned by lovr.headset.getSkeleton
.Vec2
, Vec3
, Vec4
, Quat
, Mat4
).Vec3:transform
, Vec4:transform
, and Vec3:rotate
.vec3.up
, vec2.one
, quat.identity
, etc.).Mat4:getTranslation/getRotation/getScale/getPose
.Vec3:set
that takes a Quat
.Mat4:reflect
.TerrainShape
.World:queryBox
and World:querySphere
.World:getTags
.Shape:get/setPose
.lovr.physics.newMeshShape
function.Collider:isDestroyed
.lovr.system.wasKeyPressed/wasKeyReleased
.lovr.system.getMouseX
, lovr.system.getMouseY
, and lovr.system.getMousePosition
.lovr.system.isMouseDown
.lovr.mousepressed
, lovr.mousereleased
, lovr.mousemoved
, and lovr.wheelmoved
callbacks.lovr.system.has/setKeyRepeat
.Channel:push
and Channel:pop
.lovr.graphics.submit
to no longer invalidate Pass objects.Pass:setBlendMode
/Pass:setColorWrite
to take an optional attachment index.lovr.graphics.newModel
to work when the asset references paths starting with ./
.lovr.graphics.newModel
to error when the asset references paths starting with /
.lovr.graphics.newBuffer
to take format first instead of length/data.Pass:setStencilWrite
to only set the "stencil pass" action when given a single action, instead of all 3.lovr.graphics
to show a message box on Windows when Vulkan isn't supported.JNI_OnLoad
on Android so plugins can use JNI.t.headset.overlay
to also support numeric values to control overlay sort order.World:isCollisionEnabledBetween
to take nil
s, which act as wildcard tags.Mat4:__tostring
to print matrix components.World:raycast
to prevent subsequent checks when the callback returns false
.lovr.system.isKeyDown
to take multiple keys.lovr.headset.isDown/isTouched
to return nil instead of nothing.lovr.headset.getTime
to always start at 0 to avoid precision issues.lovr.headset.getDriver
to also return the OpenXR runtime name.pitchable
flag in lovr.audio.newSource
to default to true.Buffer:setData
, Buffer:clear
, and Buffer:getPointer
to work on permanent Buffers.lovr.timer.sleep
to have higher precision on Windows.lovr.headset.animate
to no longer require a Device
argument (current variant is deprecated).lovr.physics.newBoxShape
always creating a cube.MeshShape
not working properly with some OBJ models.Model:getNodeScale
to properly return the scale instead of the rotation.Mat4:set
and Mat4
constructors to properly use TRS order when scale is given.lovr.graphics.newShader
to work with nil
shader code (uses unlit
shader).t.graphics.debug
is set but the validation layers aren't installed.Pass:setProjection
to use an infinite far plane by default.Texture:hasUsage
.Pass:points
/ Pass:line
to work with temporary vectors.lovr.event.quit
to properly exit on Android/Quest.lovr.filesystem.getDirectoryItems
.lovr.graphics.newShader
to error properly if the push constants block is too big.lovr.log
to not print second string.gsub
result.lovr.data.newImage
wouldn't initialize empty Image
object pixels to zero.Model:getMaterial
to work when given a string.Vec3:angle
would sometimes return NaNs.normalMap
shader flag was set).DistanceJoint:getAnchors
.Material:getProperties
returned an incorrect uvScale.lovr.graphics.newMaterial
.Pass:capsule
didn't render anything when its length was zero.t.window = nil
wasn't working as intended.lovr.graphics.getPass
(lovr.graphics.newPass
should be used instead).Pass:getType
. All Pass objects support both compute and rendering (computes run before draws).Pass:getTarget
(renamed to Pass:getCanvas
).Pass:getSampleCount
(Pass:getCanvas
returns a samples
key).lovr.graphics.getBuffer
(Use lovr.graphics.newBuffer
or tables).lovr.graphics.newBuffer
that takes length/data as the first argument (put format first).lovr.headset.get/setDisplayFrequency
(it's named lovr.headset.get/setRefreshRate
now).lovr.headset.getDisplayFrequencies
(it's named lovr.headset.getRefreshRates
now).lovr.headset.getOriginType
(use lovr.headset.isSeated
).lovr.headset.animate
that takes a Device
argument (just pass the Model
now).Buffer:getPointer
(renamed to Buffer:mapData
).transfer
passes (methods on Buffer
and Texture
objects can be used instead).Pass:copy
(use Buffer:setData
and Texture:setPixels
).Pass:read
(use Buffer:newReadback
and Texture:newReadback
).Pass:clear
(use Buffer:clear
and Texture:clear
).Pass:blit
(use Texture:setPixels
).Pass:mipmap
(use Texture:generateMipmaps
).Tally
(use lovr.graphics.setTimingEnabled
or Pass:beginTally/finishTally
).lovr.event.pump
(it's named lovr.system.pollEvents
now).t.headset.offset
(use t.headset.seated
).mipmaps
flag from render passes (they always regenerate mipmaps now).