mediafragments work for ogg

This commit is contained in:
Leon van Kammen 2026-06-18 15:13:30 +02:00
parent 97b14f7910
commit cd480daccc
21 changed files with 741 additions and 113 deletions

View file

@ -7,9 +7,10 @@ Download/run the binaries for your platform in the releases section.<br>
Developers can run it via [Lua](https://lua.org), [LÖVR](https://lovr.org) or [LÖVE2D](https://love2d.org) Developers can run it via [Lua](https://lua.org), [LÖVR](https://lovr.org) or [LÖVE2D](https://love2d.org)
``` ```
$ love xurfer <url> [..] # if you have LÖVE2D installed (>v12) $ cd xurfer
$ lovr xurfer <url> [..] # if you have LÖVR installed $ love . <url_or_file> [..] # if you have LÖVE2D installed (>v12)
$ lua xurfer/main.lua <url> [..] # if you have lua installed $ lovr . <url_or_file> [..] # if you have LÖVR installed
$ lua main.lua <url_or_file> [..] # if you have lua installed
``` ```
**Example**: lovr xurfer <a href="https://snips.sh/f/_U5-XctEVE?r=1">https://snips.sh/f/_U5-XctEVE?r=1</a> **Example**: lovr xurfer <a href="https://snips.sh/f/_U5-XctEVE?r=1">https://snips.sh/f/_U5-XctEVE?r=1</a>

View file

@ -11,5 +11,4 @@ package.path = package.path .. ';' ..
arg[0] .. '/' .. runtime .. '/?/init.lua;' .. arg[0] .. '/' .. runtime .. '/?/init.lua;' ..
arg[0] .. '/lib/?.lua' arg[0] .. '/lib/?.lua'
require( runtime .. "/conf") require( runtime .. "/conf")

View file

@ -7,8 +7,16 @@ ecs.init = function()
baseEntify.updatethread = true baseEntify.updatethread = true
function baseEntify:onAdd(obj) function baseEntify:onAdd(obj)
obj.commit = api.util.commit( api.world, obj, api ) obj.commit = api.util.commit( api.world, obj, api )
api.world.commit = api.util.commit( api.world, false, api)
end end
ecs.addSystem( api.world, baseEntify ) ecs.addSystem( api.world, baseEntify )
end end
ecs.clear = function()
api.ext.exec("onClear")
api.ecs.clearEntities( api.world )
api.ecs.update( api.world )
api.world.commit()
end
return ecs return ecs

View file

@ -6,16 +6,26 @@ return {
enabled = true, enabled = true,
onURI = function(obj) onURI = function(obj)
if( obj.URL ~= nil and obj.URLResponse ~= nil and if( obj.URL ~= nil and (obj.URLResponse ~= nil or obj.URL.protocol == 'file') and
(obj.URL.extension == 'GLB' or (obj.URL.extension == 'GLB' or
obj.URL.extension == 'GLTF' or obj.URL.extension == 'GLTF' or
obj.URL.extension == 'OBJ')) then obj.URL.extension == 'OBJ')) then
if obj.URLResponse.ok then if obj.URLResponse.ok or obj.URL.protocol == 'file' then
obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) ) obj.root = false
obj.root = (obj.URI.target == '_top') if obj.URI.target == '_top' then
if obj.x == nil then obj.x = 0 end obj.root = true
if obj.y == nil then obj.y = 0 end api.ext.URI.current = obj.URL -- set current url
if obj.z == nil then obj.z = 0 end end
obj.x = obj.x or 0
obj.y = obj.y or 0
obj.z = obj.z or 0
obj.scale = obj.scale or 1
if obj.URL.protocol == 'file' then
obj.model = api.graphics.newModel( obj.URL.string )
else
obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) )
end
api.ecs.add( api.world, obj )
obj.commit('on3DFile') -- notify systems obj.commit('on3DFile') -- notify systems
else else
print("[3DFile] error: could not load " .. obj.URL.string ) print("[3DFile] error: could not load " .. obj.URL.string )

View file

