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() api.world.commit()
end 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 return ecs

View file

@ -5,49 +5,82 @@ local gctl
gctl = { gctl = {
name = "gctl", name = "gazecontrol",
enabled = true, enabled = (lovr ~= nil),
index = api.ext.max * 0.9, -- important: render last! (renderOrder)
texture = {}, texture = {},
material = nil, material = nil,
dtmax = 1, dtmax = 1,
dt = 0, 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() init = function()
--lovr.mouse.setRelativeMode(true) --lovr.mouse.setRelativeMode(true)
gctl.initVisor() gctl.initVisor()
gctl.extendECS() gctl.extendECS()
api.player.matrix = lovr.math.newMat4()
end, end,
load = function()
gctl.texture.idle = lovr.graphics.newTexture("ext/gazecontrol/reticle_idle.png")
end,
initVisor = function() initVisor = function()
-- add visor object local visor = gctl.visor
gctl.visor = { visor.textures.idle = lovr.graphics.newTexture("ext/gazecontrol/reticle_idle.png")
rotate = { x = 0, y = 0, z = 0}, visor.textures.hover = lovr.graphics.newTexture("ext/gazecontrol/reticle_hover.png")
scale = 0.1, visor.texture = visor.textures.idle
texture = gctl.texture.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, end,
draw = function(pass,dt) draw = function(pass,dt)
-- *TODO* render this last (as it needs to be rendered after skybox & floor) -- *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 dirx, diry, dirz = lovr.headset.getDirection('head')
local distance = 1 local distance = 1
local x = posx + distance * dirx local x = pos.x + distance * dirx
local y = posy + distance * diry local y = pos.y + distance * diry
local z = posz + distance * dirz local z = pos.z + distance * dirz
local angle, ax, ay, az = lovr.headset.getOrientation(device) local angle, ax, ay, az = lovr.headset.getOrientation(device)
local rot = gctl.visor.rotate local visor = gctl.visor
local scale = gctl.visor.scale local rot = visor.rotate
pass:setMaterial(gctl.texture.idle) local scale = visor.scale
pass:setShader() --'unlit') if scale < visor.scaleTo then
pass:setBlendMode('alpha') scale = scale + visor.scaleSpeed
pass:setColor(1, 1, 1) visor.scale = scale
end
pass:setMaterial(visor.texture)
pass:setColor(1, 1, 1, 1)
pass:setCullMode('back') 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) pass:plane(x, y, z, scale, scale, angle, ax, ay, az)
end, 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,11 +9,9 @@ return {
--api.ext.skeleton.extendECS() --api.ext.skeleton.extendECS()
end, end,
update = function(dt) end, update = function(dt,input) end,
load = function() end,
load = function() end, draw = function(pass,dt) end,
draw = function(pass,dt) end,
-- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc -- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc
extendECS = function() extendECS = function()

View file

@ -5,6 +5,7 @@ local shader
return { return {
name = "startupscene", name = "startupscene",
enabled = (lovr ~= nil), enabled = (lovr ~= nil),
index = api.ext.max * 0.1,
init = function() end, init = function() end,
@ -51,13 +52,15 @@ return {
pass:setColor(1, 1, 1) pass:setColor(1, 1, 1)
pass:setMaterial(envTex) pass:setMaterial(envTex)
pass:setDepthWrite(false)
pass:draw(envTex, 0, 0, 0, 512, math.pi * 0.5, 1, 0, 0) pass:draw(envTex, 0, 0, 0, 512, math.pi * 0.5, 1, 0, 0)
-- floor -- floor
--pass:setMaterial() pass:setMaterial()
pass:setDepthWrite(true)
pass:setShader() pass:setShader()
pass:setCullMode('front') --pass:setCullMode('front')
pass:setBlendMode('alpha') --pass:setBlendMode('alpha')
pass:setMaterial() pass:setMaterial()
pass:setColor(0.8,0.8,0.8,1) 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) 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, ecs = ecs,
world = ecs.world(), world = ecs.world(),
protocol = {}, 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 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 exec = function(...) util.exec(api.ext,...) end -- util function to call func on each extension
}, },
i = 1 i = 1
@ -39,13 +42,10 @@ api = {
api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime 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") require( runtime.path .. "/main")
util.loaddir( "ext", api, api.ext ) api.ext.exec('init', api)
util.loaddir( "media", api, api.media )
ecs.init(api)
api.ext.exec('init')
-- load urls passed on the cli -- load urls passed on the cli
foreach( arg, function(k,uri) foreach( arg, function(k,uri)

View file

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

View file

@ -15,10 +15,26 @@ util.loaddir = function( path, api, target)
if api.filesystem.isFile(ext) then if api.filesystem.isFile(ext) then
print("[i] loading '" .. v .. "'") print("[i] loading '" .. v .. "'")
target[ v ] = api.filesystem.load( ext )(api) target[ v ] = api.filesystem.load( ext )(api)
if target[ v ].index == nil then target[v].index = api.ext.max / 2 end
end end
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) util.cmdExist = function(cmd)
if api.runtime.os == 'unixy' then cmd = cmd .. " 1>/dev/null 2>/dev/null" end if api.runtime.os == 'unixy' then cmd = cmd .. " 1>/dev/null 2>/dev/null" end
local stdout, retstr, retcode = os.execute(cmd) local stdout, retstr, retcode = os.execute(cmd)
@ -27,7 +43,7 @@ end
util.exec = function(obj, fn,a,b,c,d,e,f) util.exec = function(obj, fn,a,b,c,d,e,f)
foreach( obj, foreach( obj,
when('enabled',true, util.call(fn,a,b,c,d,e,f) ) when('enabled', true, util.call(fn,a,b,c,d,e,f) )
) )
end end
@ -198,9 +214,9 @@ end
-- LUA SYNTAX SUGAR (from here on ) -- LUA SYNTAX SUGAR (from here on )
-- --
function foreach(table, f) function foreach(t, f, sort)
if table ~= nil then if t ~= nil then
for k, v in pairs(table) do f(k, v) end for k, v in pairs(t) do f(k, v) end
end end
end end