diff --git a/README.md b/README.md index ac04db5..64cfa5b 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ Download/run the binaries for your platform in the releases section.
Developers can run it via [Lua](https://lua.org), [LÖVR](https://lovr.org) or [LÖVE2D](https://love2d.org) ``` -$ love xurfer [..] # if you have LÖVE2D installed (>v12) -$ lovr xurfer [..] # if you have LÖVR installed -$ lua xurfer/main.lua [..] # if you have lua installed +$ cd xurfer +$ love . [..] # if you have LÖVE2D installed (>v12) +$ lovr . [..] # if you have LÖVR installed +$ lua main.lua [..] # if you have lua installed ``` **Example**: lovr xurfer https://snips.sh/f/_U5-XctEVE?r=1 diff --git a/xurfer/conf.lua b/xurfer/conf.lua index ba25fe6..8b9459a 100644 --- a/xurfer/conf.lua +++ b/xurfer/conf.lua @@ -11,5 +11,4 @@ package.path = package.path .. ';' .. arg[0] .. '/' .. runtime .. '/?/init.lua;' .. arg[0] .. '/lib/?.lua' - require( runtime .. "/conf") diff --git a/xurfer/ecs.lua b/xurfer/ecs.lua index 865a47e..9cb2f06 100644 --- a/xurfer/ecs.lua +++ b/xurfer/ecs.lua @@ -7,8 +7,16 @@ ecs.init = function() baseEntify.updatethread = true function baseEntify:onAdd(obj) obj.commit = api.util.commit( api.world, obj, api ) + api.world.commit = api.util.commit( api.world, false, api) end ecs.addSystem( api.world, baseEntify ) end +ecs.clear = function() + api.ext.exec("onClear") + api.ecs.clearEntities( api.world ) + api.ecs.update( api.world ) + api.world.commit() +end + return ecs diff --git a/xurfer/ext/3DFile/main.lua b/xurfer/ext/3DFile/main.lua index ae5667c..17db0b3 100644 --- a/xurfer/ext/3DFile/main.lua +++ b/xurfer/ext/3DFile/main.lua @@ -6,16 +6,26 @@ return { enabled = true, 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 == 'GLTF' or obj.URL.extension == 'OBJ')) then - if obj.URLResponse.ok then - obj.model = api.graphics.newModel( api.data.newBlob( obj.URLResponse.data) ) - obj.root = (obj.URI.target == '_top') - if obj.x == nil then obj.x = 0 end - if obj.y == nil then obj.y = 0 end - if obj.z == nil then obj.z = 0 end + if obj.URLResponse.ok or obj.URL.protocol == 'file' then + obj.root = false + if obj.URI.target == '_top' then + obj.root = true + api.ext.URI.current = obj.URL -- set current url + 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 else print("[3DFile] error: could not load " .. obj.URL.string ) diff --git a/xurfer/ext/AudioFile/main.lua b/xurfer/ext/AudioFile/main.lua new file mode 100644 index 0000000..56eccc0 --- /dev/null +++ b/xurfer/ext/AudioFile/main.lua @@ -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 + +} diff --git a/xurfer/ext/URI/main.lua b/xurfer/ext/URI/main.lua index 722953f..4b6e452 100644 --- a/xurfer/ext/URI/main.lua +++ b/xurfer/ext/URI/main.lua @@ -4,6 +4,7 @@ local ecs = api.ecs return { name = "URI", enabled = true, + current = {}, -- will hold the most recent (_top) URL object init = function() local urlListener @@ -19,23 +20,27 @@ return { to = function(URI,refererer, obj) obj = obj or {} - local msg = "[i] loading" - print("[i] loading " .. URI.url) + print("[i] loading URL: " .. URI.url) obj.URL = api.url.parse(URI.url) obj.URLResponse = {} + local protocol = "file" foreach( api.protocol, function(name, p) if obj.URL.protocol:match("^"..name) then protocol = p end end) if protocol then + if obj.URL.protocol == 'file' then + return obj.commit('onURI') + end local status, data, headers = protocol.request(URI.url) obj.URLResponse = { - ok = (status >= 200 and status < 300), + ok = (status ~= nil and status >= 200 and status < 300), status = status, data = data, headers = headers } + api.ecs.add( api.world, obj ) obj.commit('onURI') end end diff --git a/xurfer/ext/kitchensink/main.lua b/xurfer/ext/kitchensink/main.lua index 4c3c2f7..4d9be00 100644 --- a/xurfer/ext/kitchensink/main.lua +++ b/xurfer/ext/kitchensink/main.lua @@ -4,7 +4,7 @@ local sample return { name = "kitchensink", - enabled = (lover ~= nil), + enabled = false, --(lovr ~= nil), init = function() sample = require "sample" diff --git a/xurfer/ext/startupscene/main.lua b/xurfer/ext/startupscene/main.lua index 7bce0a5..97f44b8 100644 --- a/xurfer/ext/startupscene/main.lua +++ b/xurfer/ext/startupscene/main.lua @@ -1,13 +1,10 @@ local api = ... local envTex -local skybox -local environmentMap -local sphericalHarmonics local shader return { name = "startupscene", - enabled = true, + enabled = (lovr ~= nil), init = function() end, @@ -19,8 +16,8 @@ return { load = function() --if api.iui.idiom == "vr" then - envTex = lovr.graphics.newTexture("lovr/assets/img/env.png", {}) - skybox = lovr.graphics.newTexture({ + envTex = api.graphics.newTexture("lovr/assets/img/env.png", {}) + api.world.skybox = api.graphics.newTexture({ px = 'ext/startupscene/skybox/Dayright.jpg', -- 'px.png', nx = 'ext/startupscene/skybox/Dayleft.jpg', -- 'nx.png', py = 'ext/startupscene/skybox/Daytop.jpg', -- 'py.png', @@ -28,9 +25,9 @@ return { pz = 'ext/startupscene/skybox/Dayfront.jpg', -- 'pz.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.659514904022217, 0.665349841117859, 0.567680120468140 }, { 0.451633930206299, 0.450751245021820, 0.355226665735245 }, @@ -41,36 +38,6 @@ return { { -0.179465517401695, -0.181243389844894, -0.141314014792442 }, { -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, draw = function(pass) @@ -78,7 +45,7 @@ return { -- skybox pass:setCullMode('back') pass:setBlendMode() - pass:skybox(skybox) + pass:skybox(api.world.skybox) pass:setColor(1, 1, 1) 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: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:setShader(shader) - pass:send('cubemap', environmentMap) - pass:send('sphericalHarmonics', sphericalHarmonics) + + pass:setShader( lovr.shader.pbr ) + pass:send('cubemap', api.world.environmentMap) + pass:send('sphericalHarmonics', api.world.sphericalHarmonics) end end diff --git a/xurfer/ext/tween/main.lua b/xurfer/ext/tween/main.lua new file mode 100644 index 0000000..a638404 --- /dev/null +++ b/xurfer/ext/tween/main.lua @@ -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}) +} diff --git a/xurfer/ext/tween/tween.lua b/xurfer/ext/tween/tween.lua new file mode 100644 index 0000000..4f2c681 --- /dev/null +++ b/xurfer/ext/tween/tween.lua @@ -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 diff --git a/xurfer/ext/xrfragments/level2.lua b/xurfer/ext/xrfragments/level2.lua new file mode 100644 index 0000000..c0f50e5 --- /dev/null +++ b/xurfer/ext/xrfragments/level2.lua @@ -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 +} diff --git a/xurfer/ext/xrfragments/level4.lua b/xurfer/ext/xrfragments/level4.lua new file mode 100644 index 0000000..2a5057a --- /dev/null +++ b/xurfer/ext/xrfragments/level4.lua @@ -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 +} diff --git a/xurfer/ext/xrfragments/lovr-xrf.lua b/xurfer/ext/xrfragments/lovr-xrf.lua index 432279c..2edb459 100644 --- a/xurfer/ext/xrfragments/lovr-xrf.lua +++ b/xurfer/ext/xrfragments/lovr-xrf.lua @@ -25,6 +25,7 @@ -- or implied, of local json = require("json") +local url = require("url") local xrf = {} xrf.traverseNodesContaining = function(key,obj,cb) @@ -36,14 +37,17 @@ xrf.traverseNodesContaining = function(key,obj,cb) for k, tkey in pairs(keys) do local i = 1 local tree = metadata[tkey] + local foundhrefs = false xrf.traverse( tree, function(node) if node['extras'] ~= nil then if node['extras'][key] ~= nil then + foundhrefs = true cb(node,obj, i, metadata) end end i = i + 1 end) + if foundhrefs then break end -- blender puts duplicate (undo?) data in 'meshes' sometimes? end end @@ -86,7 +90,6 @@ xrf.makeClickable = function( physicsWorld, cb) local model = obj['model'] local node = node_or_mesh 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 mesh = model:getMesh( meshindex ) 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[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 }) end end @@ -122,4 +126,18 @@ xrf.traverse = function(arr, cb, key) 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 diff --git a/xurfer/ext/xrfragments/main.lua b/xurfer/ext/xrfragments/main.lua index 29a081e..3fc72a0 100644 --- a/xurfer/ext/xrfragments/main.lua +++ b/xurfer/ext/xrfragments/main.lua @@ -1,9 +1,12 @@ -local api = ... +local api = ... +local url = require('url') +local tween = require('tween') local ecs = api.ecs -local xrf = require("ext/xrfragments/lovr-xrf") -local xrfsystem +local xrf = require("ext/xrfragments/lovr-xrf") +xrf.level2 = api.util.addModuleHooks( require("ext/xrfragments/level2") ) +xrf.level4 = api.util.addModuleHooks( require("ext/xrfragments/level4") ) -return { +xrfimpl = { enabled = true, init = function() end, @@ -13,12 +16,41 @@ return { xrf.traverseNodesContaining('href', obj, xrf.makeClickable( api.ecs.worldPhysics, function(obj, collider) - print("\nhref was clicked: " .. obj.node.extras.href) - print_r(obj) + local href = obj.node.extras.href + 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, + + 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 } + +return xrfimpl diff --git a/xurfer/ext/xrfragments/url.lua b/xurfer/ext/xrfragments/url.lua index f2a11c2..3a857bc 100644 --- a/xurfer/ext/xrfragments/url.lua +++ b/xurfer/ext/xrfragments/url.lua @@ -57,7 +57,9 @@ local function parseArgs(fragment) for _, item in ipairs(items) do local key_value = split(item, "=") 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 ARG[key_value[1]] = "" end @@ -67,6 +69,7 @@ end -- Main URL Parser function attached to the module object function url.parse(url_string) + if type(url_string) == 'table' then return url_string end -- already parsed -- Setup initial URI table structure local URI = { domain = "", @@ -86,6 +89,8 @@ function url.parse(url_string) if protocol then URI.protocol = protocol:gsub("://", "") remaining = remaining:sub(#protocol + 1) + else + URI.protocol = "file" end local hash = string.match(remaining, "(#.*)$") -- fragment if hash then @@ -140,5 +145,17 @@ function url.parse(url_string) return URI 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 url diff --git a/xurfer/lib/tiny-ecs.lua b/xurfer/lib/tiny-ecs.lua index 4dca2cd..d09f6c8 100644 --- a/xurfer/lib/tiny-ecs.lua +++ b/xurfer/lib/tiny-ecs.lua @@ -656,8 +656,10 @@ function tiny_manageEntities(world) elseif index then system.modified = true local tmpEntity = ses[#ses] - ses[index] = tmpEntity - seis[tmpEntity] = index + if tmpEntity ~= nil then + ses[index] = tmpEntity + seis[tmpEntity] = index + end seis[entity] = nil ses[#ses] = nil local onRemove = system.onRemove diff --git a/xurfer/lovr/main.lua b/xurfer/lovr/main.lua index 8b15b89..bf7567d 100644 --- a/xurfer/lovr/main.lua +++ b/xurfer/lovr/main.lua @@ -22,7 +22,6 @@ ecs.filterUpdate = ecs.requireAll('updatethread') -- ecs systems local interactionsUpdater -local renderer --- @type Texture local envTex @@ -120,7 +119,7 @@ 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, button=button} + interactionsUpdater.mouse.moved = {x=x, y=y} end function lovr.mousepressed(x, y, button) @@ -146,7 +145,7 @@ if launch.mode == "desktop" then function lovr.keyreleased(key, scancode) backend.keyreleased(key, scancode) - interactionsUpdater.key.released = {key=key,scancode=scancode,isRepeat=isRepeat} + interactionsUpdater.key.released = {key=key,scancode=scancode} end function lovr.textinput(text) @@ -155,27 +154,6 @@ if launch.mode == "desktop" then 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) 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) if collider then - if lovr.headset.isDown(hand, 'trigger') then - print("buttondown") - end - if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then - local node = collider:getUserData() - interactionsUpdater.selectedBox = collider - interactionsUpdater.hitpoint:set(x, y, z) - if node.onclick ~= nil then node.onclick(node,collider) end + for i, hand in ipairs(lovr.headset.getHands()) do + if lovr.headset.isDown(hand, 'trigger') then + print("buttondown") + end + if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then + 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 -- reset stuff @@ -216,5 +196,21 @@ function initInteractions(ecs) ecs.addSystem( api.world, interactionsUpdater ) 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) +initCleanups(api.ecs) +require("render/model").init(api) diff --git a/xurfer/lovr/render/model.lua b/xurfer/lovr/render/model.lua new file mode 100644 index 0000000..596d232 --- /dev/null +++ b/xurfer/lovr/render/model.lua @@ -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 diff --git a/xurfer/main.lua b/xurfer/main.lua index 8006ba7..7dac0f6 100644 --- a/xurfer/main.lua +++ b/xurfer/main.lua @@ -9,6 +9,14 @@ -- ./lovr xurfer https://snips.sh/f/rHFLg-cewi?r=1' -- cube -- ./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 package.path = package.path .. ';' .. 'xurfer/?.lua' require('conf') @@ -32,13 +40,7 @@ api = { } } -local runtime -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 ) +api = util.merge( api, runtime.api ) -- overlay api onto lovr/love-compatible runtime api require( runtime.path .. "/main") @@ -49,9 +51,7 @@ api.ext.exec('init') -- load urls passed on the cli foreach( arg, function(k,uri) - foreach( api.protocol, function(name, p) - if uri:match("^"..name) then - api.ecs.add( api.world, { URI = { url = uri, method = 'GET', target = '_top' } }) - end - end) + if uri:find(".") and k ~= 0 and k ~= -1 then + api.ecs.add( api.world, { URI = { url = uri, method = 'GET', target = '_top' } }) + end end) diff --git a/xurfer/test.glb b/xurfer/test.glb new file mode 100644 index 0000000..2cee76d Binary files /dev/null and b/xurfer/test.glb differ diff --git a/xurfer/util.lua b/xurfer/util.lua index 08c8fb0..1a50031 100644 --- a/xurfer/util.lua +++ b/xurfer/util.lua @@ -69,6 +69,10 @@ function util.dump(value, indent, seen) 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) local i = 0 if self == nil then return i end @@ -141,6 +145,16 @@ function when(k,v,cb) 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) local t3 = {} @@ -180,4 +194,23 @@ util.commit = function(world, obj, api) 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