@ -0,0 +1,34 @@
local api = ...
local ecs = api.ecs
return {
name = "AudioFile",
enabled = true,
onURI = function(obj)
if( obj.URL ~= nil and (obj.URLResponse ~= nil or obj.URL.protocol == 'file') and
(obj.URL.extension == 'OGG' or
obj.URL.extension == 'WAV' or
obj.URL.extension == 'MP3')) then
if obj.URLResponse.ok or obj.URL.protocol == 'file' then
obj.x = obj.x or 0
obj.y = obj.y or 0
obj.z = obj.z or 0
local opts = {
spatial = obj.x == 0 and obj.y == 0 and obj.z == 0
}
if obj.URL.protocol == 'file' then
obj.audio = api.audio.newSource( obj.URL.file, opts)
else
obj.audio = api.audio.newSource( api.data.newBlob( obj.URLResponse.data), opts )
end
api.ecs.add( api.world, obj )
obj.commit('onAudioFile') -- notify systems
obj.audio:play()
else
print("[AudioFile] error: could not load " .. obj.URL.string )
end
end
end
}

View file

@ -4,6 +4,7 @@ local ecs = api.ecs
return { return {
name = "URI", name = "URI",
enabled = true, enabled = true,
current = {}, -- will hold the most recent (_top) URL object
init = function() init = function()
local urlListener local urlListener
@ -19,23 +20,27 @@ return {
to = function(URI,refererer, obj) to = function(URI,refererer, obj)
obj = obj or {} obj = obj or {}
local msg = "[i] loading" print("[i] loading URL: " .. URI.url)
print("[i] loading " .. URI.url)
obj.URL = api.url.parse(URI.url) obj.URL = api.url.parse(URI.url)
obj.URLResponse = {} obj.URLResponse = {}
local protocol = "file"
foreach( api.protocol, function(name, p) foreach( api.protocol, function(name, p)
if obj.URL.protocol:match("^"..name) then if obj.URL.protocol:match("^"..name) then
protocol = p protocol = p
end end
end) end)
if protocol then if protocol then
if obj.URL.protocol == 'file' then
return obj.commit('onURI')
end
local status, data, headers = protocol.request(URI.url) local status, data, headers = protocol.request(URI.url)
obj.URLResponse = { obj.URLResponse = {
ok = (status >= 200 and status < 300), ok = (status ~= nil and status >= 200 and status < 300),
status = status, status = status,
data = data, data = data,
headers = headers headers = headers
} }
api.ecs.add( api.world, obj )
obj.commit('onURI') obj.commit('onURI')
end end
end end

View file

@ -4,7 +4,7 @@ local sample
return { return {
name = "kitchensink", name = "kitchensink",
enabled = (lover ~= nil), enabled = false, --(lovr ~= nil),
init = function() init = function()
sample = require "sample" sample = require "sample"

View file

@ -1,13 +1,10 @@
local api = ... local api = ...
local envTex local envTex
local skybox
local environmentMap
local sphericalHarmonics
local shader local shader
return { return {
name = "startupscene", name = "startupscene",
enabled = true, enabled = (lovr ~= nil),
init = function() end, init = function() end,
@ -19,8 +16,8 @@ return {
load = function() load = function()
--if api.iui.idiom == "vr" then --if api.iui.idiom == "vr" then
envTex = lovr.graphics.newTexture("lovr/assets/img/env.png", {}) envTex = api.graphics.newTexture("lovr/assets/img/env.png", {})
skybox = lovr.graphics.newTexture({ api.world.skybox = api.graphics.newTexture({
px = 'ext/startupscene/skybox/Dayright.jpg', -- 'px.png', px = 'ext/startupscene/skybox/Dayright.jpg', -- 'px.png',
nx = 'ext/startupscene/skybox/Dayleft.jpg', -- 'nx.png', nx = 'ext/startupscene/skybox/Dayleft.jpg', -- 'nx.png',
py = 'ext/startupscene/skybox/Daytop.jpg', -- 'py.png', py = 'ext/startupscene/skybox/Daytop.jpg', -- 'py.png',
@ -28,9 +25,9 @@ return {
pz = 'ext/startupscene/skybox/Dayfront.jpg', -- 'pz.png', pz = 'ext/startupscene/skybox/Dayfront.jpg', -- 'pz.png',
nz = 'ext/startupscene/skybox/Dayback.jpg' -- 'nz.png' nz = 'ext/startupscene/skybox/Dayback.jpg' -- 'nz.png'
}) })
environmentMap = lovr.graphics.newTexture('ext/startupscene/skybox/ibl.ktx') api.world.environmentMap = lovr.graphics.newTexture('ext/startupscene/skybox/ibl.ktx')
sphericalHarmonics = lovr.graphics.newBuffer({ 'vec3', layout = 'std140' }, { api.world.sphericalHarmonics = lovr.graphics.newBuffer({ 'vec3', layout = 'std140' }, {
{ 0.611764907836914, 0.599504590034485, 0.479980736970901 }, { 0.611764907836914, 0.599504590034485, 0.479980736970901 },
{ 0.659514904022217, 0.665349841117859, 0.567680120468140 }, { 0.659514904022217, 0.665349841117859, 0.567680120468140 },
{ 0.451633930206299, 0.450751245021820, 0.355226665735245 }, { 0.451633930206299, 0.450751245021820, 0.355226665735245 },
@ -41,36 +38,6 @@ return {
{ -0.179465517401695, -0.181243389844894, -0.141314014792442 }, { -0.179465517401695, -0.181243389844894, -0.141314014792442 },
{ -0.144527092576027, -0.143508568406105, -0.122757166624069 } { -0.144527092576027, -0.143508568406105, -0.122757166624069 }
}) })
shader = lovr.graphics.newShader([[
vec4 lovrmain() {
return DefaultPosition;
}
]], [[
uniform textureCube cubemap;
uniform sphericalHarmonics { vec3 sh[9]; };
vec4 lovrmain() {
Surface surface;
initSurface(surface);
vec3 color = vec3(0);
vec3 lightDirection = vec3(-1, -1, -1);
vec4 lightColorAndBrightness = vec4(1, 1, 1, 3);
float visibility = 1.;
color += getLighting(surface, lightDirection, lightColorAndBrightness, visibility);
color += getIndirectLighting(surface, cubemap, sh);
return vec4(color, 1);
}
]], {
flags = {
glow = true,
normalMap = true,
vertexTangents = false, -- DamagedHelmet doesn't have vertex tangents
tonemap = true
}
})
--end
end, end,
draw = function(pass) draw = function(pass)
@ -78,7 +45,7 @@ return {
-- skybox -- skybox
pass:setCullMode('back') pass:setCullMode('back')
pass:setBlendMode() pass:setBlendMode()
pass:skybox(skybox) pass:skybox(api.world.skybox)
pass:setColor(1, 1, 1) pass:setColor(1, 1, 1)
pass:setMaterial(envTex) pass:setMaterial(envTex)
@ -93,10 +60,10 @@ return {
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)
pass:setColor( 0, 0, 0, 0.75 ) pass:setColor( 0, 0, 0, 0.75 )
pass:plane(0, 0.01, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "line", 100, 100) pass:plane(0, 0.01, 0, 500, 500, math.pi * 0.5, 1, 0, 0, "line", 100, 100)
pass:setShader(shader) pass:setShader( lovr.shader.pbr )
pass:send('cubemap', environmentMap) pass:send('cubemap', api.world.environmentMap)
pass:send('sphericalHarmonics', sphericalHarmonics) pass:send('sphericalHarmonics', api.world.sphericalHarmonics)
end end
end end

33
xurfer/ext/tween/main.lua Normal file
View file

@ -0,0 +1,33 @@
local api = ...
return {
enabled = false,
init = function()
local tweenUpdater = api.ecs.processingSystem({
updatethread = true,
filter = api.ecs.requireAll('tween'),
nocache = true,
process = function( self, obj, dt)
print("JAAA")
foreach( obj.tween, function(k,t)
print("TWEENING!")
if t:update(dt) == true then
obj.tween[k] = nil -- delete tween
if count(obj.tween) == 0 then
obj.tween = nil
print("deleted tweens")
end
end
end)
end
})
api.ecs.addSystem( api.world, tweenUpdater )
end
---- visualize click
--onClick
--obj.scale = 0.66
--obj.tween = obj.tween or {}
--obj.tween['scale'] = tween.new(0.5, obj, {scale = 1})
}

367
xurfer/ext/tween/tween.lua Normal file
View file

@ -0,0 +1,367 @@
local tween = {
_VERSION = 'tween 2.1.1',
_DESCRIPTION = 'tweening for lua',
_URL = 'https://github.com/kikito/tween.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-- easing
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
-- For all easing functions:
-- t = time == how much time has to pass for the tweening to complete
-- b = begin == starting property value
-- c = change == ending - beginning
-- d = duration == running time. How much time has passed *right now*
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
-- linear
local function linear(t, b, c, d) return c * t / d + b end
-- quad
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 2) + b end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
local function outInQuad(t, b, c, d)
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
-- cubic
local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * t * t * t + b end
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
local function outInCubic(t, b, c, d)
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
-- quart
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 4) + b end
return -c / 2 * (pow(t - 2, 4) - 2) + b
end
local function outInQuart(t, b, c, d)
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
-- quint
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 5) + b end
return c / 2 * (pow(t - 2, 5) + 2) + b
end
local function outInQuint(t, b, c, d)
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
-- sine
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
local function outInSine(t, b, c, d)
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
return inSine((t * 2) -d, b + c / 2, c / 2, d)
end
-- expo
local function inExpo(t, b, c, d)
if t == 0 then return b end
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
local function outExpo(t, b, c, d)
if t == d then return b + c end
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
end
local function outInExpo(t, b, c, d)
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
-- circ
local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
local function outInCirc(t, b, c, d)
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
-- elastic
local function calculatePAS(p,a,c,d)
p, a = p or d * 0.3, a or 0
if a < abs(c) then return p, c, p / 4 end -- p, a, s
return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
end
local function inElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
local function outElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
local function inOutElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
end
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
-- back
local function inBack(t, b, c, d, s)
s = s or 1.70158
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
s = s or 1.70158
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
s = (s or 1.70158) * 1.525
t = t / d * 2
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
-- bounce
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
if t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
end
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
local function inOutBounce(t, b, c, d)
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
local function outInBounce(t, b, c, d)
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
tween.easing = {
linear = linear,
inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
}
-- private stuff
local function copyTables(destination, keysTable, valuesTable)
valuesTable = valuesTable or keysTable
local mt = getmetatable(keysTable)
if mt and getmetatable(destination) == nil then
setmetatable(destination, mt)
end
for k,v in pairs(keysTable) do
if type(v) == 'table' then
destination[k] = copyTables({}, v, valuesTable[k])
else
destination[k] = valuesTable[k]
end
end
return destination
end
local function checkSubjectAndTargetRecursively(subject, target, path)
path = path or {}
local targetType, newPath
for k,targetValue in pairs(target) do
targetType, newPath = type(targetValue), copyTables({}, path)
table.insert(newPath, tostring(k))
if targetType == 'number' then
assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
elseif targetType == 'table' then
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
else
assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
end
end
end
local function checkNewParams(duration, subject, target, easing)
assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
local tsubject = type(subject)
assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject))
assert(type(target)== 'table', "target must be a table. Was " .. tostring(target))
assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing))
checkSubjectAndTargetRecursively(subject, target)
end
local function getEasingFunction(easing)
easing = easing or "linear"
if type(easing) == 'string' then
local name = easing
easing = tween.easing[name]
if type(easing) ~= 'function' then
error("The easing function name '" .. name .. "' is invalid")
end
end
return easing
end
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
local t,b,c,d
for k,v in pairs(target) do
if type(v) == 'table' then
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
else
t,b,c,d = clock, initial[k], v - initial[k], duration
subject[k] = easing(t,b,c,d)
end
end
end
-- Tween methods
local Tween = {}
local Tween_mt = {__index = Tween}
function Tween:set(clock)
assert(type(clock) == 'number', "clock must be a positive number or 0")
self.initial = self.initial or copyTables({}, self.target, self.subject)
self.clock = clock
if self.clock <= 0 then
self.clock = 0
copyTables(self.subject, self.initial)
elseif self.clock >= self.duration then -- the tween has expired
self.clock = self.duration
copyTables(self.subject, self.target)
else
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
end
return self.clock >= self.duration
end
function Tween:reset()
return self:set(0)
end
function Tween:update(dt)
assert(type(dt) == 'number', "dt must be a number")
return self:set(self.clock + dt)
end
-- Public interface
function tween.new(duration, subject, target, easing)
easing = getEasingFunction(easing)
checkNewParams(duration, subject, target, easing)
return setmetatable({
duration = duration,
subject = subject,
target = target,
easing = easing,
clock = 0
}, Tween_mt)
end
return tween

View file

@ -0,0 +1,16 @@
local api = ...
local xrf = require("ext/xrfragments/lovr-xrf")
local url = require("url")
return {
load = function( href, obj, refererURL, cb )
if xrf.isExternalHref( href ) and not xrf.isImport( href ) then
if type(refererURL) ~= 'table' then refererURL = url.parse(refererURL) end
local URL = url.getAbsolute( href, refererURL.string )
cb( URL.string, obj)
return true
end
return false
end
}

View file

@ -0,0 +1,15 @@
local xrf = require("ext/xrfragments/lovr-xrf")
local url = require("url")
return {
import = function( href, obj, refererURL, cb ) -- imports object
if xrf.isImport( href ) then
if type(refererURL) ~= 'table' then refererURL = url.parse(refererURL) end
local URL = url.getAbsolute( href, refererURL )
cb( URL.string, obj )
return true
end
return false
end
}

View file

@ -25,6 +25,7 @@
-- or implied, of -- or implied, of
local json = require("json") local json = require("json")
local url = require("url")
local xrf = {} local xrf = {}
xrf.traverseNodesContaining = function(key,obj,cb) xrf.traverseNodesContaining = function(key,obj,cb)
@ -36,14 +37,17 @@ xrf.traverseNodesContaining = function(key,obj,cb)
for k, tkey in pairs(keys) do for k, tkey in pairs(keys) do
local i = 1 local i = 1
local tree = metadata[tkey] local tree = metadata[tkey]
local foundhrefs = false
xrf.traverse( tree, function(node) xrf.traverse( tree, function(node)
if node['extras'] ~= nil then if node['extras'] ~= nil then
if node['extras'][key] ~= nil then if node['extras'][key] ~= nil then
foundhrefs = true
cb(node,obj, i, metadata) cb(node,obj, i, metadata)
end end
end end
i = i + 1 i = i + 1
end) end)
if foundhrefs then break end -- blender puts duplicate (undo?) data in 'meshes' sometimes?
end end
end end
@ -86,7 +90,6 @@ xrf.makeClickable = function( physicsWorld, cb)
local model = obj['model'] local model = obj['model']
local node = node_or_mesh local node = node_or_mesh
if node['mesh'] == nil then node = xrf.getNodeFromMesh(node_or_mesh, metadata, i) end if node['mesh'] == nil then node = xrf.getNodeFromMesh(node_or_mesh, metadata, i) end
print("making node " .. node['name'] .. " clickable")
local meshindex = 1 + node['mesh'] local meshindex = 1 + node['mesh']
local mesh = model:getMesh( meshindex ) local mesh = model:getMesh( meshindex )
local minx, maxx, miny, maxy, minz, maxz = mesh:getBoundingBox() local minx, maxx, miny, maxy, minz, maxz = mesh:getBoundingBox()
@ -103,6 +106,7 @@ xrf.makeClickable = function( physicsWorld, cb)
scale[2] * (h + 0.01), -- in case of scale[2] * (h + 0.01), -- in case of
scale[3] * (d + 0.01) -- planes e.g. scale[3] * (d + 0.01) -- planes e.g.
) )
print("making node " .. node['name'] .. " clickable: " .. node.extras.href )
collider:setUserData( {name = node['name'], node = node, onclick = cb }) collider:setUserData( {name = node['name'], node = node, onclick = cb })
end end
end end
@ -122,4 +126,18 @@ xrf.traverse = function(arr, cb, key)
end end
end end
xrf.isImport = function(href)
local URL = href
if type(URL) ~= 'table' then URL = url.parse(href) end
return URL.string:find("#!")
end
xrf.isExternalHref = function(href)
local URL = href
if type(URL) ~= 'table' then URL = url.parse(href) end
return (URL.protocol == 'file') and
(URL.string:sub(1,1) ~= '#') and
not xrf.isImport(URL)
end
return xrf return xrf

View file

@ -1,9 +1,12 @@
local api = ... local api = ...
local url = require('url')
local tween = require('tween')
local ecs = api.ecs local ecs = api.ecs
local xrf = require("ext/xrfragments/lovr-xrf") local xrf = require("ext/xrfragments/lovr-xrf")
local xrfsystem xrf.level2 = api.util.addModuleHooks( require("ext/xrfragments/level2") )
xrf.level4 = api.util.addModuleHooks( require("ext/xrfragments/level4") )
return { xrfimpl = {
enabled = true, enabled = true,
init = function() end, init = function() end,
@ -13,12 +16,41 @@ return {
xrf.traverseNodesContaining('href', obj, xrf.traverseNodesContaining('href', obj,
xrf.makeClickable( api.ecs.worldPhysics, xrf.makeClickable( api.ecs.worldPhysics,
function(obj, collider) function(obj, collider)
print("\nhref was clicked: " .. obj.node.extras.href) local href = obj.node.extras.href
print_r(obj) trace("\n[xrf] href was clicked: " .. href)
local ok = xrf.level2.load( href, obj, api.ext.URI.current, xrfimpl.onLoad)
or
xrf.level4.import( href, obj, api.ext.URI.current, xrfimpl.onImport)
end end
) )
) )
end end
end,
onAudioFile = function(obj)
-- see https://xrfragment.org/#%F0%9F%93%9C%20level3%3A%20Media%20Fragments
if obj.audio ~= nil and obj.URL.fragment.t ~= nil then
trace("[xrf.level3] seeking to " .. obj.URL.fragment.t .. " seconds in audio")
obj.audio:seek( tonumber(obj.URL.fragment.t) )
obj.audio:setLooping( obj.URL.fragment.loop or false )
end
end,
onLoad = function(absoluteHref,obj)
trace("[xrf.level2] explicit (external) href hyperlink found")
api.ecs.clear()
api.ecs.add( api.world, {
URI = { url = absoluteHref, method = 'GET', target = '_top' }
})
end,
onImport = function(absoluteHref,obj)
trace("[xrf.level4] ! (import) operator found")
api.ecs.add( api.world, { URI = { url = absoluteHref, method = 'GET', target = obj } })
end end
} }
return xrfimpl

View file

@ -57,7 +57,9 @@ local function parseArgs(fragment)
for _, item in ipairs(items) do for _, item in ipairs(items) do
local key_value = split(item, "=") local key_value = split(item, "=")
if #key_value > 1 then if #key_value > 1 then
ARG[key_value[1]] = guess_type(key_value[2]) local k = key_value[1]
ARG[k] = guess_type(key_value[2]) -- xr fragment operators preserved
ARG[k:gsub("[^%w]","") ] = guess_type(key_value[2]) -- alphanumeric only
elseif #key_value == 1 then elseif #key_value == 1 then
ARG[key_value[1]] = "" ARG[key_value[1]] = ""
end end
@ -67,6 +69,7 @@ end
-- Main URL Parser function attached to the module object -- Main URL Parser function attached to the module object
function url.parse(url_string) function url.parse(url_string)
if type(url_string) == 'table' then return url_string end -- already parsed
-- Setup initial URI table structure -- Setup initial URI table structure
local URI = { local URI = {
domain = "", domain = "",
@ -86,6 +89,8 @@ function url.parse(url_string)
if protocol then if protocol then
URI.protocol = protocol:gsub("://", "") URI.protocol = protocol:gsub("://", "")
remaining = remaining:sub(#protocol + 1) remaining = remaining:sub(#protocol + 1)
else
URI.protocol = "file"
end end
local hash = string.match(remaining, "(#.*)$") -- fragment local hash = string.match(remaining, "(#.*)$") -- fragment
if hash then if hash then
@ -140,5 +145,17 @@ function url.parse(url_string)
return URI return URI
end end
url.getAbsolute = function(href,referer)
local URL = url.parse(href)
local URLref = url.parse(referer)
if referer ~= nil and URL.protocol == 'file' and URLref.protocol ~= 'file' then
URL.protocol = URLref.protocol
URL.domain = URLref.domain
URL.string = URLref.protocol .. '://' .. URLref.domain .. '/' .. URL.path .. URL.hash
URL.URN = URL.string:gsub("#.*", "")
end
return URL
end
-- Return the module table so it can be assigned via require() -- Return the module table so it can be assigned via require()
return url return url

View file

@ -656,8 +656,10 @@ function tiny_manageEntities(world)
elseif index then elseif index then
system.modified = true system.modified = true
local tmpEntity = ses[#ses] local tmpEntity = ses[#ses]
ses[index] = tmpEntity if tmpEntity ~= nil then
seis[tmpEntity] = index ses[index] = tmpEntity
seis[tmpEntity] = index
end
seis[entity] = nil seis[entity] = nil
ses[#ses] = nil ses[#ses] = nil
local onRemove = system.onRemove local onRemove = system.onRemove

View file

@ -22,7 +22,6 @@ ecs.filterUpdate = ecs.requireAll('updatethread')
-- ecs systems -- ecs systems
local interactionsUpdater local interactionsUpdater
local renderer
--- @type Texture --- @type Texture
local envTex local envTex
@ -120,7 +119,7 @@ end
if launch.mode == "desktop" then 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)
interactionsUpdater.mouse.moved = {x=x, y=y, button=button} interactionsUpdater.mouse.moved = {x=x, y=y}
end end
function lovr.mousepressed(x, y, button) function lovr.mousepressed(x, y, button)
@ -146,7 +145,7 @@ if launch.mode == "desktop" then
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,isRepeat=isRepeat} interactionsUpdater.key.released = {key=key,scancode=scancode}
end end
function lovr.textinput(text) function lovr.textinput(text)
@ -155,27 +154,6 @@ if launch.mode == "desktop" then
end end
end end
local initECS = function(ecs)
-- lovr.draw => ecs.draw (render-logic thread)
renderer = ecs.processingSystem()
renderer.filter = ecs.requireAll('model')
renderer.drawthread = true
function renderer:process(obj, pass)
if obj.model ~= nil and obj.root ~= nil then
pass:setMaterial()
pass:setCullMode('none')
pass:draw(
obj['model'],
obj['x'],
obj['y'],
obj['z'],
1, 0, 1, 0, 0, 1)
end
end
ecs.addSystem( api.world, renderer )
end
function initInteractions(ecs) function initInteractions(ecs)
ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0) ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0)
@ -198,14 +176,16 @@ function initInteractions(ecs)
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
if lovr.headset.isDown(hand, 'trigger') then for i, hand in ipairs(lovr.headset.getHands()) do
print("buttondown") if lovr.headset.isDown(hand, 'trigger') then
end print("buttondown")
if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then end
local node = collider:getUserData() if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then
interactionsUpdater.selectedBox = collider local node = collider:getUserData()
interactionsUpdater.hitpoint:set(x, y, z) interactionsUpdater.selectedBox = collider
if node.onclick ~= nil then node.onclick(node,collider) end interactionsUpdater.hitpoint:set(x, y, z)
if node.onclick ~= nil then node.onclick(node,collider) end
end
end end
end end
-- reset stuff -- reset stuff
@ -216,5 +196,21 @@ function initInteractions(ecs)
ecs.addSystem( api.world, interactionsUpdater ) ecs.addSystem( api.world, interactionsUpdater )
end end
initECS(api.ecs) function initCleanups(ecs)
local navigateListener = ecs.processingSystem({
updatethread = true,
filter = ecs.requireAll('URI', ecs.rejectAll('URLResponse') ),
onAdd = function(self,obj)
-- clear colliders if needed
if obj.URI.target ~= nil and obj.URI.target == '_top' then
ecs.worldPhysics:destroy()
ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0)
end
end
})
ecs.addSystem( api.world, navigateListener )
end
initInteractions(api.ecs) initInteractions(api.ecs)
initCleanups(api.ecs)
require("render/model").init(api)

View file

@ -0,0 +1,75 @@
local api = ...
local modelRenderer
modelRenderer = {
-- lovr.draw => ecs.draw (render-logic thread)
init = function(api)
local ecs = api.ecs
api.world.shader = { pbr = false }
lovr.shader = lovr.shader or {}
lovr.shader['pbr'] = modelRenderer.initShaderPBR()
renderer = ecs.processingSystem()
renderer.filter = ecs.requireAll('model')
renderer.drawthread = true -- important
function renderer:process(obj, pass)
if obj.model ~= nil and obj.root ~= nil then
pass:setMaterial()
pass:setBlendMode()
pass:setFaceCull('none') -- supposed to be defined per-mesh in GLB
pass:setCullMode('none')
pass:setShader( lovr.shader.pbr )
if api.world.environmentMap ~= nil then
pass:send('cubemap', api.world.environmentMap)
end
if api.world.sphericalHarmonics ~= nil then
pass:send('sphericalHarmonics', api.world.sphericalHarmonics)
end
pass:draw(
obj['model'],
obj.x or 0,
obj.y or 0,
obj.z or 0,
obj.scale or 1,
0, 1, 0, 0, 1)
end
end
ecs.addSystem( api.world, renderer )
end,
initShaderPBR = function()
return lovr.graphics.newShader([[
vec4 lovrmain() {
return DefaultPosition;
}
]], [[
uniform textureCube cubemap;
uniform sphericalHarmonics { vec3 sh[9]; };
vec4 lovrmain() {
Surface surface;
initSurface(surface);
vec3 color = vec3(0);
vec3 lightDirection = vec3(-1, -1, -1);
vec4 lightColorAndBrightness = vec4(1, 1, 1, 3);
float visibility = 1.;
color += getLighting(surface, lightDirection, lightColorAndBrightness, visibility);
color += getIndirectLighting(surface, cubemap, sh);
return vec4(color, 1);
}
]], {
flags = {
glow = true,
normalMap = true,
vertexTangents = false, -- DamagedHelmet doesn't have vertex tangents
tonemap = true
}
})
end
}
return modelRenderer

View file

@ -9,6 +9,14 @@
-- ./lovr xurfer https://snips.sh/f/rHFLg-cewi?r=1' -- cube -- ./lovr xurfer https://snips.sh/f/rHFLg-cewi?r=1' -- cube
-- ./lovr xurfer https://snips.sh/f/_U5-XctEVE?r=1' -- cube, monkey, scene -- ./lovr xurfer https://snips.sh/f/_U5-XctEVE?r=1' -- cube, monkey, scene
local runtime = nil
if lovr ~= nil then
runtime = { path = "lovr", api = lovr }
elseif love ~= nil then
runtime = { path = "love", api = love };
end
if runtime == nil then if runtime == nil then
package.path = package.path .. ';' .. 'xurfer/?.lua' package.path = package.path .. ';' .. 'xurfer/?.lua'
require('conf') require('conf')
@ -32,13 +40,7 @@ api = {
} }
} }
local runtime api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime api
local runtimepath
if lovr ~= nil then runtime = { path = "lovr", api = lovr } end
if love ~= nil then runtime = { path = "love", api = love } end
api = util.merge( api, runtime.api )
require( runtime.path .. "/main") require( runtime.path .. "/main")
@ -49,9 +51,7 @@ 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)
foreach( api.protocol, function(name, p) if uri:find(".") and k ~= 0 and k ~= -1 then
if uri:match("^"..name) then api.ecs.add( api.world, { URI = { url = uri, method = 'GET', target = '_top' } })
api.ecs.add( api.world, { URI = { url = uri, method = 'GET', target = '_top' } }) end
end
end)
end) end)

