gaze-based controls

This commit is contained in:
Leon van Kammen 2026-07-07 17:48:47 +02:00
parent 0930bd7544
commit f11b420197
9 changed files with 222 additions and 118 deletions

View file

@ -30,4 +30,14 @@ ecs.clear = function()
api.world.commit()
end
ecs.extension = {
name = "ecs",
enabled = true,
index = 300,
init = function(api) ecs.init(api) end,
draw = function(pass)
ecs.update( api.world, pass, ecs.filterDraw )
end
}
return ecs

View file

@ -5,49 +5,82 @@ local gctl
gctl = {
name = "gctl",
enabled = true,
name = "gazecontrol",
enabled = (lovr ~= nil),
index = api.ext.max * 0.9, -- important: render last! (renderOrder)
texture = {},
material = nil,
dtmax = 1,
dt = 0,
visor = {
rotate = { x = 0, y = 0, z = 0},
scaleTo = 0.1,
scale = 0.1,
scaleSpeed = 0.005,
walkingSpeed = 2,
texture = false,
textures = {}
},
init = function()
--lovr.mouse.setRelativeMode(true)
gctl.initVisor()
gctl.extendECS()
api.player.matrix = lovr.math.newMat4()
end,
load = function()
gctl.texture.idle = lovr.graphics.newTexture("ext/gazecontrol/reticle_idle.png")
end,
initVisor = function()
-- add visor object
gctl.visor = {
rotate = { x = 0, y = 0, z = 0},
scale = 0.1,
texture = gctl.texture.idle
}
local visor = gctl.visor
visor.textures.idle = lovr.graphics.newTexture("ext/gazecontrol/reticle_idle.png")
visor.textures.hover = lovr.graphics.newTexture("ext/gazecontrol/reticle_hover.png")
visor.texture = visor.textures.idle
end,
update = function(dt, input)
local visor = gctl.visor
if input.pointer.hover then
visor.texture = visor.textures.hover
if visor.scale == visor.scaleTo then
visor.scale = 0.05
end
else
visor.texture = visor.textures.idle
visor.scale = visor.scaleTo
end
if input.pointer.wheel then -- move forward/backward via scrollwheel
print( input.pointer.wheel.y)
local direction = vector(lovr.headset.getDirection('head'))
if not api.player.flying then direction = vector(direction.x, 0, direction.z) end
-- move forward/backward via scrollwheel
api.player.matrix:translate(direction * (input.pointer.wheel.y * visor.walkingSpeed) )
end
end,
draw = function(pass,dt)
-- *TODO* render this last (as it needs to be rendered after skybox & floor)
local posx, posy, posz = lovr.headset.getPosition('head')
local pos = vector( lovr.headset.getPosition('head') ) + vector( api.player.matrix:getPosition() )
local dirx, diry, dirz = lovr.headset.getDirection('head')
local distance = 1
local x = posx + distance * dirx
local y = posy + distance * diry
local z = posz + distance * dirz
local x = pos.x + distance * dirx
local y = pos.y + distance * diry
local z = pos.z + distance * dirz
local angle, ax, ay, az = lovr.headset.getOrientation(device)
local rot = gctl.visor.rotate
local scale = gctl.visor.scale
pass:setMaterial(gctl.texture.idle)
pass:setShader() --'unlit')
pass:setBlendMode('alpha')
pass:setColor(1, 1, 1)
local visor = gctl.visor
local rot = visor.rotate
local scale = visor.scale
if scale < visor.scaleTo then
scale = scale + visor.scaleSpeed
visor.scale = scale
end
pass:setMaterial(visor.texture)
pass:setColor(1, 1, 1, 1)
pass:setCullMode('back')
--pass:setFaceCull()
--pass:setDepthWrite(true)
pass:setBlendMode("alpha", "alphamultiply")
--pass:setColorWrite(true)
pass:plane(x, y, z, scale, scale, angle, ax, ay, az)
end,

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -9,10 +9,8 @@ return {
--api.ext.skeleton.extendECS()
end,
update = function(dt) end,
update = function(dt,input) end,
load = function() end,
draw = function(pass,dt) end,
-- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc

View file

@ -5,6 +5,7 @@ local shader
return {
name = "startupscene",
enabled = (lovr ~= nil),
index = api.ext.max * 0.1,
init = function() end,
@ -51,13 +52,15 @@ return {
pass:setColor(1, 1, 1)
pass:setMaterial(envTex)
pass:setDepthWrite(false)
pass:draw(envTex, 0, 0, 0, 512, math.pi * 0.5, 1, 0, 0)
-- floor
--pass:setMaterial()
pass:setMaterial()
pass:setDepthWrite(true)
pass:setShader()
pass:setCullMode('front')
pass:setBlendMode('alpha')
--pass:setCullMode('front')
--pass:setBlendMode('alpha')
pass:setMaterial()
pass:setColor(0.8,0.8,0.8,1)
pass:plane(0, 0, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "fill", 5, 5)

View file

@ -31,7 +31,10 @@ api = {
ecs = ecs,
world = ecs.world(),
protocol = {},
player = { x = 0, y = 0, z = 0, ox = 0, oy = 1, oz = 0, oangle = 0, flying = true },
ext = { -- all extensions are loaded here from disk at runtime
ecs = ecs.extension,
max = 10000, -- max plugins (used for sorting)
exec = function(...) util.exec(api.ext,...) end -- util function to call func on each extension
},
i = 1
@ -39,13 +42,10 @@ api = {
api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime api
util.loaddir( "ext", api, api.ext )
require( runtime.path .. "/main")
util.loaddir( "ext", api, api.ext )
util.loaddir( "media", api, api.media )
ecs.init(api)
api.ext.exec('init')
api.ext.exec('init', api)
-- load urls passed on the cli
foreach( arg, function(k,uri)

View file

@ -28,9 +28,30 @@ local ecs = api.ecs
ecs.filterDraw = ecs.requireAll('drawthread')
ecs.filterUpdate = ecs.requireAll('updatethread')
-- sort drawcalls of extensions
api.ext.lovr = {
drawcalls = {},
enabled = true,
name = "lovr",
index = api.ext.max-1,
init = function()
api.ext.lovr.sortDrawCalls()
end,
sortDrawCalls = function()
local drawcalls = api.ext.lovr.drawcalls
foreach( api.ext, function(k,v)
if type(v) == 'table' and v.draw ~= nil then
table.insert(drawcalls, v)
end
end)
table.sort( drawcalls, function(a,b) return a.index < b.index end)
end
}
-- ecs systems
local interactionsUpdater
local interactions
--- @type Texture
local envTex
@ -40,8 +61,7 @@ local mainWindow
function lovr.load()
if launch.mode == "desktop" then
backend.mouse = require "lib.lovr-mouse"
--backend.mouse = require "lib.lovr-mouse"
lovr.system.setKeyRepeat(true)
end
@ -50,7 +70,10 @@ function lovr.load()
api.ext.exec("load")
if iui.idiom == "vr" then
lovr.headset.setPassthrough("opaque")
local modes = lovr.headset.getPassthroughModes()
local mode = "opaque"
if modes.blend then mode = "blend" end
lovr.headset.setPassthrough( mode )
mainWindow = backend.worldWindow.new()
api.mainWindow = mainWindow
@ -66,7 +89,7 @@ function lovr.update(dt)
-- In desktop mode, we use IUI's standard window API to fill the screen.
iui.beginWindow(lovr.system.getWindowDimensions())
api.ext.exec("update", dt)
api.ext.exec("update", dt, interactions.event )
iui.endWindow()
@ -76,7 +99,7 @@ function lovr.update(dt)
if mainWindow:beginFrame() then
iui.beginWindow(mainWindow.w, mainWindow.h)
api.ext.exec("update", dt)
api.ext.exec("update", dt, interactions.event )
iui.endWindow()
@ -88,6 +111,10 @@ function lovr.update(dt)
end
function lovr.draw(pass)
--api.player.matrix:setPosition( api.player.x, api.player.y, api.player.z )
--api.player.matrix.setOrientation( api.player.x, api.player.y, api.player.z )
pass:transform(mat4(api.player.matrix):invert())
if iui.idiom == "desktop" then
backend.graphics.pass = pass
@ -97,27 +124,28 @@ function lovr.draw(pass)
pass:setClear(0.2, 0.2, 0.2)
end
api.ext.exec("draw",pass)
ecs.update( api.world, pass, ecs.filterDraw )
-- call draw() function of extensions (sorted by .index)
foreach( api.ext.lovr.drawcalls, function(k,ext) ext.draw(pass) end)
-- Dot
if interactionsUpdater.selectedBox then
pass:setColor(0, 0, 1)
pass:sphere( interactionsUpdater.hitpoint, .01)
end
-- Laser pointers
local hands = {"left","right"}
foreach( hands, function(k,side)
local hand = vec3(lovr.headset.getPosition('hand/' .. side .. '/point'))
local direction = quat(lovr.headset.getOrientation('hand/' .. side .. '/point')):direction()
pass:setColor(1, 1, 1)
pass:line(hand, interactionsUpdater.selectedBox and interactionsUpdater.hitpoint or (hand + direction * 50))
end)
-- -- Dot
--if interactions.selectedBox then
-- pass:setColor(0, 0, 1)
-- pass:sphere( interactions.hitpoint, .01)
--end
--
---- Laser pointers
--local hands = {"left","right"}
--foreach( hands, function(k,side)
-- local hand = vec3(lovr.headset.getPosition('hand/' .. side .. '/point'))
-- local direction = quat(lovr.headset.getOrientation('hand/' .. side .. '/point')):direction()
-- pass:setColor(1, 1, 1)
-- pass:line(hand, interactions.selectedBox and interactions.hitpoint or (hand + direction * 50))
--end)
if api.mainWindow then
api.mainWindow:draw(pass)
end
--if api.mainWindow then
-- api.mainWindow:draw(pass)
--end
return false
end
@ -127,59 +155,62 @@ function lovr.recenter()
end
end
if launch.mode == "desktop" then
function lovr.mousemoved(x, y, dx, dy)
backend.mousemoved(x, y, dx, dy)
interactionsUpdater.mouse.moved = {x=x, y=y}
interactions.pointer.moved = {x=x, y=y}
end
function lovr.mousepressed(x, y, button)
backend.mousepressed(x, y, button)
interactionsUpdater.mouse.pressed = {x=x, y=y, button=button}
interactions.pointer.pressed = {x=x, y=y, button=button}
end
function lovr.mousereleased(x, y, button)
backend.mousereleased(x, y, button)
interactionsUpdater.mouse.released = {x=x, y=y, button=button}
print("mouserelease")
interactions.pointer.released = {x=x, y=y, button=button}
end
function lovr.wheelmoved(x, y)
backend.wheelmoved(x, y)
interactionsUpdater.mouse.wheel = {x=x, y=y}
interactions.pointer.wheel = {x=x, y=y}
print_r(interactions.pointer.wheel)
end
function lovr.keypressed(key, scancode, isRepeat)
backend.keypressed(key, scancode, isRepeat)
interactionsUpdater.key.pressed = {key=key,scancode=scancode,isRepeat=isRepeat}
interactions.key.pressed = {key=key,scancode=scancode,isRepeat=isRepeat}
end
function lovr.keyreleased(key, scancode)
backend.keyreleased(key, scancode)
interactionsUpdater.key.released = {key=key,scancode=scancode}
interactions.key.released = {key=key,scancode=scancode}
end
function lovr.textinput(text)
backend.textinput(text)
interactionsUpdater.input = text
end
interactions.input = text
end
function initInteractions(ecs)
ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0)
interactionsUpdater = ecs.processingSystem()
interactionsUpdater.updatethread = true
interactionsUpdater.filter = function(obj) return true end -- always run
interactions = ecs.processingSystem()
interactions.updatethread = true
interactions.filter = function(obj) return true end -- always run
-- collider vars
interactionsUpdater.mouse = { released = false, pressed = false, moved = false, wheel = false}
interactionsUpdater.key = { released = false, pressed = false }
interactionsUpdater.input = ""
interactionsUpdater.hitpoint = lovr.math.newVec3()
interactionsUpdater.selectedBox = nil
interactionsUpdater.clear = function(o) foreach( o, function(k,v) o[k] = false end ) end
interactions.pointer = { released = false, pressed = false, moved = false, hover = false, wheel = false}
interactions.key = { released = false, pressed = false }
interactions.input = ""
interactions.hitpoint = vector()
interactions.selectedBox = nil
interactions.clearTable = function(o) foreach( o, function(k,v) o[k] = false end ) end
interactions.clear = function()
interactions.clearTable( interactions.pointer )
interactions.clearTable( interactions.key )
interactions.input = false
end
function interactionsUpdater:process(obj, dt)
function interactions:process(obj, dt)
ecs.worldPhysics:update(dt)
local castsrc = 'hand/left/point'
@ -189,30 +220,43 @@ function initInteractions(ecs)
local direction = quat(lovr.headset.getOrientation(castsrc)):direction()
if direction ~= nil then
local dx, dy, dz = direction:mul(50):unpack()
local dx, dy, dz = vector( direction * 50 ):unpack()
local collider, shape, x, y, z = ecs.worldPhysics:raycast(ox, oy, oz, ox + dx, oy + dy, oz + dz)
if collider then
interactions.pointer.hover = true
local node = collider:getUserData()
interactions.selectedBox = collider
interactions.hitpoint.x = x
interactions.hitpoint.y = y
interactions.hitpoint.z = z
for i, hand in ipairs(lovr.headset.getHands()) do
if lovr.headset.isDown(hand, 'trigger') then
print("buttondown")
interactions.pointer.pressed = true
end
if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then
print("cast: mousedown!")
local node = collider:getUserData()
interactionsUpdater.selectedBox = collider
interactionsUpdater.hitpoint:set(x, y, z)
if node.onclick ~= nil then node.onclick(node,collider) end
if lovr.headset.wasReleased(hand, 'trigger') then
interactions.pointer.released = true
end
end
if interactions.pointer.released and node.onclick ~= nil then
node.onclick(node,collider)
end
end
end
-- backup & reset stuff
interactions.event = {
input = util.clone(interactions.input),
pointer = util.clone(interactions.pointer),
key = util.clone(interactions.key),
hitpoint = { x = interactions.hitpoint.x, y = interactions.hitpoint.y, z = interactions.z },
--head = { x = ox, y = oy, z = oz, direction = direction},
collider = collider
}
-- clear interactions
interactions.clear()
end
-- reset stuff
interactionsUpdater.clear( interactionsUpdater.mouse )
interactionsUpdater.clear( interactionsUpdater.key )
interactionsUpdater.input = false
end
ecs.addSystem( api.world, interactionsUpdater )
ecs.addSystem( api.world, interactions )
end
function initCleanups(ecs)

View file

@ -15,10 +15,26 @@ util.loaddir = function( path, api, target)
if api.filesystem.isFile(ext) then
print("[i] loading '" .. v .. "'")
target[ v ] = api.filesystem.load( ext )(api)
if target[ v ].index == nil then target[v].index = api.ext.max / 2 end
end
end)
end
util.clone = function(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[util.clone(orig_key)] = util.clone(orig_value)
end
setmetatable(copy, util.clone(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
util.cmdExist = function(cmd)
if api.runtime.os == 'unixy' then cmd = cmd .. " 1>/dev/null 2>/dev/null" end
local stdout, retstr, retcode = os.execute(cmd)
@ -198,9 +214,9 @@ end
-- LUA SYNTAX SUGAR (from here on )
--
function foreach(table, f)
if table ~= nil then
for k, v in pairs(table) do f(k, v) end
function foreach(t, f, sort)
if t ~= nil then
for k, v in pairs(t) do f(k, v) end
end
end