316 lines
9.3 KiB
Lua
316 lines
9.3 KiB
Lua
-- Buildat: builtin/voxelworld/client_lua/module.lua
|
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
|
|
local log = buildat.Logger("voxelworld")
|
|
local magic = require("buildat/extension/urho3d")
|
|
local replicate = require("buildat/extension/replicate")
|
|
local cereal = require("buildat/extension/cereal")
|
|
local dump = buildat.dump
|
|
local M = {}
|
|
|
|
--local LOD_DISTANCE = 140
|
|
--local LOD_DISTANCE = 100
|
|
local LOD_DISTANCE = 80
|
|
--local LOD_DISTANCE = 50
|
|
local LOD_THRESHOLD = 0.2
|
|
|
|
local PHYSICS_DISTANCE = 100
|
|
local INITIAL_PHYSICS_NEAR_WEIGHT = 1.0
|
|
|
|
local INITIAL_GEOMETRY_NEAR_WEIGHT = 0.15
|
|
|
|
local MAX_LOD = 4
|
|
|
|
local camera_node = nil
|
|
local update_counter = -1
|
|
|
|
local camera_p = magic.Vector3(0, 0, 0)
|
|
local camera_dir = magic.Vector3(0, 0, 0)
|
|
-- Start higher than any conceivable value because otherwise things will never
|
|
-- be triggered to reflect this value
|
|
local camera_far_clip = 1000
|
|
|
|
local camera_last_p = camera_p
|
|
local camera_last_dir = camera_dir
|
|
|
|
local end_of_update_processing_us = 0
|
|
|
|
M.chunk_size_voxels = nil
|
|
M.section_size_chunks = nil
|
|
M.section_size_voxels = nil
|
|
|
|
function M.init()
|
|
log:info("voxelworld.init()")
|
|
|
|
local node_update_queue = buildat.SpatialUpdateQueue()
|
|
|
|
local function queue_initial_node_update(node)
|
|
-- TODO: node:GetWorldPosition() is not at center of node
|
|
node_update_queue:put(node:GetWorldPosition(),
|
|
INITIAL_GEOMETRY_NEAR_WEIGHT, camera_far_clip * 1.2,
|
|
nil, nil, {
|
|
type = "geometry",
|
|
current_lod = 0,
|
|
node_id = node:GetID(),
|
|
})
|
|
-- TODO: node:GetWorldPosition() is not at center of node
|
|
node_update_queue:put(node:GetWorldPosition(),
|
|
INITIAL_PHYSICS_NEAR_WEIGHT, PHYSICS_DISTANCE, nil, nil, {
|
|
type = "physics",
|
|
node_id = node:GetID(),
|
|
})
|
|
end
|
|
|
|
buildat.sub_packet("voxelworld:init", function(data)
|
|
local values = cereal.binary_input(data, {"object",
|
|
{"chunk_size_voxels", {"object",
|
|
{"x", "int32_t"},
|
|
{"y", "int32_t"},
|
|
{"z", "int32_t"},
|
|
}},
|
|
{"section_size_chunks", {"object",
|
|
{"x", "int32_t"},
|
|
{"y", "int32_t"},
|
|
{"z", "int32_t"},
|
|
}},
|
|
})
|
|
log:info("voxelworld:init: "..dump(values))
|
|
M.chunk_size_voxels = buildat.Vector3(values.chunk_size_voxels)
|
|
M.section_size_chunks = buildat.Vector3(values.section_size_chunks)
|
|
M.section_size_voxels =
|
|
M.chunk_size_voxels:mul_components(M.section_size_chunks)
|
|
end)
|
|
|
|
buildat.sub_packet("voxelworld:node_voxel_data_updated", function(data)
|
|
local values = cereal.binary_input(data, {"object",
|
|
{"node_id", "int32_t"},
|
|
})
|
|
log:info("voxelworld:node_voxel_data_updated: "..dump(values))
|
|
local node = replicate.main_scene:GetNode(values.node_id)
|
|
queue_initial_node_update(node)
|
|
end)
|
|
|
|
local function update_voxel_geometry(node)
|
|
local data = node:GetVar("buildat_voxel_data"):GetBuffer()
|
|
--local registry_name = node:GetVar("buildat_voxel_registry_name"):GetBuffer()
|
|
|
|
-- TODO: node:GetWorldPosition() is not at center of node
|
|
local node_p = node:GetWorldPosition()
|
|
local d = (node_p - camera_p):Length()
|
|
local lod_fraction = d / LOD_DISTANCE
|
|
local lod = math.floor(1 + lod_fraction)
|
|
if lod > MAX_LOD then lod = MAX_LOD end
|
|
|
|
log:verbose("update_voxel_geometry(): lod="..lod..
|
|
", d="..math.floor(d)..", #data="..data:GetSize()..
|
|
", node="..node:GetID())
|
|
|
|
local near_trigger_d = nil
|
|
local near_weight = nil
|
|
local far_trigger_d = nil
|
|
local far_weight = nil
|
|
|
|
if d >= camera_far_clip * 1.4 then
|
|
log:verbose("Clearing voxel geometry outside camera far clip ("
|
|
..camera_far_clip..")")
|
|
buildat.clear_voxel_geometry(node)
|
|
|
|
-- clip out -> 4
|
|
near_trigger_d = 1.2 * camera_far_clip
|
|
near_weight = 0.4
|
|
elseif lod == 1 then
|
|
buildat.set_voxel_geometry(node, data, registry_name)
|
|
|
|
-- 1 -> 2
|
|
far_trigger_d = LOD_DISTANCE * (1.0 + LOD_THRESHOLD)
|
|
far_weight = 0.5
|
|
else
|
|
buildat.set_voxel_lod_geometry(lod, node, data, registry_name)
|
|
|
|
if lod == 1 then
|
|
-- Shouldn't go here
|
|
elseif lod == 2 then
|
|
-- 2 -> 1
|
|
near_trigger_d = LOD_DISTANCE * (1.0 - LOD_THRESHOLD)
|
|
near_weight = 0.75
|
|
-- 2 -> 3
|
|
far_trigger_d = 2 * LOD_DISTANCE * (1.0 + LOD_THRESHOLD)
|
|
far_weight = 0.4
|
|
elseif lod == 3 then
|
|
-- 3 -> 2
|
|
near_trigger_d = 2 * LOD_DISTANCE * (1.0 - LOD_THRESHOLD)
|
|
near_weight = 0.6
|
|
-- 3 -> 4
|
|
far_trigger_d = 3 * LOD_DISTANCE * (1.0 + LOD_THRESHOLD)
|
|
far_weight = 0.4
|
|
elseif lod == 4 then
|
|
-- 4 -> 3
|
|
near_trigger_d = 3 * LOD_DISTANCE * (1.0 - LOD_THRESHOLD)
|
|
near_weight = 0.5
|
|
-- 4 -> clip out
|
|
far_trigger_d = camera_far_clip * 1.4
|
|
far_weight = 0.4
|
|
end
|
|
end
|
|
node_update_queue:put(node_p,
|
|
near_weight, near_trigger_d,
|
|
far_weight, far_trigger_d, {
|
|
type = "geometry",
|
|
current_lod = lod,
|
|
node_id = node:GetID(),
|
|
})
|
|
end
|
|
|
|
local function update_voxel_physics(node)
|
|
local data = node:GetVar("buildat_voxel_data"):GetBuffer()
|
|
--local registry_name = node:GetVar("buildat_voxel_registry_name"):GetBuffer()
|
|
|
|
local node_p = node:GetWorldPosition()
|
|
local d = (node_p - camera_p):Length()
|
|
|
|
log:verbose("update_voxel_physics(): d="..math.floor(d)..
|
|
", #data="..data:GetSize()..", node="..node:GetID())
|
|
|
|
local near_trigger_d = nil
|
|
local near_weight = nil
|
|
local far_trigger_d = nil
|
|
local far_weight = nil
|
|
|
|
if d > PHYSICS_DISTANCE then
|
|
log:verbose("Clearing physics boxes outside physics distance ("..
|
|
PHYSICS_DISTANCE..")")
|
|
buildat.clear_voxel_physics_boxes(node)
|
|
|
|
near_trigger_d = PHYSICS_DISTANCE * 0.8
|
|
near_weight = 1.0
|
|
else
|
|
buildat.set_voxel_physics_boxes(node, data, registry_name)
|
|
|
|
far_trigger_d = PHYSICS_DISTANCE * 1.2
|
|
far_weight = 0.2
|
|
end
|
|
node_update_queue:put(node_p,
|
|
near_weight, near_trigger_d,
|
|
far_weight, far_trigger_d, {
|
|
type = "physics",
|
|
node_id = node:GetID(),
|
|
})
|
|
end
|
|
|
|
magic.SubscribeToEvent("Update", function(event_type, event_data)
|
|
buildat.profiler_block_begin("Buildat|voxelworld:update")
|
|
|
|
--local t0 = buildat.get_time_us()
|
|
--local dt = event_data:GetFloat("TimeStep")
|
|
update_counter = update_counter + 1
|
|
|
|
if camera_node then
|
|
camera_dir = camera_node.direction
|
|
camera_p = camera_node:GetWorldPosition()
|
|
camera_far_clip = camera_node:GetComponent("Camera").farClip
|
|
end
|
|
|
|
if camera_node and M.section_size_voxels then
|
|
-- TODO: How should position information be sent to the server?
|
|
local p = camera_p
|
|
if update_counter % 60 == 0 then
|
|
local section_p = buildat.Vector3(p):div_components(
|
|
M.section_size_voxels):floor()
|
|
--log:info("p: "..p.x..", "..p.y..", "..p.z.." -> section_p: "..
|
|
-- section_p.x..", "..section_p.y..", "..section_p.z)
|
|
--[[send_get_section(section_p + buildat.Vector3( 0, 0, 0))
|
|
send_get_section(section_p + buildat.Vector3(-1, 0, 0))
|
|
send_get_section(section_p + buildat.Vector3( 1, 0, 0))
|
|
send_get_section(section_p + buildat.Vector3( 0, 1, 0))
|
|
send_get_section(section_p + buildat.Vector3( 0,-1, 0))
|
|
send_get_section(section_p + buildat.Vector3( 0, 0, 1))
|
|
send_get_section(section_p + buildat.Vector3( 0, 0,-1))]]
|
|
end
|
|
end
|
|
|
|
-- Node updates: Handle one or a few per frame
|
|
local current_us = buildat.get_time_us()
|
|
-- Spend time doing this proportionate to the rest of the update cycle
|
|
local max_handling_time_us = (current_us - end_of_update_processing_us) / 3
|
|
local stop_at_us = current_us + max_handling_time_us
|
|
|
|
node_update_queue:set_p(camera_p)
|
|
-- Scale queue update operations according to the handling time of the
|
|
-- rest of the processing
|
|
node_update_queue:update(max_handling_time_us / 50 + 1)
|
|
|
|
for i = 1, 10 do -- Usually there is time only for a few
|
|
local f = node_update_queue:peek_next_f()
|
|
local fw = node_update_queue:peek_next_fw()
|
|
local did_update = false
|
|
if f and f <= 1.0 then
|
|
local node_update = node_update_queue:get()
|
|
local node = replicate.main_scene:GetNode(node_update.node_id)
|
|
log:verbose("Node update #"..
|
|
node_update_queue:get_length()..
|
|
" (f="..(math.floor(f*100)/100)..""..
|
|
", fw="..(math.floor(fw*100)/100)..")"..
|
|
": "..node:GetName())
|
|
if node_update.type == "geometry" then
|
|
update_voxel_geometry(node)
|
|
end
|
|
if node_update.type == "physics" then
|
|
update_voxel_physics(node)
|
|
end
|
|
did_update = true
|
|
else
|
|
log:verbose("Poked update #"..
|
|
node_update_queue:get_length()..
|
|
" (f="..(math.floor((f or -1)*100)/100)..""..
|
|
", fw="..(math.floor((fw or -1)*100)/100)..")")
|
|
--node_update_queue.queue = {} -- For testing
|
|
end
|
|
-- Check this at the end of the loop so at least one is handled
|
|
if not did_update or buildat.get_time_us() >= stop_at_us then
|
|
log:debug("Handled "..i.." node updates")
|
|
break
|
|
end
|
|
end
|
|
end_of_update_processing_us = buildat.get_time_us()
|
|
|
|
if camera_node then
|
|
camera_last_dir = camera_dir
|
|
camera_last_p = camera_p
|
|
end
|
|
--log:info(buildat.get_time_us()-t0)
|
|
buildat.profiler_block_end()
|
|
end)
|
|
|
|
replicate.sub_sync_node_added({}, function(node)
|
|
if not node:GetVar("buildat_voxel_data"):IsEmpty() then
|
|
queue_initial_node_update(node)
|
|
end
|
|
--local name = node:GetName()
|
|
end)
|
|
end
|
|
|
|
function M.set_camera(new_camera_node)
|
|
camera_node = new_camera_node
|
|
end
|
|
|
|
function send_get_section(p)
|
|
local data = cereal.binary_output({
|
|
p = {
|
|
x = p.x,
|
|
y = p.y,
|
|
z = p.z,
|
|
},
|
|
}, {"object",
|
|
{"p", {"object",
|
|
{"x", "int32_t"},
|
|
{"y", "int32_t"},
|
|
{"z", "int32_t"},
|
|
}},
|
|
})
|
|
--log:info(dump(buildat.bytes(data)))
|
|
buildat.send_packet("voxelworld:get_section", data)
|
|
end
|
|
|
|
return M
|
|
-- vim: set noet ts=4 sw=4:
|