BIN
xurfer/test.glb Normal file

Binary file not shown.

View file

@ -69,6 +69,10 @@ function util.dump(value, indent, seen)
end end
end end
-- error on undefined variable access
local function err(t,k,v) error("Accessed an undefined variable: " .. tostring(k)) end
setmetatable(_G, { __index = err })
function util.count(self) function util.count(self)
local i = 0 local i = 0
if self == nil then return i end if self == nil then return i end
@ -141,6 +145,16 @@ function when(k,v,cb)
end end
end end
function trace(o)
if os.getenv('DEBUG') then
if type(o) == 'table' then
print_r(o)
else
print(o)
end
end
end
function util.merge(t1,t2) function util.merge(t1,t2)
local t3 = {} local t3 = {}
@ -180,4 +194,23 @@ util.commit = function(world, obj, api)
end end
end end
util.addHooks = function(fn, cb )
return function(...)
fn = (fn:gsub("^%l", string.upper)) -- uppercase first char
trace("api.ext.exec('before" .. fn .. "')")
api.ext.exec( "before" .. fn, ... )
local result = cb(...)
trace("api.ext.exec('after" .. fn .. "')")
api.ext.exec( "after" .. fn, ... )
return result
end
end
util.addModuleHooks = function(mod)
foreach( mod, function(k,v)
mod[k] = util.addHooks(k, v )
end)
return mod
end
return util return util