1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-09-30 19:22:14 +00:00

Merge branch 'luanti-org:master' into skybox

This commit is contained in:
Mircea Kitsune 2025-07-01 19:40:30 +03:00 committed by GitHub
commit 770ead7624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 1815 additions and 1848 deletions

View file

@ -25,16 +25,16 @@ Contributions are welcome! Here's how you can help:
the work, to avoid disappointment.
You may also benefit from discussing on our IRC development channel
[#luanti-dev](http://www.luanti.org/irc/). Note that a proper IRC client
[#luanti-dev](https://docs.luanti.org/about/irc/). Note that a proper IRC client
is required to speak on this channel.
3. Start coding!
- Refer to the
[Lua API](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md),
[Developer Wiki](https://dev.luanti.org/) and other
[Luanti Documentation](https://docs.luanti.org/) and other
[documentation](https://github.com/luanti-org/luanti/tree/master/doc).
- Follow the [C/C++](https://dev.luanti.org/Code_style_guidelines) and
[Lua](https://dev.luanti.org/Lua_code_style_guidelines) code style guidelines.
- Follow the [C/C++](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) and
[Lua](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/) code style guidelines.
- Check your code works as expected and document any changes to the Lua API.
- To avoid conflicting changes between contributions, do not do the following manually. They will be done before each release.
- Run `updatepo.sh` or update `luanti.po{,t}` even if your code adds new translatable strings.
@ -64,8 +64,8 @@ Contributions are welcome! Here's how you can help:
picture of the project.
2. It works.
3. It follows the code style for
[C/C++](https://dev.luanti.org/Code_style_guidelines) or
[Lua](https://dev.luanti.org/Lua_code_style_guidelines).
[C/C++](https://docs.luanti.org/for-engine-devs/code-style-guidelines/) or
[Lua](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/).
4. The code's interfaces are well designed, regardless of other aspects that
might need more work in the future.
5. It uses protocols and formats which include the required compatibility.
@ -106,7 +106,7 @@ the project page with a list of current languages
Builtin (the component which contains things like server messages, chat command
descriptions, privilege descriptions) is translated separately; it needs to be
translated by editing a `.tr` text file. See
[Translation](https://dev.luanti.org/Translation) for more information.
[Translation](https://docs.luanti.org/for-creators/translation/) for more information.
## Donations
@ -116,11 +116,11 @@ methods on [our website](http://www.luanti.org/development/#donate).
# Maintaining
* This is a concise version of the
[Rules & Guidelines](https://dev.luanti.org/engine-dev-process/) on the developer wiki.*
[Rules & Guidelines](https://docs.luanti.org/for-engine-devs/) on the Luanti Documentation.*
These notes are for those who have push access Luanti (core developers / maintainers).
- See the [project organisation](https://dev.luanti.org/Organisation) for the people involved.
- See the [project organisation](https://docs.luanti.org/for-engine-devs/organization/) for the people involved.
## Concept approvals and roadmaps
@ -169,4 +169,4 @@ Submit a :+1: (+1) or "Looks good" comment to show you believe the pull-request
## Releasing a new version
*Refer to [dev.luanti.org/Releasing_Luanti](https://dev.luanti.org/Releasing_Luanti)*
*Refer to [docs.luanti.org/for-engine-devs/releasing-luanti](https://docs.luanti.org/for-engine-devs/releasing-luanti/)*

View file

@ -76,6 +76,8 @@ jobs:
env:
CC: gcc-14
CXX: g++-14
# just to check that they compile correctly
CMAKE_FLAGS: '-DBUILD_BENCHMARKS=1'
- name: Test
run: |

View file

@ -28,7 +28,7 @@ Table of Contents
Further documentation
----------------------
- Website: https://www.luanti.org/
- Wiki: https://wiki.luanti.org/
- Luanti Documentation: https://docs.luanti.org/
- Forum: https://forum.luanti.org/
- GitHub: https://github.com/luanti-org/luanti/
- [Developer documentation](doc/developing/)

View file

@ -438,13 +438,30 @@ function make.key(setting)
if value == "" then
return height
end
local critical_keys = {
keymap_drop = true,
keymap_dig = true,
keymap_place = true,
}
for _, o in ipairs(core.full_settingtypes) do
if o.type == "key" and o.name ~= setting.name and core.are_keycodes_equal(core.settings:get(o.name), value) then
if o.type == "key" and o.name ~= setting.name and
core.are_keycodes_equal(core.settings:get(o.name), value) then
local is_current_close_world = setting.name == "keymap_close_world"
local is_other_close_world = o.name == "keymap_close_world"
local is_current_critical = critical_keys[setting.name]
local is_other_critical = critical_keys[o.name]
if (is_other_critical or is_current_critical) or
(not is_current_close_world and not is_other_close_world) then
table.insert(fs, ("label[0,%f;%s]"):format(height + 0.3,
core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext(o.readable_name)))))
height = height + 0.6
end
end
end
return height
end
return {

View file

@ -16,7 +16,7 @@ local minetest_example_header = [[
# to the program, eg. "luanti.exe --config ../minetest.conf.example".
# Further documentation:
# https://wiki.luanti.org/
# https://docs.luanti.org/
]]

View file

@ -118,7 +118,7 @@ function ui.update()
if (active_toplevel_ui_elements > 1) then
core.log("warning", "more than one active ui "..
"element, self most likely isn't intended")
"element, this most likely isn't intended")
end
if (active_toplevel_ui_elements == 0) then

View file

@ -740,16 +740,16 @@ core.noneitemdef_default = { -- This is used for the hand and unknown items
--
local get_node_raw = core.get_node_raw
core.get_node_raw = nil
local get_name_from_content_id = core.get_name_from_content_id
function core.get_node(pos)
local content, param1, param2 = get_node_raw(pos.x, pos.y, pos.z)
return {name = core.get_name_from_content_id(content), param1 = param1, param2 = param2}
return {name = get_name_from_content_id(content), param1 = param1, param2 = param2}
end
function core.get_node_or_nil(pos)
local content, param1, param2, pos_ok = get_node_raw(pos.x, pos.y, pos.z)
return pos_ok and
{name = core.get_name_from_content_id(content), param1 = param1, param2 = param2}
{name = get_name_from_content_id(content), param1 = param1, param2 = param2}
or nil
end

View file

@ -3,18 +3,61 @@
-- SPDX-License-Identifier: LGPL-2.1-or-later
local function get_info_formspec(size, padding, text)
return table.concat({
"formspec_version[6]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
local function get_description_hypertext(package, info, loading_error)
-- Screenshots and description
local hypertext = "<big><b>" .. core.hypertext_escape(package.short_description) .. "</b></big>\n"
"label[4,4.35;", text, "]",
"container[", padding.x, ",", size.y - 0.8 - padding.y, "]",
"button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container_end[]",
})
local screenshots = info and info.screenshots or {{url = package.thumbnail}}
local winfo = core.get_window_info()
local fs_to_px = winfo.size.x / winfo.max_formspec_size.x
for i, ss in ipairs(screenshots) do
local path = get_screenshot(package, ss.url, 2)
hypertext = hypertext .. "<action name=\"ss_".. i .. "\"><img name=\"" ..
core.hypertext_escape(path) .. "\" width=" .. (3 * fs_to_px) ..
" height=" .. (2 * fs_to_px) .. "></action>"
if i ~= #screenshots then
hypertext = hypertext .. "<img name=\"blank.png\" width=" .. (0.25 * fs_to_px) ..
" height=" .. (2.25 * fs_to_px).. ">"
end
end
if info then
hypertext = hypertext .. "\n" .. info.long_description.head
local first = true
local function add_link_button(label, name)
if info[name] then
if not first then
hypertext = hypertext .. " | "
end
hypertext = hypertext .. "<action name=link_" .. name .. ">" .. label .. "</action>"
info.long_description.links["link_" .. name] = info[name]
first = false
end
end
add_link_button(hgettext("Donate"), "donate_url")
add_link_button(hgettext("Website"), "website")
add_link_button(hgettext("Source"), "repo")
add_link_button(hgettext("Issue Tracker"), "issue_tracker")
add_link_button(hgettext("Translate"), "translation_url")
add_link_button(hgettext("Forum Topic"), "forum_url")
hypertext = hypertext .. "\n\n" .. info.long_description.body
elseif loading_error then
hypertext = hypertext .. "\n\n" .. hgettext("Error loading package information")
else
hypertext = hypertext .. "\n\n" .. hgettext("Loading...")
end
-- Fix the path to blank.png. This is needed for bullet indentation,
-- and also used for screenshot spacing.
hypertext = hypertext:gsub("<img name=\"?blank.png\"? ",
"<img name=\"" .. core.hypertext_escape(defaulttexturedir) .. "blank.png\" ")
return hypertext
end
@ -41,18 +84,10 @@ local function get_formspec(data)
assert(data.package.name == info.name)
data.info = info
-- note: get_full_package_info can also return cached info immediately
ui.update()
end)
end
-- get_full_package_info can return cached info immediately, so
-- check to see if that happened
if not data.info then
if data.loading_error then
return get_info_formspec(size, window_padding, fgettext("Error loading package information"))
end
return get_info_formspec(size, window_padding, fgettext("Loading..."))
end
end
-- Check installation status
@ -60,10 +95,14 @@ local function get_formspec(data)
local info = data.info
local info_line =
fgettext("by $1 — $2 downloads — +$3 / $4 / -$5",
local info_line
if info then
info_line = fgettext_ne("by $1 — $2 downloads — +$3 / $4 / -$5",
info.author, info.downloads,
info.reviews.positive, info.reviews.neutral, info.reviews.negative)
else
info_line = fgettext_ne("by $1", package.author)
end
local bottom_buttons_y = H - 0.8
@ -79,7 +118,7 @@ local function get_formspec(data)
"button[", W - 3, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]",
"style_type[label;font_size=+24;font=bold]",
"label[0,0.4;", core.formspec_escape(info.title), "]",
"label[0,0.4;", core.formspec_escape(package.title), "]",
"style_type[label;font_size=;font=]",
"label[0,1.2;", core.formspec_escape(info_line), "]",
@ -100,11 +139,13 @@ local function get_formspec(data)
formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not package.path then
local label = info and fgettext("Install [$1]", info.download_size) or
fgettext("Install")
formspec[#formspec + 1] = "style[install;bgcolor=green]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] =";install;"
formspec[#formspec + 1] = fgettext("Install [$1]", info.download_size)
formspec[#formspec + 1] = label
formspec[#formspec + 1] = "]"
else
if package.installed_release < package.release then
@ -125,13 +166,15 @@ local function get_formspec(data)
formspec[#formspec + 1] = "]"
end
local review_count = info.reviews.positive + info.reviews.neutral + info.reviews.negative
local current_tab = data.current_tab or 1
local tab_titles = {
fgettext("Description"),
fgettext("Information"),
fgettext("Reviews") .. core.formspec_escape(" [" .. review_count .. "]"),
}
if info then
local review_count = info.reviews.positive + info.reviews.neutral + info.reviews.negative
table.insert(tab_titles, fgettext("Information"))
table.insert(tab_titles, fgettext("Reviews") .. core.formspec_escape(" [" .. review_count .. "]"))
end
local tab_body_height = bottom_buttons_y - 2.8
@ -147,59 +190,21 @@ local function get_formspec(data)
})
if current_tab == 1 then
-- Screenshots and description
local hypertext = "<big><b>" .. core.hypertext_escape(info.short_description) .. "</b></big>\n"
local winfo = core.get_window_info()
local fs_to_px = winfo.size.x / winfo.max_formspec_size.x
for i, ss in ipairs(info.screenshots) do
local path = get_screenshot(package, ss.url, 2)
hypertext = hypertext .. "<action name=\"ss_".. i .. "\"><img name=\"" ..
core.hypertext_escape(path) .. "\" width=" .. (3 * fs_to_px) ..
" height=" .. (2 * fs_to_px) .. "></action>"
if i ~= #info.screenshots then
hypertext = hypertext .. "<img name=\"blank.png\" width=" .. (0.25 * fs_to_px) ..
" height=" .. (2.25 * fs_to_px).. ">"
end
end
hypertext = hypertext .. "\n" .. info.long_description.head
local first = true
local function add_link_button(label, name)
if info[name] then
if not first then
hypertext = hypertext .. " | "
end
hypertext = hypertext .. "<action name=link_" .. name .. ">" .. core.hypertext_escape(label) .. "</action>"
info.long_description.links["link_" .. name] = info[name]
first = false
end
end
add_link_button(fgettext("Donate"), "donate_url")
add_link_button(fgettext("Website"), "website")
add_link_button(fgettext("Source"), "repo")
add_link_button(fgettext("Issue Tracker"), "issue_tracker")
add_link_button(fgettext("Translate"), "translation_url")
add_link_button(fgettext("Forum Topic"), "forum_url")
hypertext = hypertext .. "\n\n" .. info.long_description.body
-- Fix the path to blank.png. This is needed for bullet indentation.
hypertext = hypertext:gsub("<img name=\"?blank.png\"? ",
"<img name=\"" .. core.hypertext_escape(defaulttexturedir) .. "blank.png\" ")
local hypertext = get_description_hypertext(package, info, data.loading_error)
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";desc;", core.formspec_escape(hypertext), "]",
})
elseif current_tab == 2 then
assert(info)
local hypertext = info.info_hypertext.head .. info.info_hypertext.body
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";info;", core.formspec_escape(hypertext), "]",
})
elseif current_tab == 3 then
assert(info)
if not package.reviews and not data.reviews_error and not data.reviews_loading then
data.reviews_loading = true
@ -286,10 +291,6 @@ local function handle_submit(this, fields)
return true
end
if not info then
return false
end
if fields.open_contentdb then
local version = core.get_version()
local url = core.settings:get("contentdb_url") .. "/packages/" .. package.url_part ..
@ -312,6 +313,12 @@ local function handle_submit(this, fields)
return true
end
-- The events handled below are only valid if the package info has finished
-- loading.
if not info then
return false
end
if fields.tabs then
this.data.current_tab = tonumber(fields.tabs)
return true

View file

@ -33,13 +33,13 @@ end
local function buttonhandler(this, fields)
if fields.reconfigure then
local parent = this.parent
close_dialog(this)
local maintab = ui.find_by_name("maintab")
local dlg = create_settings_dlg("controls_keyboard_and_mouse")
dlg:set_parent(maintab)
maintab:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
return true
@ -74,7 +74,7 @@ local function create_rebind_keys_dlg()
return dlg
end
function migrate_keybindings()
function migrate_keybindings(parent)
-- Show migration dialog if the user upgraded from an earlier version
-- and this has not yet been shown before, *or* if keys settings had to be changed
if core.is_first_run then
@ -95,14 +95,14 @@ function migrate_keybindings()
end
if not has_migration then
return
return parent
end
local maintab = ui.find_by_name("maintab")
local dlg = create_rebind_keys_dlg()
dlg:set_parent(maintab)
maintab:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
ui.update()
return dlg
end

View file

@ -11,7 +11,7 @@
local SETTING_NAME = "no_mtg_notification"
function check_reinstall_mtg()
function check_reinstall_mtg(parent)
-- used to be in minetest.conf
if core.settings:get_bool(SETTING_NAME) then
cache_settings:set_bool(SETTING_NAME, true)
@ -19,14 +19,14 @@ function check_reinstall_mtg()
end
if cache_settings:get_bool(SETTING_NAME) then
return
return parent
end
local games = core.get_games()
for _, game in ipairs(games) do
if game.id == "minetest" then
cache_settings:set_bool(SETTING_NAME, true)
return
return parent
end
end
@ -40,16 +40,16 @@ function check_reinstall_mtg()
end
if not mtg_world_found then
cache_settings:set_bool(SETTING_NAME, true)
return
return parent
end
local maintab = ui.find_by_name("maintab")
local dlg = create_reinstall_mtg_dlg()
dlg:set_parent(maintab)
maintab:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
ui.update()
return dlg
end
local function get_formspec(dialogdata)
@ -74,22 +74,22 @@ end
local function buttonhandler(this, fields)
if fields.reinstall then
local parent = this.parent
-- Don't set "no_mtg_notification" here so that the dialog will be shown
-- again if downloading MTG fails for whatever reason.
this:delete()
local maintab = ui.find_by_name("maintab")
local dlg = create_contentdb_dlg(nil, "minetest/minetest")
dlg:set_parent(maintab)
maintab:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
return true
end
if fields.dismiss then
cache_settings:set_bool("no_mtg_notification", true)
cache_settings:set_bool(SETTING_NAME, true)
this:delete()
return true
end

View file

@ -112,8 +112,12 @@ local function init_globals()
tv_main:show()
ui.update()
check_reinstall_mtg()
migrate_keybindings()
-- synchronous, chain parents to only show one at a time
local parent = tv_main
parent = migrate_keybindings(parent)
check_reinstall_mtg(parent)
-- asynchronous, will only be shown if we're still on "maintab"
check_new_version()
end

View file

@ -182,23 +182,17 @@ invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false
[**Keybindings]
# Key for moving the player forward.
keymap_forward (Move forward) key SYSTEM_SCANCODE_26
# Key for moving the player backward.
# Will also disable autoforward, when active.
keymap_backward (Move backward) key SYSTEM_SCANCODE_22
# Key for moving the player left.
keymap_left (Move left) key SYSTEM_SCANCODE_4
# Key for moving the player right.
keymap_right (Move right) key SYSTEM_SCANCODE_7
# Key for jumping.
keymap_jump (Jump) key SYSTEM_SCANCODE_44
# Key for sneaking.
# Also used for climbing down and descending in water if aux1_descends is disabled.
keymap_sneak (Sneak) key SYSTEM_SCANCODE_225
@ -210,13 +204,11 @@ keymap_dig (Dig/punch/use) key KEY_LBUTTON
# (Note: The actual meaning might vary on a per-game basis.)
keymap_place (Place/use) key KEY_RBUTTON
# Key for opening the inventory.
keymap_inventory (Open inventory) key SYSTEM_SCANCODE_12
# Key for moving fast in fast mode.
keymap_aux1 (Aux1) key SYSTEM_SCANCODE_8
# Key for opening the chat window.
keymap_chat (Open chat) key SYSTEM_SCANCODE_23
# Key for opening the chat window to type commands.
@ -225,70 +217,48 @@ keymap_cmd (Command) key SYSTEM_SCANCODE_56
# Key for opening the chat window to type local commands.
keymap_cmd_local (Local command) key SYSTEM_SCANCODE_55
# Key for toggling unlimited view range.
keymap_rangeselect (Range select) key
keymap_rangeselect (Toggle unlimited view range) key
# Key for toggling flying.
keymap_freemove (Toggle fly) key SYSTEM_SCANCODE_14
# Key for toggling pitch move mode.
keymap_pitchmove (Toggle pitchmove) key
# Key for toggling fast mode.
keymap_fastmove (Toggle fast) key SYSTEM_SCANCODE_13
# Key for toggling noclip mode.
keymap_noclip (Toggle noclip) key SYSTEM_SCANCODE_11
# Key for selecting the next item in the hotbar.
keymap_hotbar_next (Hotbar: select next item) key SYSTEM_SCANCODE_17
# Key for selecting the previous item in the hotbar.
keymap_hotbar_previous (Hotbar: select previous item) key SYSTEM_SCANCODE_5
# Key for muting the game.
keymap_mute (Mute) key SYSTEM_SCANCODE_16
# Key for increasing the volume.
keymap_increase_volume (Increase volume) key
# Key for decreasing the volume.
keymap_decrease_volume (Decrease volume) key
# Key for toggling autoforward.
keymap_autoforward (Toggle automatic forward) key
# Key for toggling cinematic mode.
keymap_cinematic (Toggle cinematic mode) key
# Key for toggling display of minimap.
keymap_minimap (Toggle minimap) key SYSTEM_SCANCODE_25
# Key for taking screenshots.
keymap_screenshot (Screenshot) key SYSTEM_SCANCODE_69
# Key for toggling fullscreen mode.
keymap_fullscreen (Toggle fullscreen) key SYSTEM_SCANCODE_68
# Key for dropping the currently selected item.
keymap_drop (Drop item) key SYSTEM_SCANCODE_20
# Key to use view zoom when possible.
keymap_zoom (Zoom) key SYSTEM_SCANCODE_29
# Key for toggling the display of the HUD.
keymap_toggle_hud (Toggle HUD) key SYSTEM_SCANCODE_58
# Key for toggling the display of chat.
keymap_toggle_chat (Toggle chat log) key SYSTEM_SCANCODE_59
# Key for toggling the display of the large chat console.
keymap_console (Large chat console) key SYSTEM_SCANCODE_67
keymap_console (Toggle large chat console) key SYSTEM_SCANCODE_67
# Key for toggling the display of fog.
keymap_toggle_fog (Toggle fog) key SYSTEM_SCANCODE_60
# Key for toggling the display of debug info.
keymap_toggle_debug (Toggle debug info) key SYSTEM_SCANCODE_62
# Key for toggling the display of the profiler. Used for development.
@ -297,109 +267,78 @@ keymap_toggle_profiler (Toggle profiler) key SYSTEM_SCANCODE_63
# Key for toggling the display of mapblock boundaries.
keymap_toggle_block_bounds (Toggle block bounds) key
# Key for switching between first- and third-person camera.
keymap_camera_mode (Toggle camera mode) key SYSTEM_SCANCODE_6
# Key for increasing the viewing range.
keymap_increase_viewing_range_min (Increase view range) key SYSTEM_SCANCODE_46
# Key for decreasing the viewing range.
keymap_decrease_viewing_range_min (Decrease view range) key SYSTEM_SCANCODE_45
# Key for selecting the first hotbar slot.
# Modifier key bind for closing your world.
# Requires ESC + the selected key to work.
keymap_close_world (Return to Main Menu) key
keymap_slot1 (Hotbar slot 1) key SYSTEM_SCANCODE_30
# Key for selecting the second hotbar slot.
keymap_slot2 (Hotbar slot 2) key SYSTEM_SCANCODE_31
# Key for selecting the third hotbar slot.
keymap_slot3 (Hotbar slot 3) key SYSTEM_SCANCODE_32
# Key for selecting the fourth hotbar slot.
keymap_slot4 (Hotbar slot 4) key SYSTEM_SCANCODE_33
# Key for selecting the fifth hotbar slot.
keymap_slot5 (Hotbar slot 5) key SYSTEM_SCANCODE_34
# Key for selecting the sixth hotbar slot.
keymap_slot6 (Hotbar slot 6) key SYSTEM_SCANCODE_35
# Key for selecting the seventh hotbar slot.
keymap_slot7 (Hotbar slot 7) key SYSTEM_SCANCODE_36
# Key for selecting the eighth hotbar slot.
keymap_slot8 (Hotbar slot 8) key SYSTEM_SCANCODE_37
# Key for selecting the ninth hotbar slot.
keymap_slot9 (Hotbar slot 9) key SYSTEM_SCANCODE_38
# Key for selecting the tenth hotbar slot.
keymap_slot10 (Hotbar slot 10) key SYSTEM_SCANCODE_39
# Key for selecting the 11th hotbar slot.
keymap_slot11 (Hotbar slot 11) key
# Key for selecting the 12th hotbar slot.
keymap_slot12 (Hotbar slot 12) key
# Key for selecting the 13th hotbar slot.
keymap_slot13 (Hotbar slot 13) key
# Key for selecting the 14th hotbar slot.
keymap_slot14 (Hotbar slot 14) key
# Key for selecting the 15th hotbar slot.
keymap_slot15 (Hotbar slot 15) key
# Key for selecting the 16th hotbar slot.
keymap_slot16 (Hotbar slot 16) key
# Key for selecting the 17th hotbar slot.
keymap_slot17 (Hotbar slot 17) key
# Key for selecting the 18th hotbar slot.
keymap_slot18 (Hotbar slot 18) key
# Key for selecting the 19th hotbar slot.
keymap_slot19 (Hotbar slot 19) key
# Key for selecting the 20th hotbar slot.
keymap_slot20 (Hotbar slot 20) key
# Key for selecting the 21st hotbar slot.
keymap_slot21 (Hotbar slot 21) key
# Key for selecting the 22nd hotbar slot.
keymap_slot22 (Hotbar slot 22) key
# Key for selecting the 23rd hotbar slot.
keymap_slot23 (Hotbar slot 23) key
# Key for selecting the 24th hotbar slot.
keymap_slot24 (Hotbar slot 24) key
# Key for selecting the 25th hotbar slot.
keymap_slot25 (Hotbar slot 25) key
# Key for selecting the 26th hotbar slot.
keymap_slot26 (Hotbar slot 26) key
# Key for selecting the 27th hotbar slot.
keymap_slot27 (Hotbar slot 27) key
# Key for selecting the 28th hotbar slot.
keymap_slot28 (Hotbar slot 28) key
# Key for selecting the 29th hotbar slot.
keymap_slot29 (Hotbar slot 29) key
# Key for selecting the 30th hotbar slot.
keymap_slot30 (Hotbar slot 30) key
# Key for selecting the 31st hotbar slot.
keymap_slot31 (Hotbar slot 31) key
# Key for selecting the 32nd hotbar slot.
keymap_slot32 (Hotbar slot 32) key
[*Touchscreen]
@ -2074,11 +2013,11 @@ transparency_sorting_group_by_buffers (Transparency Sorting Group by Buffers) bo
cloud_radius (Cloud radius) int 12 8 62
# Delay between mesh updates on the client in ms. Increasing this will slow
# down the rate of mesh updates, thus reducing jitter on slower clients.
mesh_generation_interval (Mapblock mesh generation delay) int 0 0 50
# down the rate of mesh updates, which can help reduce jitter.
mesh_generation_interval (Mapblock mesh generation delay) int 0 0 25
# Number of threads to use for mesh generation.
# Value of 0 (default) will let Luanti autodetect the number of available threads.
# Value of 0 (default) will let Luanti automatically choose the number of threads.
mesh_generation_threads (Mapblock mesh generation threads) int 0 0 8
# All mesh buffers with less than this number of vertices will be merged

60
doc/README.md Normal file
View file

@ -0,0 +1,60 @@
# Documentation
This directory contains mostly reference documentation for the Luanti engine.
For a less prescriptive and more guiding documentation, also look at:
https://docs.luanti.org
Note that the inner workings of the engine are not well documented. It's most
often better to read the code.
Markdown files are written in a way that they can also be read in plain text.
When modifying, please keep it that way!
Here is a list with descriptions of relevant files:
## Server Modding
- [lua_api.md](lua_api.md): Server Modding API reference. (Not only the Lua part,
but also file structure and everything else.)
If you want to make a mod or game, look here!
A rendered version is also available at <https://api.luanti.org/>.
- [builtin_entities.md](builtin_entities.md): Doc for entities predefined by the
engine (in builtin), i.e. dropped items and falling nodes.
## Client-Side Content
- [texture_packs.md](texture_packs.md): Layout and description of Luanti's
texture packs structure and configuration.
- [client_lua_api.md](client_lua_api.md): Client-Provided Client-Side Modding
(CPCSM) API reference.
## Mainmenu scripting
- [menu_lua_api.md](menu_lua_api.md): API reference for the mainmenu scripting
environment.
- [fst_api.txt](fst_api.txt): Formspec Toolkit API, included in builtin for the
main menu.
## Formats and Protocols
- [world_format.md](world_format.md): Structure of Luanti world directories and
format of the files therein.
Note: If you want to write your own deserializer, it will be easier to read
the `serialize()` and `deSerialize()` functions of the various structures in
C++, e.g. `MapBlock::deSerialize()`.
- [protocol.txt](protocol.txt): *Rough* outline of Luanti's network protocol.
## Misc.
- [compiling/](compiling/): Compilation instructions, and options.
- [ides/](ides/): Instructions for configuring certain IDEs for engine development.
- [developing/](developing/): Information about Luanti development.
Note: [developing/profiling.md](developing/profiling.md) can be useful for
modders and server owners!
- [android.md](android.md): Android quirks.
- [direction.md](direction.md): Information related to the future direction of
Luanti. Commonly referred to as the roadmap document.
- [breakages.md](breakages.md): List of planned breakages for the next major
release, i.e. 6.0.0.
- [docker_server.md](docker_server.md): Information about our Docker server
images in the ghcr.

View file

@ -42,7 +42,7 @@ configuration file can usually be found at:
* After 5.4.2:
* `/sdcard/Android/data/net.minetest.minetest/` or `/storage/emulated/0/Android/data/net.minetest.minetest/` if stored on the device
* `/storage/emulated/(varying folder name)/Android/data/net.minetest.minetest/` if stored on the SD card
* [Learn more about Android directory](https://wiki.luanti.org/Accessing_Android_Data_Directory)
* [Learn more about Android directory](https://docs.luanti.org/for-players/mobile/)
## Useful settings

View file

@ -1,26 +1,27 @@
# Developer documentation
## Wiki
## Luanti Documentation
Some important development docs are found in the wiki: https://dev.luanti.org/
Some important development docs are found on the docs site: https://docs.luanti.org/
Notable pages:
- [Releasing Luanti](https://dev.luanti.org/Releasing_Luanti)
- [Engine translations](https://dev.luanti.org/Translation#Maintaining_engine_translations)
- [Changelog](https://dev.luanti.org/Changelog)
- [Organisation](https://dev.luanti.org/Organisation)
- [Code style guidelines](https://dev.luanti.org/Code_style_guidelines)
- [Releasing Luanti](https://docs.luanti.org/for-engine-devs/releasing-luanti/)
- [Engine translations](https://docs.luanti.org/for-creators/translation/)
- [Changelog](https://docs.luanti.org/about/changelog/)
- [Organisation](https://docs.luanti.org/for-engine-devs/organization/)
- [Code style guidelines](https://docs.luanti.org/for-engine-devs/code-style-guidelines/)
and [Lua code style guidelines](https://docs.luanti.org/for-engine-devs/lua-code-style-guidelines/)
## In this folder
- [Developing minetestserver with Docker](docker.md)
- [Android tips & tricks](android.md)
- [OS/library compatibility policy](os-compatibility.md)
- [Miscellaneous](misc.md)
- [docker.md](docker.md): Developing minetestserver with Docker
- [android.md](android.md): Android tips & tricks
- [os-compatibility.md](os-compatibility.md): OS/library compatibility policy
- [profiling.md](profiling.md): Profiling instructions
## IRC
Oftentimes knowledge hasn't been written down (yet) and your best bet is to ask someone experienced and/or the core developers.
Feel free to join the [#minetest-dev IRC](https://wiki.luanti.org/IRC) and ask questions related to **engine development**.
Feel free to join the [#luanti-dev IRC](https://docs.luanti.org/about/irc/) and ask questions related to **engine development**.

View file

@ -1,4 +1,4 @@
# Miscellaneous
# Profiling
## Profiling Luanti on Linux with perf

View file

@ -37,7 +37,7 @@ Examples include
[general view distance](https://github.com/luanti-org/luanti/issues/7222).
This includes work on maintaining
[our Irrlicht fork](https://github.com/minetest/irrlicht), and switching to
[our Irrlicht fork](https://github.com/luanti-org/luanti/tree/master/irr), and switching to
alternative libraries to replace Irrlicht functionality as needed
### 2.2 Internal code refactoring

View file

@ -3,8 +3,10 @@ Formspec toolkit api 0.0.3
Formspec toolkit is a set of functions to create basic ui elements.
You can find the files in builtin/fstk/.
File: fst/ui.lua
File: fstk/ui.lua
----------------
ui.lua adds base ui interface to add additional components to.
@ -25,7 +27,7 @@ ui.find_by_name(name) --> returns component or nil
^ find a component within ui
^ name: name of component to look for
File: fst/tabview.lua
File: fstk/tabview.lua
---------------------
tabview_create(name, size, tabheaderpos) --> returns tabview component
@ -92,7 +94,7 @@ methods:
* icon: path to icon
* on_click(tabview): callback function
File: fst/dialog.lua
File: fstk/dialog.lua
---------------------
Only one dialog can be shown at a time. If a dialog is closed it's parent is
gonna be activated and shown again.
@ -129,7 +131,7 @@ members:
- parent
^ parent component to return to on exit
File: fst/buttonbar.lua
File: fstk/buttonbar.lua
-----------------------
buttonbar_create(name, pos, size, bgcolor, cbf_buttonhandler)

View file

@ -10,7 +10,7 @@ safely without breaking backwards compatibility.
* More information at <http://www.luanti.org/>
* Additional documentation: <https://docs.luanti.org/>
* (Unofficial) Minetest Modding Book by rubenwardy: <https://rubenwardy.com/minetest_modding_book/>
* (Unofficial) Luanti Modding Book by rubenwardy: <https://rubenwardy.com/minetest_modding_book/>
* Modding tools: <https://github.com/luanti-org/modtools>
Introduction
@ -315,6 +315,9 @@ due to their space savings.
Bone weights should be normalized, e.g. using ["normalize all" in Blender](https://docs.blender.org/manual/en/4.2/grease_pencil/modes/weight_paint/weights_menu.html#normalize-all).
Note that nodes using matrix transforms must not be animated.
This also extends to bone overrides, which must not be applied to them.
You can use the [Khronos glTF validator](https://github.com/KhronosGroup/glTF-Validator)
to check whether a model is a valid glTF file.
@ -6478,6 +6481,11 @@ Environment access
* `core.get_node_or_nil(pos)`
* Same as `get_node` but returns `nil` for unloaded areas.
* Note that even loaded areas can contain "ignore" nodes.
* `core.get_node_raw(x, y, z)`
* Same as `get_node` but a faster low-level API
* Returns `content_id`, `param1`, `param2`, and `pos_ok`
* The `content_id` can be mapped to a name using `core.get_name_from_content_id()`
* If `pos_ok` is false, the area is unloaded and `content_id == core.CONTENT_IGNORE`
* `core.get_node_light(pos[, timeofday])`
* Gets the light value at the given position. Note that the light value
"inside" the node at the given position is returned, so you usually want
@ -6583,6 +6591,7 @@ Environment access
* `core.get_value_noise(noiseparams)`
* Return world-specific value noise.
* The actual seed used is the noiseparams seed plus the world seed.
* **Important**: Requires the mapgen environment to be initalized, do not use at load time.
* `core.get_value_noise(seeddiff, octaves, persistence, spread)`
* Deprecated: use `core.get_value_noise(noiseparams)` instead.
* `core.get_perlin(noiseparams)`
@ -6657,6 +6666,9 @@ Environment access
of the *active* mapgen setting `"mapgen_limit"`.
* `chunksize` is an optional number. If it is absent, its value is that
of the *active* mapgen setting `"chunksize"`.
* `core.get_mapgen_chunksize()`
* Returns the currently active chunksize of the mapgen, as a vector.
The size is specified in blocks.
* `core.get_mapgen_setting(name)`
* Gets the *active* mapgen setting (or nil if none exists) in string
format with the following order of precedence:
@ -6836,6 +6848,7 @@ You can find mod channels communication scheme in `doc/mod_channels.png`.
* Server joins channel `channel_name`, and creates it if necessary. You
should listen for incoming messages with
`core.register_on_modchannel_message`
* This returns a [ModChannel] object.
Inventory
---------
@ -6867,10 +6880,15 @@ Formspec
* `core.show_formspec(playername, formname, formspec)`
* `playername`: name of player to show formspec
* `formname`: name passed to `on_player_receive_fields` callbacks.
It should follow the `"modname:<whatever>"` naming convention.
* `formname` must not be empty, unless you want to reshow
the inventory formspec without updating it for future opens.
* It should follow the `"modname:<whatever>"` naming convention.
* If empty: Shows a custom, temporary inventory formspec.
* An inventory formspec shown this way will also be updated if
`ObjectRef:set_inventory_formspec` is called.
* Use `ObjectRef:set_inventory_formspec` to change the player's
inventory formspec for future opens.
* Supported if server AND client are both of version >= 5.13.0.
* `formspec`: formspec to display
* See also: `core.register_on_player_receive_fields`
* `core.close_formspec(playername, formname)`
* `playername`: name of player to close formspec
* `formname`: has to exactly match the one given in `show_formspec`, or the
@ -8648,9 +8666,12 @@ child will follow movement and rotation of that bone.
* Returns `nil` if no attribute found.
* `get_meta()`: Returns metadata associated with the player (a PlayerMetaRef).
* `set_inventory_formspec(formspec)`
* Redefine player's inventory form
* Should usually be called in `on_joinplayer`
* Redefines the player's inventory formspec.
* Should usually be called at least once in the `on_joinplayer` callback.
* If `formspec` is `""`, the player's inventory is disabled.
* If the inventory formspec is currently open on the client, it is
updated immediately.
* See also: `core.register_on_player_receive_fields`
* `get_inventory_formspec()`: returns a formspec string
* `set_formspec_prepend(formspec)`:
* the formspec string will be added to every formspec shown to the user,
@ -9270,6 +9291,8 @@ It can be created via `ValueNoise()` or `core.get_value_noise()`.
For `core.get_value_noise()`, the actual seed used is the noiseparams seed
plus the world seed, to create world-specific noise.
**Important**: These require the mapgen environment to be initalized, do not use at load time.
* `ValueNoise(noiseparams)`
* `ValueNoise(seed, octaves, persistence, spread)` (deprecated)
* `core.get_value_noise(noiseparams)`
@ -9306,6 +9329,8 @@ For each of the functions with an optional `buffer` parameter: If `buffer` is
not nil, this table will be used to store the result instead of creating a new
table.
**Important**: These require the mapgen environment to be initalized, do not use at load time.
### Methods
* `get_2d_map(pos)`: returns a `<size.x>` times `<size.y>` 2D array of 2D noise

View file

@ -3,7 +3,8 @@ Updated 2011-06-18
A custom protocol over UDP.
Integers are big endian.
Refer to connection.{h,cpp} for further reference.
Refer to network/mtp/internal.h, network/networkprotocol.{h,cpp}, and
server/clientiface.h for further reference.
Initialization:
- A dummy reliable packet with peer_id=PEER_ID_INEXISTENT=0 is sent to the server:

View file

@ -401,7 +401,7 @@ See below for description.
Luanti will correct lighting in the day light bank when the block at
`(1, 0, 0)` is also loaded.
Timestamp and node ID mappings were introduced in map format version 29.
Timestamp and node ID mappings come here if map format version >= 29.
* `u32` timestamp
* Timestamp when last saved, as seconds from starting the game.
* `0xffffffff` = invalid/unknown timestamp, nothing should be done with the time
@ -482,13 +482,7 @@ Timestamp and node ID mappings were introduced in map format version 29.
* `s32` timeout * 1000
* `s32` elapsed * 1000
* Since map format version 25:
* `u8` length of the data of a single timer (always 2+4+4=10)
* `u16` `num_of_timers`
* foreach `num_of_timers`:
* `u16` timer position (`(z*16*16 + y*16 + x)`)
* `s32` timeout * 1000
* `s32` elapsed * 1000
* Map format version >= 25: see below
`u8` static object version:
* Always 0
@ -516,6 +510,14 @@ Before map format version 29:
* `u16` `name_len`
* `u8[name_len]` `name`
Since map format version 25, node timers come here:
* `u8` length of the data of a single timer (always 2+4+4=10)
* `u16` `num_of_timers`
* foreach `num_of_timers`:
* `u16` timer position (`(z*16*16 + y*16 + x)`)
* `s32` timeout * 1000
* `s32` elapsed * 1000
End of File (EOF).
# Format of Nodes

View file

@ -116,6 +116,7 @@ core.register_chatcommand("bench_bulk_set_node", {
((middle_time - start_time)) / 1000,
((end_time - middle_time)) / 1000
)
print(msg)
return true, msg
end,
})
@ -129,16 +130,13 @@ core.register_chatcommand("bench_bulk_get_node", {
return false, "No player."
end
local pos_list = get_positions_cube(player:get_pos())
local dummy = 0
local function bench()
local start_time = core.get_us_time()
for i = 1, #pos_list do
local n = core.get_node(pos_list[i])
-- Make sure the name lookup is never optimized away.
-- Table allocation might still be omitted. But only accessing
-- the name of a node is a common pattern anyways.
if n.name == "benchmarks:nonexistent_node" then
error("should never happen")
end
-- Make sure the name lookup is not optimized away (can this even happen?)
dummy = dummy + #n.name
end
return core.get_us_time() - start_time
end
@ -151,6 +149,115 @@ core.register_chatcommand("bench_bulk_get_node", {
local msg = string.format("Benchmark results: core.get_node loop 1: %.2f ms",
result_us / 1000)
print(msg)
return true, msg
end,
})
core.register_chatcommand("bench_bulk_get_node_raw", {
params = "",
description = "Benchmark: Bulk-get 99×99×99 nodes with raw API",
func = function(name, param)
local player = core.get_player_by_name(name)
if not player then
return false, "No player."
end
local pos_list = get_positions_cube(player:get_pos())
local dummy = 0
local function bench()
local start_time = core.get_us_time()
for i = 1, #pos_list do
local pos_i = pos_list[i]
local nid = core.get_node_raw(pos_i.x, pos_i.y, pos_i.z)
-- Make sure the result is not optimized away
dummy = dummy + nid
end
return core.get_us_time() - start_time
end
core.chat_send_player(name, "Benchmarking core.get_node_raw. Warming up ...")
bench()
core.chat_send_player(name, "Warming up finished, now benchmarking ...")
local result_us = bench()
local msg = string.format("Benchmark results: core.get_node_raw loop 1: %.2f ms",
result_us / 1000)
print(msg)
return true, msg
end,
})
core.register_chatcommand("bench_bulk_get_node_raw2", {
params = "",
description = "Benchmark: Bulk-get 99×99×99 nodes with raw API and lookup names",
func = function(name, param)
local player = core.get_player_by_name(name)
if not player then
return false, "No player."
end
local pos_list = get_positions_cube(player:get_pos())
local dummy = 0
local function bench()
local start_time = core.get_us_time()
for i = 1, #pos_list do
local pos_i = pos_list[i]
local nid = core.get_node_raw(pos_i.x, pos_i.y, pos_i.z)
local name = core.get_name_from_content_id(nid)
-- Make sure the name lookup is not optimized away
dummy = dummy + #name
end
return core.get_us_time() - start_time
end
core.chat_send_player(name, "Benchmarking core.get_node_raw+get_name_from_content_id. Warming up ...")
bench()
core.chat_send_player(name, "Warming up finished, now benchmarking ...")
local result_us = bench()
local msg = string.format("Benchmark results: core.get_node_raw+get_name_from_content_id loop 1: %.2f ms",
result_us / 1000)
print(msg)
return true, msg
end,
})
core.register_chatcommand("bench_bulk_get_node_vm", {
params = "",
description = "Benchmark: Bulk-get 99×99×99 nodes with voxel manipulator",
func = function(name, param)
local player = core.get_player_by_name(name)
if not player then
return false, "No player."
end
local pos = player:get_pos()
local dummy = 0
local function bench()
local start_time = core.get_us_time()
local vm = core.get_voxel_manip(pos:offset(1,1,1), pos:offset(100,100,100))
local data = vm:get_data()
local mid_time = core.get_us_time()
-- Note that the VManip will actually retrieve more than just the 100³ nodes
-- and also we don't need to iterate pos_list here, so it's not an entirely
-- fair comparison.
for i = 1, 99*99*99 do
local nid = data[i]
-- Make sure the table lookup is not optimized away
dummy = dummy + nid
end
return core.get_us_time() - start_time, mid_time - start_time
end
core.chat_send_player(name, "Benchmarking core.get_voxel_vmanip+get_data+loop . Warming up ...")
bench()
core.chat_send_player(name, "Warming up finished, now benchmarking ...")
local result_us, get_data_us = bench()
local msg = string.format("Benchmark results: core.get_voxel_vmanip+get_data+loop loop 1: %.2f ms of which get_data() %.2f ms",
result_us / 1000, get_data_us / 1000)
print(msg)
return true, msg
end,
})
@ -184,6 +291,7 @@ core.register_chatcommand("bench_bulk_swap_node", {
((middle_time - start_time)) / 1000,
((end_time - middle_time)) / 1000
)
print(msg)
return true, msg
end,
})

View file

@ -13,3 +13,9 @@ Jordach (CC BY-SA 3.0):
Zeg9 (CC BY-SA 3.0):
testentities_lava_flan.x
testentities_lava_flan.png
"Cool Guy":
hecks (refer to irr/LICENSE):
testentities_cool_guy.x
testentities_cool_guy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

View file

@ -102,6 +102,19 @@ core.register_entity("testentities:lava_flan", {
end,
})
core.register_entity("testentities:cool_guy", {
initial_properties = {
visual = "mesh",
mesh = "testentities_cool_guy.x",
textures = {
"testentities_cool_guy.png"
},
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 29}, 30, 0, true)
end,
})
-- Advanced visual tests
-- An entity for testing animated and yaw-modulated sprites

View file

@ -29,6 +29,6 @@ if core.ipc_cas("unittests:mg_once", nil, true) then
end
core.register_on_generated(function(vm, pos1, pos2, blockseed)
local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1
assert(pos2:subtract(pos1) == vector.new(n, n, n))
local cs = core.get_mapgen_chunksize()
assert(pos2:subtract(pos1) == cs:multiply(core.MAP_BLOCKSIZE):subtract(1))
end)

View file

@ -1,9 +1,9 @@
IrrlichtMt version 1.9
======================
IrrlichtMt is the 3D engine of [Minetest](https://github.com/minetest).
IrrlichtMt is the 3D engine of [Luanti](https://github.com/luanti-org).
It is based on the [Irrlicht Engine](https://irrlicht.sourceforge.io/) but is now developed independently.
It is intentionally not compatible to upstream and is planned to be eventually absorbed into Minetest.
It is intentionally not compatible to upstream and is planned to be eventually absorbed into Luanti.
Build
-----

View file

@ -13,8 +13,8 @@ namespace scene
//! Interface for an animated mesh.
/** There are already simple implementations of this interface available so
you don't have to implement this interface on your own if you need to:
You might want to use irr::scene::SAnimatedMesh, irr::scene::SMesh,
irr::scene::SMeshBuffer etc. */
You might want to use irr::scene::SMesh, irr::scene::SMeshBuffer etc.
*/
class IAnimatedMesh : public IMesh
{
public:
@ -34,22 +34,8 @@ public:
scene node the mesh is instantiated in.*/
virtual void setAnimationSpeed(f32 fps) = 0;
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number, >= 0, <= getMaxFrameNumber()
Linear interpolation is used if this is between two frames.
\return Returns the animated mesh for the given frame */
virtual IMesh *getMesh(f32 frame) = 0;
//! Returns the type of the animated mesh.
/** In most cases it is not necessary to use this method.
This is useful for making a safe downcast. For example,
if getMeshType() returns EAMT_MD2 it's safe to cast the
IAnimatedMesh to IAnimatedMeshMD2.
\returns Type of the mesh. */
E_ANIMATED_MESH_TYPE getMeshType() const override
{
return EAMT_UNKNOWN;
}
//! Returns the type of the animated mesh. Useful for safe downcasts.
E_ANIMATED_MESH_TYPE getMeshType() const = 0;
};
} // end namespace scene

View file

@ -12,35 +12,8 @@ namespace irr
{
namespace scene
{
enum E_JOINT_UPDATE_ON_RENDER
{
//! do nothing
EJUOR_NONE = 0,
//! get joints positions from the mesh (for attached nodes, etc)
EJUOR_READ,
//! control joint positions in the mesh (eg. ragdolls, or set the animation from animateJoints() )
EJUOR_CONTROL
};
class IAnimatedMeshSceneNode;
//! Callback interface for catching events of ended animations.
/** Implement this interface and use
IAnimatedMeshSceneNode::setAnimationEndCallback to be able to
be notified if an animation playback has ended.
**/
class IAnimationEndCallBack : public virtual IReferenceCounted
{
public:
//! Will be called when the animation playback has ended.
/** See IAnimatedMeshSceneNode::setAnimationEndCallback for
more information.
\param node: Node of which the animation has ended. */
virtual void OnAnimationEnd(IAnimatedMeshSceneNode *node) = 0;
};
//! Scene node capable of displaying an animated mesh.
class IAnimatedMeshSceneNode : public ISceneNode
{
@ -120,11 +93,10 @@ public:
/** When true the animations are played looped */
virtual bool getLoopMode() const = 0;
//! Sets a callback interface which will be called if an animation playback has ended.
/** Set this to 0 to disable the callback again.
Please note that this will only be called when in non looped
mode, see IAnimatedMeshSceneNode::setLoopMode(). */
virtual void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) = 0;
//! Will be called right after the joints have been animated,
//! but before the transforms have been propagated recursively to children.
virtual void setOnAnimateCallback(
const std::function<void(f32 dtime)> &cb) = 0;
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style.
/** In this way it is possible to change the materials a mesh
@ -139,20 +111,15 @@ public:
virtual void setMesh(IAnimatedMesh *mesh) = 0;
//! Returns the current mesh
virtual IAnimatedMesh *getMesh(void) = 0;
//! Set how the joints should be updated on render
virtual void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) = 0;
virtual IAnimatedMesh *getMesh() = 0;
//! Sets the transition time in seconds
/** Note: This needs to enable joints, and setJointmode set to
EJUOR_CONTROL. You must call animateJoints(), or the mesh will
not animate. */
/** Note: You must call animateJoints(), or the mesh will not animate. */
virtual void setTransitionTime(f32 Time) = 0;
//! animates the joints in the mesh based on the current frame.
/** Also takes in to account transitions. */
virtual void animateJoints(bool CalculateAbsolutePositions = true) = 0;
virtual void animateJoints() = 0;
//! render mesh ignoring its transformation.
/** Culling is unaffected. */

View file

@ -11,85 +11,41 @@ namespace irr
namespace scene
{
//! Enumeration for different bone animation modes
enum E_BONE_ANIMATION_MODE
{
//! The bone is usually animated, unless it's parent is not animated
EBAM_AUTOMATIC = 0,
//! The bone is animated by the skin, if it's parent is not animated then animation will resume from this bone onward
EBAM_ANIMATED,
//! The bone is not animated by the skin
EBAM_UNANIMATED,
//! Not an animation mode, just here to count the available modes
EBAM_COUNT
};
enum E_BONE_SKINNING_SPACE
{
//! local skinning, standard
EBSS_LOCAL = 0,
//! global skinning
EBSS_GLOBAL,
EBSS_COUNT
};
//! Names for bone animation modes
const c8 *const BoneAnimationModeNames[] = {
"automatic",
"animated",
"unanimated",
0,
};
//! Interface for bones used for skeletal animation.
/** Used with SkinnedMesh and IAnimatedMeshSceneNode. */
class IBoneSceneNode : public ISceneNode
{
public:
IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1) :
ISceneNode(parent, mgr, id), positionHint(-1), scaleHint(-1), rotationHint(-1) {}
IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr,
s32 id = -1, u32 boneIndex = 0,
const std::optional<std::string> &boneName = std::nullopt)
:
ISceneNode(parent, mgr, id),
BoneIndex(boneIndex)
{
setName(boneName);
}
//! Get the index of the bone
virtual u32 getBoneIndex() const = 0;
//! Returns the index of the bone
u32 getBoneIndex() const
{
return BoneIndex;
}
//! Sets the animation mode of the bone.
/** \return True if successful. (Unused) */
virtual bool setAnimationMode(E_BONE_ANIMATION_MODE mode) = 0;
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override
{
return Box;
}
//! Gets the current animation mode of the bone
virtual E_BONE_ANIMATION_MODE getAnimationMode() const = 0;
const u32 BoneIndex;
//! Get the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override = 0;
//! Returns the relative transformation of the scene node.
// virtual core::matrix4 getRelativeTransformation() const = 0;
//! The animation method.
void OnAnimate(u32 timeMs) override = 0;
// Bogus box; bone scene nodes are not rendered anyways.
static constexpr core::aabbox3d<f32> Box = {{0, 0, 0}};
//! The render method.
/** Does nothing as bones are not visible. */
void render() override {}
//! How the relative transformation of the bone is used
virtual void setSkinningSpace(E_BONE_SKINNING_SPACE space) = 0;
//! How the relative transformation of the bone is used
virtual E_BONE_SKINNING_SPACE getSkinningSpace() const = 0;
//! Updates the absolute position based on the relative and the parents position
virtual void updateAbsolutePositionOfAllChildren() = 0;
s32 positionHint;
s32 scaleHint;
s32 rotationHint;
};
} // end namespace scene

View file

@ -20,38 +20,6 @@ enum E_ANIMATED_MESH_TYPE
//! Unknown animated mesh type.
EAMT_UNKNOWN = 0,
//! Quake 2 MD2 model file
EAMT_MD2,
//! Quake 3 MD3 model file
EAMT_MD3,
//! Maya .obj static model
EAMT_OBJ,
//! Quake 3 .bsp static Map
EAMT_BSP,
//! 3D Studio .3ds file
EAMT_3DS,
//! My3D Mesh, the file format by Zhuck Dimitry
EAMT_MY3D,
//! Pulsar LMTools .lmts file. This Irrlicht loader was written by Jonas Petersen
EAMT_LMTS,
//! Cartography Shop .csm file. This loader was created by Saurav Mohapatra.
EAMT_CSM,
//! .oct file for Paul Nette's FSRad or from Murphy McCauley's Blender .oct exporter.
/** The oct file format contains 3D geometry and lightmaps and
can be loaded directly by Irrlicht */
EAMT_OCT,
//! Halflife MDL model file
EAMT_MDL_HALFLIFE,
//! generic skinned mesh
EAMT_SKINNED,
@ -119,9 +87,7 @@ public:
virtual void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) = 0;
//! Returns the type of the meshes.
/** This is useful for making a safe downcast. For example,
if getMeshType() returns EAMT_MD2 it's safe to cast the
IMesh to IAnimatedMeshMD2.
/** This is useful for making a safe downcast.
Note: It's no longer just about animated meshes, that name has just historical reasons.
\returns Type of the mesh */
virtual E_ANIMATED_MESH_TYPE getMeshType() const

View file

@ -66,26 +66,6 @@ public:
IReferenceCounted::drop() for more information. */
virtual SMesh *createMeshCopy(IMesh *mesh) const = 0;
//! Get amount of polygons in mesh.
/** \param mesh Input mesh
\return Number of polygons in mesh. */
virtual s32 getPolyCount(IMesh *mesh) const = 0;
//! Get amount of polygons in mesh.
/** \param mesh Input mesh
\return Number of polygons in mesh. */
virtual s32 getPolyCount(IAnimatedMesh *mesh) const = 0;
//! Create a new AnimatedMesh and adds the mesh to it
/** \param mesh Input mesh
\param type The type of the animated mesh to create.
\return Newly created animated mesh with mesh as its only
content. When you don't need the animated mesh anymore, you
should call IAnimatedMesh::drop(). See
IReferenceCounted::drop() for more information. */
virtual IAnimatedMesh *createAnimatedMesh(IMesh *mesh,
scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) const = 0;
//! Apply a manipulator on the Meshbuffer
/** \param func A functor defining the mesh manipulation.
\param buffer The Meshbuffer to apply the manipulator to.

View file

@ -32,7 +32,7 @@ public:
//! Get the currently defined mesh for display.
/** \return Pointer to mesh which is displayed by this node. */
virtual IMesh *getMesh(void) = 0;
virtual IMesh *getMesh() = 0;
//! Sets if the scene node should not copy the materials of the mesh but use them directly.
/** In this way it is possible to change the materials of a mesh

View file

@ -94,16 +94,12 @@ public:
\param timeMs Current time in milliseconds. */
virtual void OnAnimate(u32 timeMs)
{
if (IsVisible) {
// update absolute position
if (!IsVisible && Children.empty())
return;
updateAbsolutePosition();
// perform the post render process on all children
ISceneNodeList::iterator it = Children.begin();
for (; it != Children.end(); ++it)
(*it)->OnAnimate(timeMs);
}
for (auto *child : Children)
child->OnAnimate(timeMs);
}
//! Renders the node.

View file

@ -1,167 +0,0 @@
// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h
#pragma once
#include <vector>
#include "IAnimatedMesh.h"
#include "IMesh.h"
#include "aabbox3d.h"
namespace irr
{
namespace scene
{
//! Simple implementation of the IAnimatedMesh interface.
struct SAnimatedMesh final : public IAnimatedMesh
{
//! constructor
SAnimatedMesh(scene::IMesh *mesh = 0, scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) :
IAnimatedMesh(), FramesPerSecond(25.f), Type(type)
{
addMesh(mesh);
recalculateBoundingBox();
}
//! destructor
virtual ~SAnimatedMesh()
{
// drop meshes
for (auto *mesh : Meshes)
mesh->drop();
}
f32 getMaxFrameNumber() const override
{
return static_cast<f32>(Meshes.size() - 1);
}
//! Gets the default animation speed of the animated mesh.
/** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */
f32 getAnimationSpeed() const override
{
return FramesPerSecond;
}
//! Gets the frame count of the animated mesh.
/** \param fps Frames per second to play the animation with. If the amount is 0, it is not animated.
The actual speed is set in the scene node the mesh is instantiated in.*/
void setAnimationSpeed(f32 fps) override
{
FramesPerSecond = fps;
}
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index.
\return The animated mesh based for the given frame */
IMesh *getMesh(f32 frame) override
{
if (Meshes.empty())
return nullptr;
return Meshes[static_cast<s32>(frame)];
}
//! adds a Mesh
void addMesh(IMesh *mesh)
{
if (mesh) {
mesh->grab();
Meshes.push_back(mesh);
}
}
//! Returns an axis aligned bounding box of the mesh.
/** \return A bounding box of this mesh is returned. */
const core::aabbox3d<f32> &getBoundingBox() const override
{
return Box;
}
//! set user axis aligned bounding box
void setBoundingBox(const core::aabbox3df &box) override
{
Box = box;
}
//! Recalculates the bounding box.
void recalculateBoundingBox()
{
Box.reset(0, 0, 0);
if (Meshes.empty())
return;
Box = Meshes[0]->getBoundingBox();
for (u32 i = 1; i < Meshes.size(); ++i)
Box.addInternalBox(Meshes[i]->getBoundingBox());
}
//! Returns the type of the animated mesh.
E_ANIMATED_MESH_TYPE getMeshType() const override
{
return Type;
}
//! returns amount of mesh buffers.
u32 getMeshBufferCount() const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBufferCount();
}
//! returns pointer to a mesh buffer
IMeshBuffer *getMeshBuffer(u32 nr) const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBuffer(nr);
}
//! Returns pointer to a mesh buffer which fits a material
/** \param material: material to search for
\return Returns the pointer to the mesh buffer or
NULL if there is no such mesh buffer. */
IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBuffer(material);
}
//! set the hardware mapping hint, for driver
void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override
{
for (u32 i = 0; i < Meshes.size(); ++i)
Meshes[i]->setHardwareMappingHint(newMappingHint, buffer);
}
//! flags the meshbuffer as changed, reloads hardware buffers
void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override
{
for (u32 i = 0; i < Meshes.size(); ++i)
Meshes[i]->setDirty(buffer);
}
//! All meshes defining the animated mesh
std::vector<IMesh *> Meshes;
//! The bounding box of this mesh
core::aabbox3d<f32> Box{{0.0f, 0.0f, 0.0f}};
//! Default animation speed of this mesh.
f32 FramesPerSecond;
//! The type of the mesh.
E_ANIMATED_MESH_TYPE Type;
};
} // end namespace scene
} // end namespace irr

View file

@ -5,7 +5,7 @@
#pragma once
#include <vector>
#include "IMesh.h"
#include "IAnimatedMesh.h"
#include "IMeshBuffer.h"
#include "aabbox3d.h"
@ -14,7 +14,7 @@ namespace irr
namespace scene
{
//! Simple implementation of the IMesh interface.
struct SMesh final : public IMesh
struct SMesh final : public IAnimatedMesh
{
//! constructor
SMesh() {}
@ -134,6 +134,15 @@ struct SMesh final : public IMesh
//! The bounding box of this mesh
core::aabbox3d<f32> BoundingBox{{0, 0, 0}};
// Implement animated mesh interface as a static mesh.
// Slightly hacky: Eventually should be consolidated with SSkinnedMesh,
// with all the animation-related parts behind an optional.
virtual f32 getMaxFrameNumber() const override { return 0.0f; }
virtual f32 getAnimationSpeed() const override { return 0.0f; }
virtual void setAnimationSpeed(f32 fps) override {}
E_ANIMATED_MESH_TYPE getMeshType() const override { return EAMT_STATIC; }
};
} // end namespace scene

View file

@ -8,11 +8,18 @@
#include "ISceneManager.h"
#include "SMeshBuffer.h"
#include "SSkinMeshBuffer.h"
#include "aabbox3d.h"
#include "irrMath.h"
#include "irrTypes.h"
#include "matrix4.h"
#include "quaternion.h"
#include "vector3d.h"
#include "Transform.h"
#include <optional>
#include <string>
#include <variant>
#include <vector>
namespace irr
{
@ -37,9 +44,8 @@ public:
//! constructor
SkinnedMesh(SourceFormat src_format) :
EndFrame(0.f), FramesPerSecond(25.f),
LastAnimatedFrame(-1), SkinnedLastFrame(false),
HasAnimation(false), PreparedForSkinning(false),
AnimateNormals(true), HardwareSkinning(false),
AnimateNormals(true),
SrcFormat(src_format)
{
SkinningBuffers = &LocalBuffers;
@ -64,14 +70,12 @@ public:
The actual speed is set in the scene node the mesh is instantiated in.*/
void setAnimationSpeed(f32 fps) override;
//! returns the animated mesh for the given frame
IMesh *getMesh(f32) override;
//! Turns the given array of local matrices into an array of global matrices
//! by multiplying with respective parent matrices.
void calculateGlobalMatrices(std::vector<core::matrix4> &matrices) const;
//! Animates joints based on frame input
void animateMesh(f32 frame);
//! Performs a software skin on this mesh based of joint positions
void skinMesh();
//! Performs a software skin on this mesh based on the given joint matrices
void skinMesh(const std::vector<core::matrix4> &animated_transforms);
//! returns amount of mesh buffers.
u32 getMeshBufferCount() const override;
@ -89,14 +93,15 @@ public:
void setTextureSlot(u32 meshbufNr, u32 textureSlot);
//! returns an axis aligned bounding box
//! Returns bounding box of the mesh *in static pose*.
const core::aabbox3d<f32> &getBoundingBox() const override {
return BoundingBox;
// TODO ideally we shouldn't be forced to implement this
return StaticPoseBox;
}
//! set user axis aligned bounding box
//! Set bounding box of the mesh *in static pose*.
void setBoundingBox(const core::aabbox3df &box) override {
BoundingBox = box;
StaticPoseBox = box;
}
//! set the hardware mapping hint, for driver
@ -140,28 +145,15 @@ public:
return !HasAnimation;
}
//! Allows to enable hardware skinning.
/* This feature is not implemented in Irrlicht yet */
bool setHardwareSkinning(bool on);
//! Refreshes vertex data cached in joints such as positions and normals
void refreshJointCache();
//! Moves the mesh into static position.
void resetAnimation();
void updateBoundingBox();
//! Recovers the joints from the mesh
void recoverJointsFromMesh(std::vector<IBoneSceneNode *> &jointChildSceneNodes);
//! Transfers the joint data to the mesh
void transferJointsToMesh(const std::vector<IBoneSceneNode *> &jointChildSceneNodes);
//! Creates an array of joints from this mesh as children of node
void addJoints(std::vector<IBoneSceneNode *> &jointChildSceneNodes,
IAnimatedMeshSceneNode *node,
ISceneManager *smgr);
std::vector<IBoneSceneNode *> addJoints(
IAnimatedMeshSceneNode *node, ISceneManager *smgr);
//! A vertex weight
struct SWeight
@ -236,7 +228,7 @@ public:
static core::quaternion interpolateValue(core::quaternion from, core::quaternion to, f32 time) {
core::quaternion result;
result.slerp(from, to, time, 0.001f);
result.slerp(from, to, time);
return result;
}
@ -288,15 +280,14 @@ public:
});
}
void updateTransform(f32 frame,
core::vector3df &t, core::quaternion &r, core::vector3df &s) const
void updateTransform(f32 frame, core::Transform &transform) const
{
if (auto pos = position.get(frame))
t = *pos;
transform.translation = *pos;
if (auto rot = rotation.get(frame))
r = *rot;
transform.rotation = *rot;
if (auto scl = scale.get(frame))
s = *scl;
transform.scale = *scl;
}
void cleanup() {
@ -309,16 +300,34 @@ public:
//! Joints
struct SJoint
{
SJoint() : GlobalSkinningSpace(false) {}
SJoint() {}
//! The name of this joint
std::optional<std::string> Name;
//! Local matrix of this joint
core::matrix4 LocalMatrix;
//! Local transformation to be set by loaders. Mutated by animation.
using VariantTransform = std::variant<core::Transform, core::matrix4>;
VariantTransform transform{core::Transform{}};
VariantTransform animate(f32 frame) const {
if (keys.empty())
return transform;
if (std::holds_alternative<core::matrix4>(transform)) {
// .x lets animations override matrix transforms entirely,
// which is what we implement here.
// .gltf does not allow animation of nodes using matrix transforms.
// Note that a decomposition into a TRS transform need not exist!
core::Transform trs;
keys.updateTransform(frame, trs);
return {trs};
}
auto trs = std::get<core::Transform>(transform);
keys.updateTransform(frame, trs);
return {trs};
}
//! List of child joints
std::vector<SJoint *> Children;
//! List of attached meshes
std::vector<u32> AttachedMeshes;
@ -329,42 +338,49 @@ public:
//! Skin weights
std::vector<SWeight> Weights;
//! Bounding box of all affected vertices, in local space
core::aabbox3df LocalBoundingBox{{0, 0, 0}};
//! Unnecessary for loaders, will be overwritten on finalize
core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data.
core::matrix4 GlobalAnimatedMatrix;
core::matrix4 LocalAnimatedMatrix;
//! These should be set by loaders.
core::vector3df Animatedposition;
core::vector3df Animatedscale;
core::quaternion Animatedrotation;
// The .x and .gltf formats pre-calculate this
std::optional<core::matrix4> GlobalInversedMatrix;
private:
//! Internal members used by SkinnedMesh
friend class SkinnedMesh;
bool GlobalSkinningSpace;
void setParent(SJoint *parent) {
ParentJointID = parent ? parent->JointID : std::optional<u16>{};
}
u16 JointID; // TODO refactor away: pointers -> IDs (problem: .x loader abuses SJoint)
std::optional<u16> ParentJointID;
};
//! Animates joints based on frame input
std::vector<SJoint::VariantTransform> animateMesh(f32 frame);
//! Calculates a bounding box given an animation in the form of global joint transforms.
core::aabbox3df calculateBoundingBox(
const std::vector<core::matrix4> &global_transforms);
void recalculateBaseBoundingBoxes();
const std::vector<SJoint *> &getAllJoints() const {
return AllJoints;
}
protected:
void checkForAnimation();
bool checkForAnimation() const;
void topoSortJoints();
void prepareForSkinning();
void calculateStaticBoundingBox();
void calculateJointBoundingBoxes();
void calculateBufferBoundingBoxes();
void normalizeWeights();
void buildAllLocalAnimatedMatrices();
void buildAllGlobalAnimatedMatrices(SJoint *Joint = 0, SJoint *ParentJoint = 0);
void calculateGlobalMatrices(SJoint *Joint, SJoint *ParentJoint);
void skinJoint(SJoint *Joint, SJoint *ParentJoint);
void calculateTangents(core::vector3df &normal,
core::vector3df &tangent, core::vector3df &binormal,
const core::vector3df &vt1, const core::vector3df &vt2, const core::vector3df &vt3,
@ -376,25 +392,25 @@ protected:
//! Mapping from meshbuffer number to bindable texture slot
std::vector<u32> TextureSlots;
//! Joints, topologically sorted (parents come before their children).
std::vector<SJoint *> AllJoints;
std::vector<SJoint *> RootJoints;
// bool can't be used here because std::vector<bool>
// doesn't allow taking a reference to individual elements.
std::vector<std::vector<char>> Vertices_Moved;
core::aabbox3d<f32> BoundingBox{{0, 0, 0}};
//! Bounding box of just the static parts of the mesh
core::aabbox3df StaticPartsBox{{0, 0, 0}};
//! Bounding box of the mesh in static pose
core::aabbox3df StaticPoseBox{{0, 0, 0}};
f32 EndFrame;
f32 FramesPerSecond;
f32 LastAnimatedFrame;
bool SkinnedLastFrame;
bool HasAnimation;
bool PreparedForSkinning;
bool AnimateNormals;
bool HardwareSkinning;
SourceFormat SrcFormat;
};

42
irr/include/Transform.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include "irrMath.h"
#include <matrix4.h>
#include <vector3d.h>
#include <quaternion.h>
namespace irr
{
namespace core
{
struct Transform {
vector3df translation;
quaternion rotation;
vector3df scale{1};
Transform interpolate(Transform to, f32 time) const
{
core::quaternion interpolated_rotation;
interpolated_rotation.slerp(rotation, to.rotation, time);
return {
to.translation.getInterpolated(translation, time),
interpolated_rotation,
to.scale.getInterpolated(scale, time),
};
}
matrix4 buildMatrix() const
{
matrix4 T;
T.setTranslation(translation);
matrix4 R;
rotation.getMatrix_transposed(R);
matrix4 S;
S.setScale(scale);
return T * R * S;
}
};
} // end namespace core
} // end namespace irr

View file

@ -180,7 +180,7 @@ public:
linear interpolation.
*/
quaternion &slerp(quaternion q1, quaternion q2,
f32 time, f32 threshold = .05f);
f32 time, f32 threshold = .001f);
//! Set this quaternion to represent a rotation from angle and axis.
/** Axis must be unit length.

View file

@ -3,9 +3,13 @@
// For conditions of distribution and use, see copyright notice in irrlicht.h
#include "CAnimatedMeshSceneNode.h"
#include "CBoneSceneNode.h"
#include "IVideoDriver.h"
#include "ISceneManager.h"
#include "S3DVertex.h"
#include "Transform.h"
#include "irrTypes.h"
#include "matrix4.h"
#include "os.h"
#include "SkinnedMesh.h"
#include "IDummyTransformationSceneNode.h"
@ -17,6 +21,9 @@
#include "IFileSystem.h"
#include "quaternion.h"
#include <algorithm>
#include <cstddef>
#include <optional>
#include <cassert>
namespace irr
{
@ -30,13 +37,13 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh,
const core::vector3df &rotation,
const core::vector3df &scale) :
IAnimatedMeshSceneNode(parent, mgr, id, position, rotation, scale),
Mesh(0),
Mesh(nullptr),
StartFrame(0), EndFrame(0), FramesPerSecond(0.025f),
CurrentFrameNr(0.f), LastTimeMs(0),
TransitionTime(0), Transiting(0.f), TransitingBlend(0.f),
JointMode(EJUOR_NONE), JointsUsed(false),
JointsUsed(false),
Looping(true), ReadOnlyMaterials(false), RenderFromIdentity(false),
LoopCallBack(0), PassCount(0)
PassCount(0)
{
setMesh(mesh);
}
@ -44,8 +51,6 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh,
//! destructor
CAnimatedMeshSceneNode::~CAnimatedMeshSceneNode()
{
if (LoopCallBack)
LoopCallBack->drop();
if (Mesh)
Mesh->drop();
}
@ -87,8 +92,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame)
CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame);
} else // backwards...
{
} else { // backwards...
if (CurrentFrameNr < StartFrame)
CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame);
}
@ -97,18 +101,9 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
CurrentFrameNr += timeMs * FramesPerSecond;
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame) {
CurrentFrameNr = EndFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
} else // backwards...
{
if (CurrentFrameNr < StartFrame) {
CurrentFrameNr = StartFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
CurrentFrameNr = std::min(CurrentFrameNr, EndFrame);
} else { // backwards...
CurrentFrameNr = std::max(CurrentFrameNr, StartFrame);
}
}
}
@ -156,39 +151,19 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode()
IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame()
{
if (Mesh->getMeshType() != EAMT_SKINNED) {
return Mesh->getMesh(getFrameNr());
} else {
return Mesh;
}
// As multiple scene nodes may be sharing the same skinned mesh, we have to
// re-animate it every frame to ensure that this node gets the mesh that it needs.
SkinnedMesh *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
auto *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
if (JointMode == EJUOR_CONTROL) // write to mesh
skinnedMesh->transferJointsToMesh(JointChildSceneNodes);
else
skinnedMesh->animateMesh(getFrameNr());
// Update the skinned mesh for the current joint transforms.
skinnedMesh->skinMesh();
if (JointMode == EJUOR_READ) { // read from mesh
skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes);
//---slow---
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n)
if (JointChildSceneNodes[n]->getParent() == this) {
JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option
}
}
if (JointMode == EJUOR_CONTROL) {
// For meshes other than EJUOR_CONTROL, this is done by calling animateMesh()
skinnedMesh->updateBoundingBox();
}
// Matrices have already been calculated in OnAnimate
skinnedMesh->skinMesh(PerJoint.GlobalMatrices);
return skinnedMesh;
}
}
//! OnAnimate() is called just before rendering the whole scene.
void CAnimatedMeshSceneNode::OnAnimate(u32 timeMs)
@ -201,7 +176,28 @@ void CAnimatedMeshSceneNode::OnAnimate(u32 timeMs)
buildFrameNr(timeMs - LastTimeMs);
LastTimeMs = timeMs;
// This needs to be done on animate, which is called recursively *before*
// anything is rendered so that the transformations of children are up to date
animateJoints();
// Copy old transforms *before* bone overrides have been applied.
// TODO if there are no bone overrides or no animation blending, this is unnecessary.
copyOldTransforms();
if (OnAnimateCallback)
OnAnimateCallback(timeMs / 1000.0f);
IAnimatedMeshSceneNode::OnAnimate(timeMs);
if (auto *skinnedMesh = dynamic_cast<SkinnedMesh*>(Mesh)) {
for (u16 i = 0; i < PerJoint.SceneNodes.size(); ++i)
PerJoint.GlobalMatrices[i] = PerJoint.SceneNodes[i]->getRelativeTransformation();
assert(PerJoint.GlobalMatrices.size() == skinnedMesh->getJointCount());
skinnedMesh->calculateGlobalMatrices(PerJoint.GlobalMatrices);
Box = skinnedMesh->calculateBoundingBox(PerJoint.GlobalMatrices);
} else {
Box = Mesh->getBoundingBox();
}
}
//! renders the node.
@ -218,15 +214,7 @@ void CAnimatedMeshSceneNode::render()
++PassCount;
scene::IMesh *m = getMeshForCurrentFrame();
if (m) {
Box = m->getBoundingBox();
} else {
#ifdef _DEBUG
os::Printer::log("Animated Mesh returned no mesh to render.", ELL_WARNING);
#endif
return;
}
assert(m);
driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
@ -294,11 +282,12 @@ void CAnimatedMeshSceneNode::render()
if (DebugDataVisible & scene::EDS_SKELETON) {
if (Mesh->getMeshType() == EAMT_SKINNED) {
// draw skeleton
for (auto *joint : ((SkinnedMesh *)Mesh)->getAllJoints()) {
for (const auto *childJoint : joint->Children) {
driver->draw3DLine(joint->GlobalAnimatedMatrix.getTranslation(),
childJoint->GlobalAnimatedMatrix.getTranslation(),
const auto &joints = (static_cast<SkinnedMesh *>(Mesh))->getAllJoints();
for (u16 i = 0; i < PerJoint.GlobalMatrices.size(); ++i) {
const auto translation = PerJoint.GlobalMatrices[i].getTranslation();
if (auto pjid = joints[i]->ParentJointID) {
const auto parent_translation = PerJoint.GlobalMatrices[*pjid].getTranslation();
driver->draw3DLine(parent_translation, translation,
video::SColor(255, 51, 66, 255));
}
}
@ -407,12 +396,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(const c8 *jointName)
return 0;
}
if (JointChildSceneNodes.size() <= *number) {
if (PerJoint.SceneNodes.size() <= *number) {
os::Printer::log("Joint was found in mesh, but is not loaded into node", jointName, ELL_WARNING);
return 0;
}
return JointChildSceneNodes[*number];
return PerJoint.SceneNodes[*number];
}
//! Returns a pointer to a child node, which has the same transformation as
@ -426,12 +415,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(u32 jointID)
checkJoints();
if (JointChildSceneNodes.size() <= jointID) {
if (PerJoint.SceneNodes.size() <= jointID) {
os::Printer::log("Joint not loaded into node", ELL_WARNING);
return 0;
}
return JointChildSceneNodes[jointID];
return PerJoint.SceneNodes[jointID];
}
//! Gets joint count.
@ -452,9 +441,9 @@ bool CAnimatedMeshSceneNode::removeChild(ISceneNode *child)
{
if (ISceneNode::removeChild(child)) {
if (JointsUsed) { // stop weird bugs caused while changing parents as the joints are being created
for (u32 i = 0; i < JointChildSceneNodes.size(); ++i) {
if (JointChildSceneNodes[i] == child) {
JointChildSceneNodes[i] = 0; // remove link to child
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (PerJoint.SceneNodes[i] == child) {
PerJoint.SceneNodes[i] = 0; // remove link to child
break;
}
}
@ -478,22 +467,6 @@ bool CAnimatedMeshSceneNode::getLoopMode() const
return Looping;
}
//! Sets a callback interface which will be called if an animation
//! playback has ended. Set this to 0 to disable the callback again.
void CAnimatedMeshSceneNode::setAnimationEndCallback(IAnimationEndCallBack *callback)
{
if (callback == LoopCallBack)
return;
if (LoopCallBack)
LoopCallBack->drop();
LoopCallBack = callback;
if (LoopCallBack)
LoopCallBack->grab();
}
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style.
void CAnimatedMeshSceneNode::setReadOnlyMaterials(bool readonly)
{
@ -525,19 +498,16 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get materials and bounding box
Box = Mesh->getBoundingBox();
IMesh *m = Mesh->getMesh(0);
if (m) {
Materials.clear();
Materials.reallocate(m->getMeshBufferCount());
Materials.reallocate(Mesh->getMeshBufferCount());
for (u32 i = 0; i < m->getMeshBufferCount(); ++i) {
IMeshBuffer *mb = m->getMeshBuffer(i);
for (u32 i = 0; i < Mesh->getMeshBufferCount(); ++i) {
IMeshBuffer *mb = Mesh->getMeshBuffer(i);
if (mb)
Materials.push_back(mb->getMaterial());
else
Materials.push_back(video::SMaterial());
}
}
// clean up joint nodes
if (JointsUsed) {
@ -556,14 +526,7 @@ void CAnimatedMeshSceneNode::updateAbsolutePosition()
IAnimatedMeshSceneNode::updateAbsolutePosition();
}
//! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set)
void CAnimatedMeshSceneNode::setJointMode(E_JOINT_UPDATE_ON_RENDER mode)
{
checkJoints();
JointMode = mode;
}
//! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2)
//! Sets the transition time in seconds (note: This needs to enable joints)
//! you must call animateJoints(), or the mesh will not animate
void CAnimatedMeshSceneNode::setTransitionTime(f32 time)
{
@ -571,10 +534,6 @@ void CAnimatedMeshSceneNode::setTransitionTime(f32 time)
if (TransitionTime == ttime)
return;
TransitionTime = ttime;
if (ttime != 0)
setJointMode(EJUOR_CONTROL);
else
setJointMode(EJUOR_NONE);
}
//! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected)
@ -583,120 +542,104 @@ void CAnimatedMeshSceneNode::setRenderFromIdentity(bool enable)
RenderFromIdentity = enable;
}
//! updates the joint positions of this mesh
void CAnimatedMeshSceneNode::animateJoints(bool CalculateAbsolutePositions)
void CAnimatedMeshSceneNode::addJoints()
{
if (Mesh && Mesh->getMeshType() == EAMT_SKINNED) {
const auto &joints = static_cast<SkinnedMesh*>(Mesh)->getAllJoints();
PerJoint.setN(joints.size());
PerJoint.SceneNodes.clear();
PerJoint.SceneNodes.reserve(joints.size());
for (size_t i = 0; i < joints.size(); ++i) {
const auto *joint = joints[i];
ISceneNode *parent = this;
if (joint->ParentJointID)
parent = PerJoint.SceneNodes.at(*joint->ParentJointID); // exists because of topo. order
assert(parent);
const auto *matrix = std::get_if<core::matrix4>(&joint->transform);
PerJoint.SceneNodes.push_back(new CBoneSceneNode(
parent, SceneManager, 0, i, joint->Name,
matrix ? core::Transform{} : std::get<core::Transform>(joint->transform),
matrix ? *matrix : std::optional<core::matrix4>{}));
}
}
void CAnimatedMeshSceneNode::updateJointSceneNodes(
const std::vector<SkinnedMesh::SJoint::VariantTransform> &transforms)
{
for (size_t i = 0; i < transforms.size(); ++i) {
const auto &transform = transforms[i];
auto *node = static_cast<CBoneSceneNode*>(PerJoint.SceneNodes[i]);
if (const auto *trs = std::get_if<core::Transform>(&transform)) {
node->setTransform(*trs);
// .x lets animations override matrix transforms entirely.
node->Matrix = std::nullopt;
} else {
node->Matrix = std::get<core::matrix4>(transform);
}
}
}
//! updates the joint positions of this mesh
void CAnimatedMeshSceneNode::animateJoints()
{
if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED)
return;
checkJoints();
const f32 frame = getFrameNr(); // old?
SkinnedMesh *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
skinnedMesh->animateMesh(frame);
skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes);
if (!skinnedMesh->isStatic())
updateJointSceneNodes(skinnedMesh->animateMesh(getFrameNr()));
//-----------------------------------------
// Transition
//-----------------------------------------
if (Transiting != 0.f) {
// Init additional matrices
if (PretransitingSave.size() < JointChildSceneNodes.size()) {
for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n)
PretransitingSave.push_back(core::matrix4());
}
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) {
//------Position------
JointChildSceneNodes[n]->setPosition(
core::lerp(
PretransitingSave[n].getTranslation(),
JointChildSceneNodes[n]->getPosition(),
TransitingBlend));
//------Rotation------
// Code is slow, needs to be fixed up
const core::quaternion RotationStart(PretransitingSave[n].getRotationRadians());
const core::quaternion RotationEnd(JointChildSceneNodes[n]->getRotation() * core::DEGTORAD);
core::quaternion QRotation;
QRotation.slerp(RotationStart, RotationEnd, TransitingBlend);
core::vector3df tmpVector;
QRotation.toEuler(tmpVector);
tmpVector *= core::RADTODEG; // convert from radians back to degrees
JointChildSceneNodes[n]->setRotation(tmpVector);
//------Scale------
// JointChildSceneNodes[n]->setScale(
// core::lerp(
// PretransitingSave[n].getScale(),
// JointChildSceneNodes[n]->getScale(),
// TransitingBlend));
}
}
if (CalculateAbsolutePositions) {
//---slow---
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) {
if (JointChildSceneNodes[n]->getParent() == this) {
JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option
}
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (PerJoint.PreTransSaves[i]) {
PerJoint.SceneNodes[i]->setTransform(PerJoint.PreTransSaves[i]->interpolate(
PerJoint.SceneNodes[i]->getTransform(), TransitingBlend));
}
}
}
}
/*!
*/
void CAnimatedMeshSceneNode::checkJoints()
{
if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED)
return;
if (!JointsUsed) {
for (u32 i = 0; i < JointChildSceneNodes.size(); ++i)
removeChild(JointChildSceneNodes[i]);
JointChildSceneNodes.clear();
// Create joints for SkinnedMesh
((SkinnedMesh *)Mesh)->addJoints(JointChildSceneNodes, this, SceneManager);
((SkinnedMesh *)Mesh)->recoverJointsFromMesh(JointChildSceneNodes);
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i)
removeChild(PerJoint.SceneNodes[i]);
addJoints();
JointsUsed = true;
JointMode = EJUOR_READ;
}
}
/*!
*/
void CAnimatedMeshSceneNode::copyOldTransforms()
{
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (!PerJoint.SceneNodes[i]->Matrix) {
PerJoint.PreTransSaves[i] = PerJoint.SceneNodes[i]->getTransform();
} else {
PerJoint.PreTransSaves[i] = std::nullopt;
}
}
}
void CAnimatedMeshSceneNode::beginTransition()
{
if (!JointsUsed)
return;
if (TransitionTime != 0) {
// Check the array is big enough
if (PretransitingSave.size() < JointChildSceneNodes.size()) {
for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n)
PretransitingSave.push_back(core::matrix4());
}
// Copy the position of joints
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n)
PretransitingSave[n] = JointChildSceneNodes[n]->getRelativeTransformation();
Transiting = core::reciprocal((f32)TransitionTime);
}
TransitingBlend = 0.f;
}
/*!
*/
ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager *newManager)
{
if (!newParent)
@ -722,19 +665,15 @@ ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager *
newNode->EndFrame = EndFrame;
newNode->FramesPerSecond = FramesPerSecond;
newNode->CurrentFrameNr = CurrentFrameNr;
newNode->JointMode = JointMode;
newNode->JointsUsed = JointsUsed;
newNode->TransitionTime = TransitionTime;
newNode->Transiting = Transiting;
newNode->TransitingBlend = TransitingBlend;
newNode->Looping = Looping;
newNode->ReadOnlyMaterials = ReadOnlyMaterials;
newNode->LoopCallBack = LoopCallBack;
if (newNode->LoopCallBack)
newNode->LoopCallBack->grab();
newNode->PassCount = PassCount;
newNode->JointChildSceneNodes = JointChildSceneNodes;
newNode->PretransitingSave = PretransitingSave;
newNode->PerJoint.SceneNodes = PerJoint.SceneNodes;
newNode->PerJoint.PreTransSaves = PerJoint.PreTransSaves;
newNode->RenderFromIdentity = RenderFromIdentity;
return newNode;

View file

@ -4,9 +4,12 @@
#pragma once
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "IAnimatedMesh.h"
#include "SkinnedMesh.h"
#include "Transform.h"
#include "matrix4.h"
namespace irr
@ -54,9 +57,11 @@ public:
//! returns the current loop mode
bool getLoopMode() const override;
//! Sets a callback interface which will be called if an animation
//! playback has ended. Set this to 0 to disable the callback again.
void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) override;
void setOnAnimateCallback(
const std::function<void(f32 dtime)> &cb) override
{
OnAnimateCallback = cb;
}
//! sets the speed with which the animation is played
//! NOTE: setMesh will also change this value and set it to the default speed of the mesh
@ -117,15 +122,16 @@ public:
//! updates the absolute position based on the relative and the parents position
void updateAbsolutePosition() override;
//! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set)
void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) override;
//! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2)
//! Sets the transition time in seconds (note: This needs to enable joints)
//! you must call animateJoints(), or the mesh will not animate
void setTransitionTime(f32 Time) override;
void updateJointSceneNodes(const std::vector<SkinnedMesh::SJoint::VariantTransform> &transforms);
//! updates the joint positions of this mesh
void animateJoints(bool CalculateAbsolutePositions = true) override;
void animateJoints() override;
void addJoints();
//! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected)
void setRenderFromIdentity(bool On) override;
@ -142,6 +148,7 @@ private:
void buildFrameNr(u32 timeMs);
void checkJoints();
void copyOldTransforms();
void beginTransition();
core::array<video::SMaterial> Materials;
@ -158,19 +165,30 @@ private:
f32 Transiting; // is mesh transiting (plus cache of TransitionTime)
f32 TransitingBlend; // 0-1, calculated on buildFrameNr
// 0-unused, 1-get joints only, 2-set joints only, 3-move and set
E_JOINT_UPDATE_ON_RENDER JointMode;
bool JointsUsed;
bool Looping;
bool ReadOnlyMaterials;
bool RenderFromIdentity;
IAnimationEndCallBack *LoopCallBack;
s32 PassCount;
std::function<void(f32)> OnAnimateCallback;
std::vector<IBoneSceneNode *> JointChildSceneNodes;
core::array<core::matrix4> PretransitingSave;
struct PerJointData {
std::vector<CBoneSceneNode *> SceneNodes;
std::vector<core::matrix4> GlobalMatrices;
std::vector<std::optional<core::Transform>> PreTransSaves;
void setN(u16 n) {
SceneNodes.clear();
SceneNodes.resize(n);
GlobalMatrices.clear();
GlobalMatrices.resize(n);
PreTransSaves.clear();
PreTransSaves.resize(n);
}
};
PerJointData PerJoint;
};
} // end namespace scene

View file

@ -143,31 +143,25 @@ bool CB3DMeshFileLoader::readChunkNODE(SkinnedMesh::SJoint *inJoint)
os::Printer::log(logStr.c_str(), joint->Name.value_or("").c_str(), ELL_DEBUG);
#endif
f32 position[3], scale[3], rotation[4];
core::Transform transform;
{
f32 t[3], s[3], r[4];
readFloats(position, 3);
readFloats(scale, 3);
readFloats(rotation, 4);
readFloats(t, 3);
readFloats(s, 3);
readFloats(r, 4);
joint->Animatedposition = core::vector3df(position[0], position[1], position[2]);
joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]);
joint->Animatedrotation = core::quaternion(rotation[1], rotation[2], rotation[3], rotation[0]);
// Build LocalMatrix:
core::matrix4 positionMatrix;
positionMatrix.setTranslation(joint->Animatedposition);
core::matrix4 scaleMatrix;
scaleMatrix.setScale(joint->Animatedscale);
core::matrix4 rotationMatrix;
joint->Animatedrotation.getMatrix_transposed(rotationMatrix);
joint->LocalMatrix = positionMatrix * rotationMatrix * scaleMatrix;
joint->transform = transform = {
{t[0], t[1], t[2]},
{r[1], r[2], r[3], r[0]},
{s[0], s[1], s[2]},
};
}
if (inJoint)
joint->GlobalMatrix = inJoint->GlobalMatrix * joint->LocalMatrix;
joint->GlobalMatrix = inJoint->GlobalMatrix * transform.buildMatrix();
else
joint->GlobalMatrix = joint->LocalMatrix;
joint->GlobalMatrix = transform.buildMatrix();
while (B3dStack.getLast().startposition + B3dStack.getLast().length > B3DFile->getPos()) // this chunk repeats
{

View file

@ -1,86 +0,0 @@
// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h
#include "CBoneSceneNode.h"
#include <optional>
namespace irr
{
namespace scene
{
//! constructor
CBoneSceneNode::CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id,
u32 boneIndex, const std::optional<std::string> &boneName) :
IBoneSceneNode(parent, mgr, id),
BoneIndex(boneIndex),
AnimationMode(EBAM_AUTOMATIC), SkinningSpace(EBSS_LOCAL)
{
setName(boneName);
}
//! Returns the index of the bone
u32 CBoneSceneNode::getBoneIndex() const
{
return BoneIndex;
}
//! Sets the animation mode of the bone. Returns true if successful.
bool CBoneSceneNode::setAnimationMode(E_BONE_ANIMATION_MODE mode)
{
AnimationMode = mode;
return true;
}
//! Gets the current animation mode of the bone
E_BONE_ANIMATION_MODE CBoneSceneNode::getAnimationMode() const
{
return AnimationMode;
}
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &CBoneSceneNode::getBoundingBox() const
{
return Box;
}
/*
//! Returns the relative transformation of the scene node.
core::matrix4 CBoneSceneNode::getRelativeTransformation() const
{
return core::matrix4(); // RelativeTransformation;
}
*/
void CBoneSceneNode::OnAnimate(u32 timeMs)
{
if (IsVisible) {
// update absolute position
// updateAbsolutePosition();
// perform the post render process on all children
ISceneNodeList::iterator it = Children.begin();
for (; it != Children.end(); ++it)
(*it)->OnAnimate(timeMs);
}
}
void CBoneSceneNode::helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node)
{
Node->updateAbsolutePosition();
ISceneNodeList::const_iterator it = Node->getChildren().begin();
for (; it != Node->getChildren().end(); ++it) {
helper_updateAbsolutePositionOfAllChildren((*it));
}
}
void CBoneSceneNode::updateAbsolutePositionOfAllChildren()
{
helper_updateAbsolutePositionOfAllChildren(this);
}
} // namespace scene
} // namespace irr

View file

@ -7,6 +7,8 @@
// Used with SkinnedMesh and IAnimatedMeshSceneNode, for boned meshes
#include "IBoneSceneNode.h"
#include "Transform.h"
#include "matrix4.h"
#include <optional>
@ -21,49 +23,48 @@ public:
//! constructor
CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr,
s32 id = -1, u32 boneIndex = 0,
const std::optional<std::string> &boneName = std::nullopt);
//! Returns the index of the bone
u32 getBoneIndex() const override;
//! Sets the animation mode of the bone. Returns true if successful.
bool setAnimationMode(E_BONE_ANIMATION_MODE mode) override;
//! Gets the current animation mode of the bone
E_BONE_ANIMATION_MODE getAnimationMode() const override;
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override;
/*
//! Returns the relative transformation of the scene node.
//core::matrix4 getRelativeTransformation() const override;
*/
void OnAnimate(u32 timeMs) override;
void updateAbsolutePositionOfAllChildren() override;
//! How the relative transformation of the bone is used
void setSkinningSpace(E_BONE_SKINNING_SPACE space) override
const std::optional<std::string> &boneName = std::nullopt,
const core::Transform &transform = {},
const std::optional<core::matrix4> &matrix = std::nullopt) :
IBoneSceneNode(parent, mgr, id, boneIndex, boneName),
Matrix(matrix)
{
SkinningSpace = space;
setTransform(transform);
}
E_BONE_SKINNING_SPACE getSkinningSpace() const override
void setTransform(const core::Transform &transform)
{
return SkinningSpace;
setPosition(transform.translation);
{
core::vector3df euler;
auto rot = transform.rotation;
// Invert to be consistent with setRotationDegrees
rot.makeInverse();
rot.toEuler(euler);
setRotation(euler * core::RADTODEG);
}
setScale(transform.scale);
}
private:
void helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node);
core::Transform getTransform() const
{
return {
getPosition(),
core::quaternion(getRotation() * core::DEGTORAD).makeInverse(),
getScale()
};
}
u32 BoneIndex;
core::matrix4 getRelativeTransformation() const override
{
if (Matrix)
return *Matrix;
return IBoneSceneNode::getRelativeTransformation();
}
core::aabbox3d<f32> Box{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
E_BONE_ANIMATION_MODE AnimationMode;
E_BONE_SKINNING_SPACE SkinningSpace;
//! Some file formats alternatively let bones specify a transformation matrix.
//! If this is set, it overrides the TRS properties.
std::optional<core::matrix4> Matrix;
};
} // end namespace scene

View file

@ -539,34 +539,25 @@ static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, SkinnedMes
mat[i] = static_cast<f32>(m[i]);
mat = convertHandedness(mat);
// Decompose the matrix into translation, scale, and rotation.
joint->Animatedposition = mat.getTranslation();
auto scale = mat.getScale();
joint->Animatedscale = scale;
joint->Animatedrotation = mat.getRotationRadians(scale);
// Invert the rotation because it is applied using `getMatrix_transposed`,
// which again inverts.
joint->Animatedrotation.makeInverse();
// Note: "When a node is targeted for animation [...],
// only TRS properties MAY be present; matrix MUST NOT be present."
// Thus we MUST NOT do any decomposition, which in general need not exist.
joint->transform = mat;
return mat;
}
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, SkinnedMesh::SJoint *joint)
{
const auto &trans = trs.translation;
const auto &rot = trs.rotation;
const auto &scale = trs.scale;
core::matrix4 transMat;
joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2]));
transMat.setTranslation(joint->Animatedposition);
core::matrix4 rotMat;
joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3]));
core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat);
joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]);
core::matrix4 scaleMat;
scaleMat.setScale(joint->Animatedscale);
return transMat * rotMat * scaleMat;
const auto &t = trs.translation;
const auto &r = trs.rotation;
const auto &s = trs.scale;
core::Transform transform{
convertHandedness(core::vector3df(t[0], t[1], t[2])),
convertHandedness(core::quaternion(r[0], r[1], r[2], r[3])),
core::vector3df(s[0], s[1], s[2]),
};
joint->transform = transform;
return transform.buildMatrix();
}
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform,
@ -584,8 +575,7 @@ void SelfType::MeshExtractor::loadNode(
const auto &node = m_gltf_model.nodes->at(nodeIdx);
auto *joint = m_irr_model->addJoint(parent);
const core::matrix4 transform = loadTransform(node.transform, joint);
joint->LocalMatrix = transform;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * transform : transform;
if (node.name.has_value()) {
joint->Name = node.name->c_str();
}
@ -642,7 +632,6 @@ void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx)
{
const auto &anim = m_gltf_model.animations->at(animIdx);
for (const auto &channel : anim.channels) {
const auto &sampler = anim.samplers.at(channel.sampler);
bool interpolate = ([&]() {
@ -663,6 +652,11 @@ void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx)
throw std::runtime_error("no animated node");
auto *joint = m_loaded_nodes.at(*channel.target.node);
if (std::holds_alternative<core::matrix4>(joint->transform)) {
warn("nodes using matrix transforms must not be animated");
continue;
}
switch (channel.target.path) {
case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);

View file

@ -320,7 +320,6 @@ set(IRRMESHLOADER
add_library(IRRMESHOBJ OBJECT
SkinnedMesh.cpp
CBoneSceneNode.cpp
CMeshSceneNode.cpp
CAnimatedMeshSceneNode.cpp
${IRRMESHLOADER}

View file

@ -35,7 +35,7 @@ void CMeshCache::removeMesh(const IMesh *const mesh)
if (!mesh)
return;
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) {
if (Meshes[i].Mesh == mesh) {
Meshes[i].Mesh->drop();
Meshes.erase(i);
return;
@ -53,7 +53,7 @@ u32 CMeshCache::getMeshCount() const
s32 CMeshCache::getMeshIndex(const IMesh *const mesh) const
{
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh))
if (Meshes[i].Mesh == mesh)
return (s32)i;
}
@ -93,7 +93,7 @@ const io::SNamedPath &CMeshCache::getMeshName(const IMesh *const mesh) const
return emptyNamedPath;
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh))
if (Meshes[i].Mesh == mesh)
return Meshes[i].NamedPath;
}
@ -115,7 +115,7 @@ bool CMeshCache::renameMesh(u32 index, const io::path &name)
bool CMeshCache::renameMesh(const IMesh *const mesh, const io::path &name)
{
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) {
if (Meshes[i].Mesh == mesh) {
Meshes[i].NamedPath.setPath(name);
Meshes.sort();
return true;

View file

@ -6,7 +6,6 @@
#include "SkinnedMesh.h"
#include "SMesh.h"
#include "CMeshBuffer.h"
#include "SAnimatedMesh.h"
#include "os.h"
#include <cassert>
@ -178,34 +177,5 @@ SMesh *CMeshManipulator::createMeshCopy(scene::IMesh *mesh) const
return clone;
}
//! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const
{
if (!mesh)
return 0;
s32 trianglecount = 0;
for (u32 g = 0; g < mesh->getMeshBufferCount(); ++g)
trianglecount += mesh->getMeshBuffer(g)->getIndexCount() / 3;
return trianglecount;
}
//! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const
{
if (mesh && mesh->getMaxFrameNumber() != 0)
return getPolyCount(mesh->getMesh(0));
return 0;
}
//! create a new AnimatedMesh and adds the mesh to it
IAnimatedMesh *CMeshManipulator::createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const
{
return new SAnimatedMesh(mesh, type);
}
} // end namespace scene
} // end namespace irr

View file

@ -31,15 +31,6 @@ public:
//! Clones a static IMesh into a modifiable SMesh.
SMesh *createMeshCopy(scene::IMesh *mesh) const override;
//! Returns amount of polygons in mesh.
s32 getPolyCount(scene::IMesh *mesh) const override;
//! Returns amount of polygons in mesh.
s32 getPolyCount(scene::IAnimatedMesh *mesh) const override;
//! create a new AnimatedMesh and adds the mesh to it
IAnimatedMesh *createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const override;
};
} // end namespace scene

View file

@ -1238,7 +1238,7 @@ void CNullDriver::addOcclusionQuery(scene::ISceneNode *node, const scene::IMesh
else if (node->getType() == scene::ESNT_MESH)
mesh = static_cast<scene::IMeshSceneNode *>(node)->getMesh();
else
mesh = static_cast<scene::IAnimatedMeshSceneNode *>(node)->getMesh()->getMesh(0);
mesh = static_cast<scene::IAnimatedMeshSceneNode *>(node)->getMesh();
if (!mesh)
return;
}

View file

@ -7,7 +7,6 @@
#include "IVideoDriver.h"
#include "SMesh.h"
#include "SMeshBuffer.h"
#include "SAnimatedMesh.h"
#include "IReadFile.h"
#include "fast_atof.h"
#include "coreutil.h"
@ -272,23 +271,19 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file)
}
}
// Create the Animated mesh if there's anything in the mesh
SAnimatedMesh *animMesh = 0;
if (0 != mesh->getMeshBufferCount()) {
mesh->recalculateBoundingBox();
animMesh = new SAnimatedMesh();
animMesh->Type = EAMT_OBJ;
animMesh->addMesh(mesh);
animMesh->recalculateBoundingBox();
}
// Clean up the allocate obj file contents
delete[] buf;
// more cleaning up
cleanUp();
mesh->drop();
return animMesh;
// Nothing in the mesh
if (mesh->getMeshBufferCount() == 0) {
mesh->drop();
return nullptr;
}
mesh->recalculateBoundingBox();
return mesh;
}
//! Read RGB color

View file

@ -8,7 +8,6 @@
#include "CSceneManager.h"
#include "IVideoDriver.h"
#include "IFileSystem.h"
#include "SAnimatedMesh.h"
#include "CMeshCache.h"
#include "IGUIEnvironment.h"
#include "IMaterialRenderer.h"

View file

@ -4,6 +4,7 @@
#include "CXMeshFileLoader.h"
#include "SkinnedMesh.h"
#include "Transform.h"
#include "os.h"
#include "fast_atof.h"
@ -513,6 +514,7 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
if (n.has_value()) {
JointID = *n;
joint = AnimatedMesh->getAllJoints()[JointID];
joint->setParent(Parent);
}
}
@ -527,8 +529,6 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
#ifdef _XREADER_DEBUG
os::Printer::log("using joint ", name.c_str(), ELL_DEBUG);
#endif
if (Parent)
Parent->Children.push_back(joint);
}
// Now inside a frame.
@ -552,12 +552,10 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
if (!parseDataObjectFrame(joint))
return false;
} else if (objectName == "FrameTransformMatrix") {
if (!parseDataObjectTransformationMatrix(joint->LocalMatrix))
core::matrix4 matrix;
if (!parseDataObjectTransformationMatrix(matrix))
return false;
// joint->LocalAnimatedMatrix
// joint->LocalAnimatedMatrix.makeInverse();
// joint->LocalMatrix=tmp*joint->LocalAnimatedMatrix;
joint->transform = matrix;
} else if (objectName == "Mesh") {
/*
frame.Meshes.push_back(SXMesh());

View file

@ -7,7 +7,15 @@
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "SSkinMeshBuffer.h"
#include "Transform.h"
#include "aabbox3d.h"
#include "irrMath.h"
#include "matrix4.h"
#include "os.h"
#include "vector3d.h"
#include <cassert>
#include <cstddef>
#include <variant>
#include <vector>
#include <cassert>
@ -48,154 +56,55 @@ void SkinnedMesh::setAnimationSpeed(f32 fps)
FramesPerSecond = fps;
}
//! returns the animated mesh based
IMesh *SkinnedMesh::getMesh(f32 frame)
{
// animate(frame,startFrameLoop, endFrameLoop);
if (frame == -1)
return this;
animateMesh(frame);
skinMesh();
return this;
}
//--------------------------------------------------------------------------
// Keyframe Animation
//--------------------------------------------------------------------------
//! Animates joints based on frame input
void SkinnedMesh::animateMesh(f32 frame)
using VariantTransform = SkinnedMesh::SJoint::VariantTransform;
std::vector<VariantTransform> SkinnedMesh::animateMesh(f32 frame)
{
if (!HasAnimation || LastAnimatedFrame == frame)
return;
LastAnimatedFrame = frame;
SkinnedLastFrame = false;
for (auto *joint : AllJoints) {
// The joints can be animated here with no input from their
// parents, but for setAnimationMode extra checks are needed
// to their parents
joint->keys.updateTransform(frame,
joint->Animatedposition,
joint->Animatedrotation,
joint->Animatedscale);
assert(HasAnimation);
std::vector<VariantTransform> result;
result.reserve(AllJoints.size());
for (auto *joint : AllJoints)
result.push_back(joint->animate(frame));
return result;
}
// Note:
// LocalAnimatedMatrix needs to be built at some point, but this function may be called lots of times for
// one render (to play two animations at the same time) LocalAnimatedMatrix only needs to be built once.
// a call to buildAllLocalAnimatedMatrices is needed before skinning the mesh, and before the user gets the joints to move
//----------------
// Temp!
buildAllLocalAnimatedMatrices();
//-----------------
updateBoundingBox();
}
void SkinnedMesh::buildAllLocalAnimatedMatrices()
core::aabbox3df SkinnedMesh::calculateBoundingBox(
const std::vector<core::matrix4> &global_transforms)
{
for (auto *joint : AllJoints) {
// Could be faster:
if (!joint->keys.empty()) {
joint->GlobalSkinningSpace = false;
// IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility.
// Not tested so far if this was correct or wrong before quaternion fix!
// Note that using getMatrix_transposed inverts the rotation.
joint->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix);
// --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() ---
f32 *m1 = joint->LocalAnimatedMatrix.pointer();
core::vector3df &Pos = joint->Animatedposition;
m1[0] += Pos.X * m1[3];
m1[1] += Pos.Y * m1[3];
m1[2] += Pos.Z * m1[3];
m1[4] += Pos.X * m1[7];
m1[5] += Pos.Y * m1[7];
m1[6] += Pos.Z * m1[7];
m1[8] += Pos.X * m1[11];
m1[9] += Pos.Y * m1[11];
m1[10] += Pos.Z * m1[11];
m1[12] += Pos.X * m1[15];
m1[13] += Pos.Y * m1[15];
m1[14] += Pos.Z * m1[15];
// -----------------------------------
if (!joint->keys.scale.empty()) {
/*
core::matrix4 scaleMatrix;
scaleMatrix.setScale(joint->Animatedscale);
joint->LocalAnimatedMatrix *= scaleMatrix;
*/
// -------- joint->LocalAnimatedMatrix *= scaleMatrix -----------------
core::matrix4 &mat = joint->LocalAnimatedMatrix;
mat[0] *= joint->Animatedscale.X;
mat[1] *= joint->Animatedscale.X;
mat[2] *= joint->Animatedscale.X;
mat[3] *= joint->Animatedscale.X;
mat[4] *= joint->Animatedscale.Y;
mat[5] *= joint->Animatedscale.Y;
mat[6] *= joint->Animatedscale.Y;
mat[7] *= joint->Animatedscale.Y;
mat[8] *= joint->Animatedscale.Z;
mat[9] *= joint->Animatedscale.Z;
mat[10] *= joint->Animatedscale.Z;
mat[11] *= joint->Animatedscale.Z;
// -----------------------------------
assert(global_transforms.size() == AllJoints.size());
core::aabbox3df result = StaticPartsBox;
// skeletal animation
for (u16 i = 0; i < AllJoints.size(); ++i) {
auto box = AllJoints[i]->LocalBoundingBox;
global_transforms[i].transformBoxEx(box);
result.addInternalBox(box);
}
} else {
joint->LocalAnimatedMatrix = joint->LocalMatrix;
}
}
SkinnedLastFrame = false;
}
void SkinnedMesh::buildAllGlobalAnimatedMatrices(SJoint *joint, SJoint *parentJoint)
{
if (!joint) {
for (auto *rootJoint : RootJoints)
buildAllGlobalAnimatedMatrices(rootJoint, 0);
return;
} else {
// Find global matrix...
if (!parentJoint || joint->GlobalSkinningSpace)
joint->GlobalAnimatedMatrix = joint->LocalAnimatedMatrix;
else
joint->GlobalAnimatedMatrix = parentJoint->GlobalAnimatedMatrix * joint->LocalAnimatedMatrix;
}
for (auto *childJoint : joint->Children)
buildAllGlobalAnimatedMatrices(childJoint, joint);
}
//--------------------------------------------------------------------------
// Software Skinning
//--------------------------------------------------------------------------
//! Preforms a software skin on this mesh based of joint positions
void SkinnedMesh::skinMesh()
{
if (!HasAnimation || SkinnedLastFrame)
return;
//----------------
// This is marked as "Temp!". A shiny dubloon to whomever can tell me why.
buildAllGlobalAnimatedMatrices();
//-----------------
SkinnedLastFrame = true;
if (!HardwareSkinning) {
// rigid animation
for (auto *joint : AllJoints) {
for (u16 i = 0; i < AllJoints.size(); ++i) {
for (u32 j : AllJoints[i]->AttachedMeshes) {
auto box = (*SkinningBuffers)[j]->BoundingBox;
global_transforms[i].transformBoxEx(box);
result.addInternalBox(box);
}
}
return result;
}
// Software Skinning
void SkinnedMesh::skinMesh(const std::vector<core::matrix4> &global_matrices)
{
if (!HasAnimation)
return;
// rigid animation
for (size_t i = 0; i < AllJoints.size(); ++i) {
auto *joint = AllJoints[i];
for (u32 attachedMeshIdx : joint->AttachedMeshes) {
SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx];
Buffer->Transformation = joint->GlobalAnimatedMatrix;
Buffer->Transformation = global_matrices[i];
}
}
@ -204,27 +113,20 @@ void SkinnedMesh::skinMesh()
std::fill(buf.begin(), buf.end(), false);
// skin starting with the root joints
for (auto *rootJoint : RootJoints)
skinJoint(rootJoint, 0);
for (size_t i = 0; i < AllJoints.size(); ++i) {
auto *joint = AllJoints[i];
if (joint->Weights.empty())
continue;
for (auto *buffer : *SkinningBuffers)
buffer->setDirty(EBT_VERTEX);
}
updateBoundingBox();
}
void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
{
if (joint->Weights.size()) {
// Find this joints pull on vertices...
// Find this joints pull on vertices
// Note: It is assumed that the global inversed matrix has been calculated at this point.
core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value();
core::matrix4 jointVertexPull = global_matrices[i] * joint->GlobalInversedMatrix.value();
core::vector3df thisVertexMove, thisNormalMove;
auto &buffersUsed = *SkinningBuffers;
// Skin Vertices Positions and Normals...
// Skin Vertices, Positions and Normals
for (const auto &weight : joint->Weights) {
// Pull this vertex...
jointVertexPull.transformVect(thisVertexMove, weight.StaticPos);
@ -251,14 +153,11 @@ void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
//*(weight._Pos) += thisVertexMove * weight.strength;
}
buffersUsed[weight.buffer_id]->boundingBoxNeedsRecalculated();
}
}
// Skin all children
for (auto *childJoint : joint->Children)
skinJoint(childJoint, joint);
for (auto *buffer : *SkinningBuffers)
buffer->setDirty(EBT_VERTEX);
}
//! Gets joint count.
@ -310,7 +209,7 @@ IMeshBuffer *SkinnedMesh::getMeshBuffer(const video::SMaterial &material) const
if (LocalBuffers[i]->getMaterial() == material)
return LocalBuffers[i];
}
return 0;
return nullptr;
}
u32 SkinnedMesh::getTextureSlot(u32 meshbufNr) const
@ -337,29 +236,6 @@ void SkinnedMesh::setDirty(E_BUFFER_TYPE buffer)
LocalBuffers[i]->setDirty(buffer);
}
//! (This feature is not implemented in irrlicht yet)
bool SkinnedMesh::setHardwareSkinning(bool on)
{
if (HardwareSkinning != on) {
if (on) {
// set mesh to static pose...
for (auto *joint : AllJoints) {
for (const auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
const u32 vertex_id = weight.vertex_id;
LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos = weight.StaticPos;
LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal;
LocalBuffers[buffer_id]->boundingBoxNeedsRecalculated();
}
}
}
HardwareSkinning = on;
}
return HardwareSkinning;
}
void SkinnedMesh::refreshJointCache()
{
// copy cache from the mesh...
@ -384,72 +260,51 @@ void SkinnedMesh::resetAnimation()
LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal;
}
}
SkinnedLastFrame = false;
LastAnimatedFrame = -1;
}
void SkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint)
//! Turns the given array of local matrices into an array of global matrices
//! by multiplying with respective parent matrices.
void SkinnedMesh::calculateGlobalMatrices(std::vector<core::matrix4> &matrices) const
{
if (!joint && parentJoint) // bit of protection from endless loops
return;
// Go through the root bones
if (!joint) {
for (auto *rootJoint : RootJoints)
calculateGlobalMatrices(rootJoint, nullptr);
return;
// Note that the joints are topologically sorted.
for (u16 i = 0; i < AllJoints.size(); ++i) {
if (auto parent_id = AllJoints[i]->ParentJointID) {
matrices[i] = matrices[*parent_id] * matrices[i];
}
}
}
if (!parentJoint)
joint->GlobalMatrix = joint->LocalMatrix;
else
joint->GlobalMatrix = parentJoint->GlobalMatrix * joint->LocalMatrix;
joint->LocalAnimatedMatrix = joint->LocalMatrix;
joint->GlobalAnimatedMatrix = joint->GlobalMatrix;
if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated
joint->GlobalInversedMatrix = joint->GlobalMatrix;
joint->GlobalInversedMatrix->makeInverse(); // slow
}
for (auto *childJoint : joint->Children)
calculateGlobalMatrices(childJoint, joint);
SkinnedLastFrame = false;
}
void SkinnedMesh::checkForAnimation()
bool SkinnedMesh::checkForAnimation() const
{
// Check for animation...
HasAnimation = false;
for (auto *joint : AllJoints) {
if (!joint->keys.empty()) {
HasAnimation = true;
break;
return true;
}
}
// meshes with weights, are still counted as animated for ragdolls, etc
if (!HasAnimation) {
// meshes with weights are animatable
for (auto *joint : AllJoints) {
if (joint->Weights.size()) {
HasAnimation = true;
break;
}
if (!joint->Weights.empty()) {
return true;
}
}
if (HasAnimation) {
return false;
}
void SkinnedMesh::prepareForSkinning()
{
HasAnimation = checkForAnimation();
if (!HasAnimation || PreparedForSkinning)
return;
PreparedForSkinning = true;
EndFrame = 0.0f;
for (const auto *joint : AllJoints) {
EndFrame = std::max(EndFrame, joint->keys.getEndFrame());
}
}
if (HasAnimation && !PreparedForSkinning) {
PreparedForSkinning = true;
// check for bugs:
for (auto *joint : AllJoints) {
for (auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
@ -466,14 +321,11 @@ void SkinnedMesh::checkForAnimation()
}
}
// An array used in skinning
for (u32 i = 0; i < Vertices_Moved.size(); ++i)
for (u32 j = 0; j < Vertices_Moved[i].size(); ++j)
Vertices_Moved[i][j] = false;
// For skinning: cache weight values for speed
for (auto *joint : AllJoints) {
for (auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
@ -482,15 +334,118 @@ void SkinnedMesh::checkForAnimation()
weight.Moved = &Vertices_Moved[buffer_id][vertex_id];
weight.StaticPos = LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos;
weight.StaticNormal = LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal;
// weight._Pos=&Buffers[buffer_id]->getVertex(vertex_id)->Pos;
}
}
// normalize weights
normalizeWeights();
for (auto *joint : AllJoints) {
joint->keys.cleanup();
}
}
void SkinnedMesh::calculateStaticBoundingBox()
{
std::vector<std::vector<bool>> animated(getMeshBufferCount());
for (u32 mb = 0; mb < getMeshBufferCount(); mb++)
animated[mb] = std::vector<bool>(getMeshBuffer(mb)->getVertexCount());
for (auto *joint : AllJoints) {
for (auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
const u32 vertex_id = weight.vertex_id;
animated[buffer_id][vertex_id] = true;
}
}
bool first = true;
for (u16 mb = 0; mb < getMeshBufferCount(); mb++) {
for (u32 v = 0; v < getMeshBuffer(mb)->getVertexCount(); v++) {
if (!animated[mb][v]) {
auto pos = getMeshBuffer(mb)->getVertexBuffer()->getPosition(v);
if (!first) {
StaticPartsBox.addInternalPoint(pos);
} else {
StaticPartsBox.reset(pos);
first = false;
}
}
}
}
}
void SkinnedMesh::calculateJointBoundingBoxes()
{
for (auto *joint : AllJoints) {
bool first = true;
for (auto &weight : joint->Weights) {
if (weight.strength < 1e-6)
continue;
auto pos = weight.StaticPos;
joint->GlobalInversedMatrix.value().transformVect(pos);
if (!first) {
joint->LocalBoundingBox.addInternalPoint(pos);
} else {
joint->LocalBoundingBox.reset(pos);
first = false;
}
}
}
}
void SkinnedMesh::calculateBufferBoundingBoxes()
{
for (u32 j = 0; j < LocalBuffers.size(); ++j) {
// If we use skeletal animation, this will just be a bounding box of the static pose;
// if we use rigid animation, this will correctly transform the points first.
LocalBuffers[j]->recalculateBoundingBox();
}
}
void SkinnedMesh::recalculateBaseBoundingBoxes() {
calculateStaticBoundingBox();
calculateJointBoundingBoxes();
calculateBufferBoundingBoxes();
}
void SkinnedMesh::topoSortJoints()
{
size_t n = AllJoints.size();
std::vector<u16> new_to_old_id;
std::vector<std::vector<u16>> children(n);
for (u16 i = 0; i < n; ++i) {
if (auto parentId = AllJoints[i]->ParentJointID)
children[*parentId].push_back(i);
else
new_to_old_id.push_back(i);
}
// Levelorder
for (u16 i = 0; i < n; ++i) {
new_to_old_id.insert(new_to_old_id.end(),
children[new_to_old_id[i]].begin(),
children[new_to_old_id[i]].end());
}
std::vector<u16> old_to_new_id(n);
for (u16 i = 0; i < n; ++i)
old_to_new_id[new_to_old_id[i]] = i;
std::vector<SJoint *> joints(n);
for (u16 i = 0; i < n; ++i) {
joints[i] = AllJoints[new_to_old_id[i]];
joints[i]->JointID = i;
if (auto parentId = joints[i]->ParentJointID)
joints[i]->ParentJointID = old_to_new_id[*parentId];
}
AllJoints = std::move(joints);
for (u16 i = 0; i < n; ++i) {
if (auto pjid = AllJoints[i]->ParentJointID)
assert(*pjid < i);
}
SkinnedLastFrame = false;
}
//! called by loader after populating with mesh and bone data
@ -498,98 +453,44 @@ SkinnedMesh *SkinnedMeshBuilder::finalize()
{
os::Printer::log("Skinned Mesh - finalize", ELL_DEBUG);
// Make sure we recalc the next frame
LastAnimatedFrame = -1;
SkinnedLastFrame = false;
// calculate bounding box
for (auto *buffer : LocalBuffers) {
buffer->recalculateBoundingBox();
}
if (AllJoints.size() || RootJoints.size()) {
// populate AllJoints or RootJoints, depending on which is empty
if (RootJoints.empty()) {
for (auto *joint : AllJoints) {
bool foundParent = false;
for (const auto *parentJoint : AllJoints) {
for (const auto *childJoint : parentJoint->Children) {
if (childJoint == joint)
foundParent = true;
}
}
if (!foundParent)
RootJoints.push_back(joint);
}
} else {
AllJoints = RootJoints;
}
}
// Set array sizes...
topoSortJoints();
// Set array sizes
for (u32 i = 0; i < LocalBuffers.size(); ++i) {
Vertices_Moved.emplace_back(LocalBuffers[i]->getVertexCount());
}
checkForAnimation();
prepareForSkinning();
if (HasAnimation) {
std::vector<core::matrix4> matrices;
matrices.reserve(AllJoints.size());
for (auto *joint : AllJoints) {
joint->keys.cleanup();
if (const auto *matrix = std::get_if<core::matrix4>(&joint->transform))
matrices.push_back(*matrix);
else
matrices.push_back(std::get<core::Transform>(joint->transform).buildMatrix());
}
calculateGlobalMatrices(matrices);
for (size_t i = 0; i < AllJoints.size(); ++i) {
auto *joint = AllJoints[i];
if (!joint->GlobalInversedMatrix) {
joint->GlobalInversedMatrix = matrices[i];
joint->GlobalInversedMatrix->makeInverse();
}
// Needed for animation and skinning...
calculateGlobalMatrices(0, 0);
// rigid animation for non animated meshes
for (auto *joint : AllJoints) {
for (u32 attachedMeshIdx : joint->AttachedMeshes) {
SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx];
Buffer->Transformation = joint->GlobalAnimatedMatrix;
Buffer->Transformation = matrices[i];
}
}
// calculate bounding box
if (LocalBuffers.empty())
BoundingBox.reset(0, 0, 0);
else {
irr::core::aabbox3df bb(LocalBuffers[0]->BoundingBox);
LocalBuffers[0]->Transformation.transformBoxEx(bb);
BoundingBox.reset(bb);
for (u32 j = 1; j < LocalBuffers.size(); ++j) {
bb = LocalBuffers[j]->BoundingBox;
LocalBuffers[j]->Transformation.transformBoxEx(bb);
BoundingBox.addInternalBox(bb);
}
}
recalculateBaseBoundingBoxes();
StaticPoseBox = calculateBoundingBox(matrices);
return this;
}
void SkinnedMesh::updateBoundingBox()
{
if (!SkinningBuffers)
return;
BoundingBox.reset(0, 0, 0);
for (auto *buffer : *SkinningBuffers) {
buffer->recalculateBoundingBox();
core::aabbox3df bb = buffer->BoundingBox;
buffer->Transformation.transformBoxEx(bb);
BoundingBox.addInternalBox(bb);
}
}
scene::SSkinMeshBuffer *SkinnedMeshBuilder::addMeshBuffer()
{
scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer();
@ -607,14 +508,10 @@ void SkinnedMeshBuilder::addMeshBuffer(SSkinMeshBuffer *meshbuf)
SkinnedMesh::SJoint *SkinnedMeshBuilder::addJoint(SJoint *parent)
{
SJoint *joint = new SJoint;
joint->setParent(parent);
joint->JointID = AllJoints.size();
AllJoints.push_back(joint);
if (!parent) {
// Add root joints to array in finalize()
} else {
// Set parent (Be careful of the mesh loader also setting the parent)
parent->Children.push_back(joint);
}
return joint;
}
@ -684,73 +581,6 @@ void SkinnedMesh::normalizeWeights()
}
}
void SkinnedMesh::recoverJointsFromMesh(std::vector<IBoneSceneNode *> &jointChildSceneNodes)
{
for (u32 i = 0; i < AllJoints.size(); ++i) {
IBoneSceneNode *node = jointChildSceneNodes[i];
SJoint *joint = AllJoints[i];
node->setPosition(joint->LocalAnimatedMatrix.getTranslation());
node->setRotation(joint->LocalAnimatedMatrix.getRotationDegrees());
node->setScale(joint->LocalAnimatedMatrix.getScale());
node->updateAbsolutePosition();
}
}
void SkinnedMesh::transferJointsToMesh(const std::vector<IBoneSceneNode *> &jointChildSceneNodes)
{
for (u32 i = 0; i < AllJoints.size(); ++i) {
const IBoneSceneNode *const node = jointChildSceneNodes[i];
SJoint *joint = AllJoints[i];
joint->LocalAnimatedMatrix.setRotationDegrees(node->getRotation());
joint->LocalAnimatedMatrix.setTranslation(node->getPosition());
joint->LocalAnimatedMatrix *= core::matrix4().setScale(node->getScale());
joint->GlobalSkinningSpace = (node->getSkinningSpace() == EBSS_GLOBAL);
}
// Make sure we recalc the next frame
LastAnimatedFrame = -1;
SkinnedLastFrame = false;
}
void SkinnedMesh::addJoints(std::vector<IBoneSceneNode *> &jointChildSceneNodes,
IAnimatedMeshSceneNode *node, ISceneManager *smgr)
{
// Create new joints
for (u32 i = 0; i < AllJoints.size(); ++i) {
jointChildSceneNodes.push_back(new CBoneSceneNode(0, smgr, 0, i, AllJoints[i]->Name));
}
// Match up parents
for (u32 i = 0; i < jointChildSceneNodes.size(); ++i) {
const SJoint *const joint = AllJoints[i]; // should be fine
s32 parentID = -1;
for (u32 j = 0; (parentID == -1) && (j < AllJoints.size()); ++j) {
if (i != j) {
const SJoint *const parentTest = AllJoints[j];
for (u32 n = 0; n < parentTest->Children.size(); ++n) {
if (parentTest->Children[n] == joint) {
parentID = j;
break;
}
}
}
}
IBoneSceneNode *bone = jointChildSceneNodes[i];
if (parentID != -1)
bone->setParent(jointChildSceneNodes[parentID]);
else
bone->setParent(node);
bone->drop();
}
SkinnedLastFrame = false;
}
void SkinnedMesh::convertMeshToTangents()
{
// now calculate tangents

View file

@ -916,12 +916,7 @@ struct Node {
std::optional<std::size_t> skin;
std::optional<std::vector<double>> weights;
Node(const Json::Value &o)
: transform(Matrix {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
})
: transform(TRS{})
{
check(o.isObject());
if (o.isMember("camera")) {

View file

@ -158,10 +158,10 @@
<url type="homepage">https://www.luanti.org</url>
<url type="bugtracker">https://www.luanti.org/get-involved/#reporting-issues</url>
<url type="translate">https://dev.luanti.org/Translation/</url>
<url type="translate">https://docs.luanti.org/for-creators/translation/</url>
<url type="donation">https://www.luanti.org/get-involved/#donate</url>
<url type="faq">https://wiki.luanti.org/FAQ</url>
<url type="help">https://wiki.luanti.org</url>
<url type="faq">https://docs.luanti.org/about/faq</url>
<url type="help">https://docs.luanti.org</url>
<url type="vcs-browser">https://github.com/luanti-org/luanti</url>
<url type="contribute">https://www.luanti.org/get-involved</url>

View file

@ -4,11 +4,11 @@
<meta charset="utf-8">
<title>Luanti API documentation</title>
<link rel="canonical" href="https://api.minetest.netURL">
<meta http-equiv="refresh" content="0; url=https://api.minetest.netURL">
<link rel="canonical" href="https://api.luanti.orgURL">
<meta http-equiv="refresh" content="0; url=https://api.luanti.orgURL">
</head>
<body>
<h1>Redirecting&hellip;</h1>
<a href="https://api.minetest.netURL">Click here if you are not redirected.</a>
<a href="https://api.luanti.orgURL">Click here if you are not redirected.</a>
</body>
</html>

View file

@ -953,7 +953,7 @@ else()
set(MATH_FLAGS "-fno-math-errno -fno-trapping-math -fno-signed-zeros")
# Enable SSE for floating point math on 32-bit x86 by default
# reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath
# reasoning see luanti issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
check_c_source_compiles("#ifndef __i686__\n#error\n#endif\nint main(){}" IS_I686)
if(IS_I686)

View file

@ -4,6 +4,7 @@ set (BENCHMARK_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_lighting.cpp
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_serialize.cpp
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_mapblock.cpp
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_map.cpp
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_mapmodify.cpp
${CMAKE_CURRENT_SOURCE_DIR}/benchmark_sha.cpp
PARENT_SCOPE)

View file

@ -0,0 +1,179 @@
// Luanti
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) 2023 Minetest Authors
#include "catch.h"
#include "dummygamedef.h"
#include "map.h"
#include "mapsector.h"
namespace {
class TestMap : public Map {
public:
TestMap(IGameDef *gamedef) : Map(gamedef) {}
MapBlock * createBlockTest(v3s16 p)
{
v2s16 p2d(p.X, p.Z);
s16 block_y = p.Y;
MapSector *sector = getSectorNoGenerate(p2d);
if (!sector) {
sector = new MapSector(this, p2d, m_gamedef);
m_sectors[p2d] = sector;
}
MapBlock *block = sector->getBlockNoCreateNoEx(block_y);
if (block)
return block;
return sector->createBlankBlock(block_y);
}
};
}
static void fillMap(TestMap &map, s16 n)
{
for(s16 z=0; z<n; z++)
for(s16 y=0; y<n; y++)
for(s16 x=0; x<n; x++) {
v3s16 p(x,y,z);
// create an empty block
map.createBlockTest(p);
}
}
static int readBlocks(Map &map, s16 n)
{
int result = 0;
for(s16 z=0; z<n; z++)
for(s16 y=0; y<n; y++)
for(s16 x=0; x<n; x++) {
v3s16 p(x,y,z);
MapBlock *block = map.getBlockNoCreateNoEx(p);
if (block) {
result++;
}
}
return result;
}
static int readRandomBlocks(Map &map, s16 n)
{
int result = 0;
for(int i=0; i < n * n * n; i++) {
v3s16 p(myrand_range(0, n), myrand_range(0, n), myrand_range(0, n));
MapBlock *block = map.getBlockNoCreateNoEx(p);
if (block) {
result++;
}
}
return result;
}
static int readYColumn(Map &map, s16 n)
{
int result = 0;
for(s16 z=0; z<n; z++)
for(s16 x=0; x<n; x++)
for(s16 y=n-1; y>0; y--) {
v3s16 p(x,y,z);
MapBlock *block = map.getBlockNoCreateNoEx(p);
if (block) {
result++;
}
}
return result;
}
static int readNodes(Map &map, s16 n)
{
int result = 0;
for(s16 z=0; z<n * MAP_BLOCKSIZE; z+=8)
for(s16 y=0; y<n * MAP_BLOCKSIZE; y+=4)
for(s16 x=0; x<n * MAP_BLOCKSIZE; x++) {
v3s16 p(x,y,z);
MapNode n = map.getNode(p);
if (n.getContent() != CONTENT_IGNORE)
result++;
}
return result;
}
#define BENCH1(_count) \
BENCHMARK_ADVANCED("create_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
meter.measure([&] { \
fillMap(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readEmpty_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
meter.measure([&] { \
return readBlocks(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readFilled_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
fillMap(map, _count); \
meter.measure([&] { \
return readBlocks(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readEmptyYCol_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
meter.measure([&] { \
return readYColumn(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readFilledYCol_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
fillMap(map, _count); \
meter.measure([&] { \
return readYColumn(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readEmptyRandom_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
meter.measure([&] { \
return readRandomBlocks(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readFilledRandom_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
fillMap(map, _count); \
meter.measure([&] { \
return readRandomBlocks(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readEmptyNodes_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
meter.measure([&] { \
return readNodes(map, _count); \
}); \
}; \
BENCHMARK_ADVANCED("readFilledNodes_" #_count)(Catch::Benchmark::Chronometer meter) { \
DummyGameDef gamedef; \
TestMap map(&gamedef); \
fillMap(map, _count); \
meter.measure([&] { \
return readNodes(map, _count); \
}); \
}; \
TEST_CASE("benchmark_map") {
BENCH1(10)
BENCH1(40) // 64.000 blocks
}

View file

@ -612,12 +612,7 @@ void Client::step(float dtime)
if (minimap_mapblocks.empty())
do_mapper_update = false;
bool is_empty = true;
for (int l = 0; l < MAX_TILE_LAYERS; l++)
if (r.mesh->getMesh(l)->getMeshBufferCount() != 0)
is_empty = false;
if (is_empty) {
if (r.mesh->isEmpty()) {
delete r.mesh;
} else {
// Replace with the new mesh
@ -1897,39 +1892,38 @@ float Client::getCurRate()
void Client::makeScreenshot()
{
irr::video::IVideoDriver *driver = m_rendering_engine->get_video_driver();
irr::video::IImage* const raw_image = driver->createScreenShot();
video::IVideoDriver *driver = m_rendering_engine->get_video_driver();
video::IImage* const raw_image = driver->createScreenShot();
if (!raw_image)
if (!raw_image) {
errorstream << "Could not take screenshot" << std::endl;
return;
}
const struct tm tm = mt_localtime();
char timetstamp_c[64];
strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", &tm);
char timestamp_c[64];
strftime(timestamp_c, sizeof(timestamp_c), "%Y%m%d_%H%M%S", &tm);
std::string screenshot_dir;
if (fs::IsPathAbsolute(g_settings->get("screenshot_path")))
screenshot_dir = g_settings->get("screenshot_path");
else
screenshot_dir = porting::path_user + DIR_DELIM + g_settings->get("screenshot_path");
std::string screenshot_dir = g_settings->get("screenshot_path");
if (!fs::IsPathAbsolute(screenshot_dir))
screenshot_dir = porting::path_user + DIR_DELIM + screenshot_dir;
std::string filename_base = screenshot_dir
+ DIR_DELIM
+ std::string("screenshot_")
+ std::string(timetstamp_c);
+ timestamp_c;
std::string filename_ext = "." + g_settings->get("screenshot_format");
std::string filename;
// Create the directory if it doesn't already exist.
// Otherwise, saving the screenshot would fail.
fs::CreateDir(screenshot_dir);
fs::CreateAllDirs(screenshot_dir);
u32 quality = (u32)g_settings->getS32("screenshot_quality");
quality = MYMIN(MYMAX(quality, 0), 100) / 100.0 * 255;
quality = rangelim(quality, 0, 100) / 100.0f * 255;
// Try to find a unique filename
std::string filename;
unsigned serial = 0;
while (serial < SCREENSHOT_MAX_SERIAL_TRIES) {
@ -1940,23 +1934,23 @@ void Client::makeScreenshot()
}
if (serial == SCREENSHOT_MAX_SERIAL_TRIES) {
infostream << "Could not find suitable filename for screenshot" << std::endl;
errorstream << "Could not find suitable filename for screenshot" << std::endl;
} else {
irr::video::IImage* const image =
video::IImage* const image =
driver->createImage(video::ECF_R8G8B8, raw_image->getDimension());
if (image) {
raw_image->copyTo(image);
std::ostringstream sstr;
std::string msg;
if (driver->writeImageToFile(image, filename.c_str(), quality)) {
sstr << "Saved screenshot to '" << filename << "'";
msg = fmtgettext("Saved screenshot to \"%s\"", filename.c_str());
} else {
sstr << "Failed to save screenshot '" << filename << "'";
msg = fmtgettext("Failed to save screenshot to \"%s\"", filename.c_str());
}
pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
utf8_to_wide(sstr.str())));
infostream << sstr.str() << std::endl;
utf8_to_wide(msg)));
infostream << msg << std::endl;
image->drop();
}
}

View file

@ -148,7 +148,7 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
Menu-game loop
*/
bool retval = true;
bool *kill = porting::signal_handler_killstatus();
volatile auto *kill = porting::signal_handler_killstatus();
while (m_rendering_engine->run() && !*kill &&
!g_gamecallback->shutdown_requested) {
@ -540,7 +540,7 @@ bool ClientLauncher::launch_game(std::string &error_message,
void ClientLauncher::main_menu(MainMenuData *menudata)
{
bool *kill = porting::signal_handler_killstatus();
volatile auto *kill = porting::signal_handler_killstatus();
video::IVideoDriver *driver = m_rendering_engine->get_video_driver();
auto *device = m_rendering_engine->get_raw_device();

View file

@ -178,15 +178,6 @@ static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill,
matrix.setTextureScale(txs, tys);
}
// Evaluate transform chain recursively; irrlicht does not do this for us
static void updatePositionRecursive(scene::ISceneNode *node)
{
scene::ISceneNode *parent = node->getParent();
if (parent)
updatePositionRecursive(parent);
node->updateAbsolutePosition();
}
static bool logOnce(const std::ostringstream &from, std::ostream &log_to)
{
thread_local std::vector<u64> logged;
@ -682,7 +673,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
m_animated_meshnode = m_smgr->addAnimatedMeshSceneNode(mesh, m_matrixnode);
m_animated_meshnode->grab();
mesh->drop(); // The scene node took hold of it
m_animated_meshnode->animateJoints(); // Needed for some animations
m_animated_meshnode->setScale(m_prop.visual_size);
// set vertex colors to ensure alpha is set
@ -693,6 +683,21 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
m_animated_meshnode->forEachMaterial([this] (auto &mat) {
mat.BackfaceCulling = m_prop.backface_culling;
});
m_animated_meshnode->setOnAnimateCallback([&](f32 dtime) {
for (auto &it : m_bone_override) {
auto* bone = m_animated_meshnode->getJointNode(it.first.c_str());
if (!bone)
continue;
BoneOverride &props = it.second;
props.dtime_passed += dtime;
bone->setPosition(props.getPosition(bone->getPosition()));
bone->setRotation(props.getRotationEulerDeg(bone->getRotation()));
bone->setScale(props.getScale(bone->getScale()));
}
});
} else
errorstream<<"GenericCAO::addToScene(): Could not load mesh "<<m_prop.mesh<<std::endl;
break;
@ -783,7 +788,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
updateMarker();
updateNodePos();
updateAnimation();
updateBones(.0f);
updateAttachments();
setNodeLight(m_last_light);
updateMeshCulling();
@ -1174,18 +1178,6 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
rot_translator.val_current = m_rotation;
updateNodePos();
}
if (m_animated_meshnode) {
// Everything must be updated; the whole transform
// chain as well as the animated mesh node.
// Otherwise, bone attachments would be relative to
// a position that's one frame old.
if (m_matrixnode)
updatePositionRecursive(m_matrixnode);
m_animated_meshnode->updateAbsolutePosition();
m_animated_meshnode->animateJoints();
updateBones(dtime);
}
}
static void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count)
@ -1394,44 +1386,6 @@ void GenericCAO::updateAnimationSpeed()
m_animated_meshnode->setAnimationSpeed(m_animation_speed);
}
void GenericCAO::updateBones(f32 dtime)
{
if (!m_animated_meshnode)
return;
if (m_bone_override.empty()) {
m_animated_meshnode->setJointMode(scene::EJUOR_NONE);
return;
}
m_animated_meshnode->setJointMode(scene::EJUOR_CONTROL); // To write positions to the mesh on render
for (auto &it : m_bone_override) {
std::string bone_name = it.first;
scene::IBoneSceneNode* bone = m_animated_meshnode->getJointNode(bone_name.c_str());
if (!bone)
continue;
BoneOverride &props = it.second;
props.dtime_passed += dtime;
bone->setPosition(props.getPosition(bone->getPosition()));
bone->setRotation(props.getRotationEulerDeg(bone->getRotation()));
bone->setScale(props.getScale(bone->getScale()));
}
// The following is needed for set_bone_pos to propagate to
// attached objects correctly.
// Irrlicht ought to do this, but doesn't when using EJUOR_CONTROL.
for (u32 i = 0; i < m_animated_meshnode->getJointCount(); ++i) {
auto bone = m_animated_meshnode->getJointNode(i);
// Look for the root bone.
if (bone && bone->getParent() == m_animated_meshnode) {
// Update entire skeleton.
bone->updateAbsolutePositionOfAllChildren();
break;
}
}
}
void GenericCAO::updateAttachments()
{
ClientActiveObject *parent = getParent();
@ -1747,7 +1701,6 @@ void GenericCAO::processMessage(const std::string &data)
} else {
m_bone_override[bone] = props;
}
// updateBones(); now called every step
} else if (cmd == AO_CMD_ATTACH_TO) {
u16 parent_id = readS16(is);
std::string bone = deSerializeString16(is);

View file

@ -286,8 +286,6 @@ public:
void updateAnimationSpeed();
void updateBones(f32 dtime);
void processMessage(const std::string &data) override;
bool directReportPunch(v3f dir, const ItemStack *punchitem,

View file

@ -464,8 +464,6 @@ void MapblockMeshGenerator::drawSolidNode()
for (auto &layer : tiles[face].layers) {
if (backface_culling)
layer.material_flags |= MATERIAL_FLAG_BACKFACE_CULLING;
layer.material_flags |= MATERIAL_FLAG_TILEABLE_HORIZONTAL;
layer.material_flags |= MATERIAL_FLAG_TILEABLE_VERTICAL;
}
if (!data->m_smooth_lighting) {
lights[face] = getFaceLight(cur_node.n, neighbor, nodedef);
@ -474,7 +472,6 @@ void MapblockMeshGenerator::drawSolidNode()
if (!faces)
return;
u8 mask = faces ^ 0b0011'1111; // k-th bit is set if k-th face is to be *omitted*, as expected by cuboid drawing functions.
cur_node.origin = intToFloat(cur_node.p, BS);
auto box = aabb3f(v3f(-0.5 * BS), v3f(0.5 * BS));
box.MinEdge += cur_node.origin;
box.MaxEdge += cur_node.origin;
@ -1775,6 +1772,7 @@ void MapblockMeshGenerator::errorUnknownDrawtype()
void MapblockMeshGenerator::drawNode()
{
cur_node.origin = intToFloat(cur_node.p, BS);
switch (cur_node.f->drawtype) {
case NDT_AIRLIKE: // Not drawn at all
return;
@ -1785,7 +1783,6 @@ void MapblockMeshGenerator::drawNode()
default:
break;
}
cur_node.origin = intToFloat(cur_node.p, BS);
if (data->m_smooth_lighting) {
getSmoothLightFrame();
} else {

View file

@ -27,7 +27,7 @@ struct LightInfo {
float light_night;
float light_boosted;
LightPair getPair(float sunlight_boost = 0.0) const
LightPair getPair(float sunlight_boost = 0.0f) const
{
return LightPair(
(1 - sunlight_boost) * light_day

View file

@ -65,6 +65,8 @@
#include "client/sound/sound_openal.h"
#endif
#include <csignal>
class NodeDugEvent : public MtEvent
{
public:
@ -561,7 +563,7 @@ public:
Game();
~Game();
bool startup(bool *kill,
bool startup(volatile std::sig_atomic_t *kill,
InputHandler *input,
RenderingEngine *rendering_engine,
const GameStartData &game_params,
@ -797,7 +799,7 @@ private:
RenderingEngine *m_rendering_engine;
video::IVideoDriver *driver;
scene::ISceneManager *smgr;
bool *kill;
volatile std::sig_atomic_t *kill;
std::string *error_message;
bool *reconnect_requested;
PausedNodesList paused_animated_nodes;
@ -932,7 +934,7 @@ Game::~Game()
m_rendering_engine->finalize();
}
bool Game::startup(bool *kill,
bool Game::startup(volatile std::sig_atomic_t *kill,
InputHandler *input,
RenderingEngine *rendering_engine,
const GameStartData &start_data,
@ -1923,7 +1925,7 @@ void Game::processKeyInput()
if (g_settings->getBool("continuous_forward"))
toggleAutoforward();
} else if (wasKeyDown(KeyType::INVENTORY)) {
m_game_formspec.showPlayerInventory();
m_game_formspec.showPlayerInventory(nullptr);
} else if (input->cancelPressed()) {
#ifdef __ANDROID__
m_android_chat_open = false;
@ -2712,11 +2714,16 @@ void Game::handleClientEvent_DeathscreenLegacy(ClientEvent *event, CameraOrienta
void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam)
{
m_game_formspec.showFormSpec(*event->show_formspec.formspec,
*event->show_formspec.formname);
auto &fs = event->show_formspec;
delete event->show_formspec.formspec;
delete event->show_formspec.formname;
if (fs.formname->empty() && !fs.formspec->empty()) {
m_game_formspec.showPlayerInventory(fs.formspec);
} else {
m_game_formspec.showFormSpec(*fs.formspec, *fs.formname);
}
delete fs.formspec;
delete fs.formname;
}
void Game::handleClientEvent_ShowCSMFormSpec(ClientEvent *event, CameraOrientation *cam)
@ -4225,7 +4232,7 @@ void Game::readSettings()
****************************************************************************/
/****************************************************************************/
void the_game(bool *kill,
void the_game(volatile std::sig_atomic_t *kill,
InputHandler *input,
RenderingEngine *rendering_engine,
const GameStartData &start_data,

View file

@ -6,6 +6,7 @@
#include "irrlichttypes.h"
#include "config.h"
#include <csignal>
#include <string>
#if !IS_CLIENT_BUILD
@ -36,7 +37,7 @@ struct CameraOrientation {
#define GAME_FALLBACK_TIMEOUT 1.8f
#define GAME_CONNECTION_TIMEOUT 10.0f
void the_game(bool *kill,
void the_game(volatile std::sig_atomic_t *kill,
InputHandler *input,
RenderingEngine *rendering_engine,
const GameStartData &start_data,

View file

@ -178,6 +178,10 @@ public:
const std::string &getForm() const
{
LocalPlayer *player = m_client->getEnv().getLocalPlayer();
if (!player->inventory_formspec_override.empty())
return player->inventory_formspec_override;
return player->inventory_formspec;
}
@ -304,7 +308,7 @@ void GameFormSpec::showNodeFormspec(const std::string &formspec, const v3s16 &no
m_formspec->setFormSpec(formspec, inventoryloc);
}
void GameFormSpec::showPlayerInventory()
void GameFormSpec::showPlayerInventory(const std::string *fs_override)
{
/*
* Don't permit to open inventory is CAO or player doesn't exists.
@ -317,28 +321,35 @@ void GameFormSpec::showPlayerInventory()
infostream << "Game: Launching inventory" << std::endl;
PlayerInventoryFormSource *fs_src = new PlayerInventoryFormSource(m_client);
auto fs_src = std::make_unique<PlayerInventoryFormSource>(m_client);
InventoryLocation inventoryloc;
inventoryloc.setCurrentPlayer();
if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc))) {
delete fs_src;
return;
if (fs_override) {
// Temporary overwrite for this specific formspec.
player->inventory_formspec_override = *fs_override;
} else {
// Show the regular inventory formspec
player->inventory_formspec_override.clear();
}
if (fs_src->getForm().empty()) {
delete fs_src;
// If prevented by Client-Side Mods
if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc)))
return;
// Empty formspec -> do not show.
if (fs_src->getForm().empty())
return;
}
TextDest *txt_dst = new TextDestPlayerInventory(m_client);
GUIFormSpecMenu::create(m_formspec, m_client, m_rendering_engine->get_gui_env(),
&m_input->joystick, fs_src, txt_dst, m_client->getFormspecPrepend(),
&m_input->joystick, fs_src.get(), txt_dst, m_client->getFormspecPrepend(),
m_client->getSoundManager());
m_formspec->setFormSpec(fs_src->getForm(), inventoryloc);
fs_src.release(); // owned by GUIFormSpecMenu
}
#define SIZE_TAG "size[11,5.5,true]" // Fixed size (ignored in touchscreen mode)

View file

@ -34,7 +34,9 @@ struct GameFormSpec
// Currently only used for the in-game settings menu.
void showPauseMenuFormSpec(const std::string &formspec, const std::string &formname);
void showNodeFormspec(const std::string &formspec, const v3s16 &nodepos);
void showPlayerInventory();
/// If `!fs_override`: Uses `player->inventory_formspec`.
/// If ` fs_override`: Uses a temporary formspec until an update is received.
void showPlayerInventory(const std::string *fs_override);
void showDeathFormspecLegacy();
// Shows the hardcoded "main" pause menu.
void showPauseMenu();

View file

@ -137,6 +137,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
// This is separate from other keyboard handling so that it also works in menus.
if (event.EventType == EET_KEY_INPUT_EVENT) {
KeyPress keyCode(event.KeyInput);
if (keyCode == getKeySetting("keymap_fullscreen")) {
if (event.KeyInput.PressedDown && !fullscreen_is_down) {
IrrlichtDevice *device = RenderingEngine::get_raw_device();
@ -150,8 +151,15 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
}
fullscreen_is_down = event.KeyInput.PressedDown;
return true;
} else if (keyCode == EscapeKey &&
event.KeyInput.PressedDown && event.KeyInput.Shift) {
} else if (keyCode == getKeySetting("keymap_close_world")) {
close_world_down = event.KeyInput.PressedDown;
} else if (keyCode == EscapeKey) {
esc_down = event.KeyInput.PressedDown;
}
if (esc_down && close_world_down) {
g_gamecallback->disconnect();
return true;
}

View file

@ -128,6 +128,9 @@ private:
// Intentionally not reset by clearInput/releaseAllKeys.
bool fullscreen_is_down = false;
bool close_world_down = false;
bool esc_down = false;
PointerType last_pointer_type = PointerType::Mouse;
};

View file

@ -99,6 +99,8 @@ public:
std::string hotbar_image = "";
std::string hotbar_selected_image = "";
/// Temporary player inventory formspec. Empty value = feature inactive.
std::string inventory_formspec_override;
video::SColor light_color = video::SColor(255, 255, 255, 255);

View file

@ -51,15 +51,6 @@ void MeshMakeData::fillBlockDataBegin(const v3s16 &blockpos)
m_vmanip.addArea(voxel_area);
}
void MeshMakeData::fillBlockData(const v3s16 &bp, MapNode *data)
{
v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE);
VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1));
v3s16 blockpos_nodes = bp * MAP_BLOCKSIZE;
m_vmanip.copyFrom(data, data_area, v3s16(0,0,0), blockpos_nodes, data_size);
}
void MeshMakeData::fillSingleNode(MapNode data, MapNode padding)
{
m_blockpos = {0, 0, 0};
@ -626,7 +617,7 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data):
if (data->m_vmanip.getNodeNoEx(p).getContent() != CONTENT_IGNORE) {
MinimapMapblock *block = new MinimapMapblock;
m_minimap_mapblocks[mesh_grid.getOffsetIndex(ofs)] = block;
block->getMinimapNodes(&data->m_vmanip, p);
block->getMinimapNodes(&data->m_vmanip, data->m_nodedef, p);
}
}
}

View file

@ -60,7 +60,6 @@ struct MeshMakeData
Copy block data manually (to allow optimizations by the caller)
*/
void fillBlockDataBegin(const v3s16 &blockpos);
void fillBlockData(const v3s16 &bp, MapNode *data);
/*
Prepare block data for rendering a single node located at (0,0,0).
@ -212,6 +211,20 @@ public:
return minimap_mapblocks;
}
/// @return true if the mesh contains nothing to draw
bool isEmpty() const
{
if (!m_transparent_triangles.empty())
return false;
for (auto &mesh : m_mesh) {
for (u32 i = 0; i < mesh->getMeshBufferCount(); i++) {
if (mesh->getMeshBuffer(i)->getIndexCount() != 0)
return false;
}
}
return true;
}
bool isAnimationForced() const
{
return m_animation_force_timer == 0;
@ -242,7 +255,7 @@ public:
/// get the list of transparent buffers
const std::vector<PartialMeshBuffer> &getTransparentBuffers() const
{
return this->m_transparent_buffers;
return m_transparent_buffers;
}
private:

View file

@ -10,10 +10,9 @@
#include <cmath>
#include <iostream>
#include <IAnimatedMesh.h>
#include <SAnimatedMesh.h>
#include <IAnimatedMeshSceneNode.h>
#include "S3DVertex.h"
#include "SMesh.h"
#include <SMesh.h>
#include "SMeshBuffer.h"
inline static void applyShadeFactor(video::SColor& color, float factor)
@ -97,11 +96,8 @@ scene::IAnimatedMesh* createCubeMesh(v3f scale)
mesh->addMeshBuffer(buf);
buf->drop();
}
scene::SAnimatedMesh *anim_mesh = new scene::SAnimatedMesh(mesh);
mesh->drop();
scaleMesh(anim_mesh, scale); // also recalculates bounding box
return anim_mesh;
scaleMesh(mesh, scale); // also recalculates bounding box
return mesh;
}
template<typename F>

View file

@ -11,18 +11,6 @@
#include "util/directiontables.h"
#include "porting.h"
// Data placeholder used for copying from non-existent blocks
static struct BlockPlaceholder {
MapNode data[MapBlock::nodecount];
BlockPlaceholder()
{
for (std::size_t i = 0; i < MapBlock::nodecount; i++)
data[i] = MapNode(CONTENT_IGNORE);
}
} block_placeholder;
/*
QueuedMeshUpdate
*/
@ -32,6 +20,38 @@ QueuedMeshUpdate::~QueuedMeshUpdate()
delete data;
}
void QueuedMeshUpdate::retrieveBlocks(Map *map, u16 cell_size)
{
const size_t total = (cell_size+2)*(cell_size+2)*(cell_size+2);
if (map_blocks.empty())
map_blocks.resize(total);
else
assert(map_blocks.size() == total); // must not change
size_t i = 0;
v3s16 pos;
for (pos.X = p.X - 1; pos.X <= p.X + cell_size; pos.X++)
for (pos.Z = p.Z - 1; pos.Z <= p.Z + cell_size; pos.Z++)
for (pos.Y = p.Y - 1; pos.Y <= p.Y + cell_size; pos.Y++) {
if (!map_blocks[i]) {
MapBlock *block = map->getBlockNoCreateNoEx(pos);
if (block) {
block->refGrab();
map_blocks[i] = block;
}
}
i++;
}
}
void QueuedMeshUpdate::dropBlocks()
{
for (auto *block : map_blocks) {
if (block)
block->refDrop();
}
map_blocks.clear();
}
/*
MeshUpdateQueue
*/
@ -48,26 +68,28 @@ MeshUpdateQueue::~MeshUpdateQueue()
MutexAutoLock lock(m_mutex);
for (QueuedMeshUpdate *q : m_queue) {
for (auto block : q->map_blocks)
if (block)
block->refDrop();
q->dropBlocks();
delete q;
}
}
bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent)
bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server,
bool urgent, bool from_neighbor)
{
// FIXME: with cell_size > 1 there isn't a "main block" and this check is
// probably incorrect and broken
MapBlock *main_block = map->getBlockNoCreateNoEx(p);
if (!main_block)
return false;
MutexAutoLock lock(m_mutex);
MeshGrid mesh_grid = m_client->getMeshGrid();
// Mesh is placed at the corner block of a chunk
// (where all coordinate are divisible by the chunk size)
v3s16 mesh_position(mesh_grid.getMeshPos(p));
v3s16 mesh_position = mesh_grid.getMeshPos(p);
MutexAutoLock lock(m_mutex);
/*
Mark the block as urgent if requested
*/
@ -80,44 +102,26 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool
*/
for (QueuedMeshUpdate *q : m_queue) {
if (q->p == mesh_position) {
// NOTE: We are not adding a new position to the queue, thus
// refcount_from_queue stays the same.
if (ack_block_to_server)
q->ack_list.push_back(p);
q->crack_level = m_client->getCrackLevel();
q->crack_pos = m_client->getCrackPos();
q->urgent |= urgent;
v3s16 pos;
int i = 0;
for (pos.X = q->p.X - 1; pos.X <= q->p.X + mesh_grid.cell_size; pos.X++)
for (pos.Z = q->p.Z - 1; pos.Z <= q->p.Z + mesh_grid.cell_size; pos.Z++)
for (pos.Y = q->p.Y - 1; pos.Y <= q->p.Y + mesh_grid.cell_size; pos.Y++) {
if (!q->map_blocks[i]) {
MapBlock *block = map->getBlockNoCreateNoEx(pos);
if (block) {
block->refGrab();
q->map_blocks[i] = block;
}
}
i++;
}
q->retrieveBlocks(map, mesh_grid.cell_size);
return true;
}
}
/*
Make a list of blocks necessary for mesh generation and lock the blocks in memory.
Air blocks won't suddenly become visible due to a neighbor update, so
skip those.
Note: this can be extended with more precise checks in the future
*/
std::vector<MapBlock *> map_blocks;
map_blocks.reserve((mesh_grid.cell_size+2)*(mesh_grid.cell_size+2)*(mesh_grid.cell_size+2));
v3s16 pos;
for (pos.X = mesh_position.X - 1; pos.X <= mesh_position.X + mesh_grid.cell_size; pos.X++)
for (pos.Z = mesh_position.Z - 1; pos.Z <= mesh_position.Z + mesh_grid.cell_size; pos.Z++)
for (pos.Y = mesh_position.Y - 1; pos.Y <= mesh_position.Y + mesh_grid.cell_size; pos.Y++) {
MapBlock *block = map->getBlockNoCreateNoEx(pos);
map_blocks.push_back(block);
if (block)
block->refGrab();
if (from_neighbor && mesh_grid.cell_size == 1 && main_block->isAir()) {
assert(!ack_block_to_server);
m_urgents.erase(mesh_position);
g_profiler->add("MeshUpdateQueue: updates skipped", 1);
return true;
}
/*
@ -130,7 +134,7 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool
q->crack_level = m_client->getCrackLevel();
q->crack_pos = m_client->getCrackPos();
q->urgent = urgent;
q->map_blocks = std::move(map_blocks);
q->retrieveBlocks(map, mesh_grid.cell_size);
m_queue.push_back(q);
return true;
@ -188,7 +192,8 @@ void MeshUpdateQueue::fillDataFromMapBlocks(QueuedMeshUpdate *q)
for (pos.Z = q->p.Z - 1; pos.Z <= q->p.Z + mesh_grid.cell_size; pos.Z++)
for (pos.Y = q->p.Y - 1; pos.Y <= q->p.Y + mesh_grid.cell_size; pos.Y++) {
MapBlock *block = q->map_blocks[i++];
data->fillBlockData(pos, block ? block->getData() : block_placeholder.data);
if (block)
block->copyTo(data->m_vmanip);
}
data->setCrack(q->crack_level, q->crack_pos);
@ -205,20 +210,16 @@ MeshUpdateWorkerThread::MeshUpdateWorkerThread(Client *client, MeshUpdateQueue *
UpdateThread("Mesh"), m_client(client), m_queue_in(queue_in), m_manager(manager)
{
m_generation_interval = g_settings->getU16("mesh_generation_interval");
m_generation_interval = rangelim(m_generation_interval, 0, 50);
m_generation_interval = rangelim(m_generation_interval, 0, 25);
}
void MeshUpdateWorkerThread::doUpdate()
{
QueuedMeshUpdate *q;
while ((q = m_queue_in->pop())) {
if (m_generation_interval)
sleep_ms(m_generation_interval);
porting::TriggerMemoryTrim();
ScopeProfiler sp(g_profiler, "Client: Mesh making (sum)");
// This generates the mesh:
MapBlockMesh *mesh_new = new MapBlockMesh(m_client, q->data);
MeshUpdateResult r;
@ -227,11 +228,19 @@ void MeshUpdateWorkerThread::doUpdate()
r.solid_sides = get_solid_sides(q->data);
r.ack_list = std::move(q->ack_list);
r.urgent = q->urgent;
r.map_blocks = q->map_blocks;
r.map_blocks = std::move(q->map_blocks);
m_manager->putResult(r);
m_queue_in->done(q->p);
delete q;
sp.stop();
porting::TriggerMemoryTrim();
// do this after we're done so the interval is enforced without
// adding extra latency.
if (m_generation_interval)
sleep_ms(m_generation_interval);
}
}
@ -244,12 +253,12 @@ MeshUpdateManager::MeshUpdateManager(Client *client):
{
int number_of_threads = rangelim(g_settings->getS32("mesh_generation_threads"), 0, 8);
// Automatically use 33% of the system cores for mesh generation, max 4
// Automatically use 25% of the system cores for mesh generation, max 3
if (number_of_threads == 0)
number_of_threads = MYMIN(4, Thread::getNumberOfProcessors() / 3);
number_of_threads = std::min(3U, Thread::getNumberOfProcessors() / 4);
// use at least one thread
number_of_threads = MYMAX(1, number_of_threads);
number_of_threads = std::max(1, number_of_threads);
infostream << "MeshUpdateManager: using " << number_of_threads << " threads" << std::endl;
for (int i = 0; i < number_of_threads; i++)
@ -262,7 +271,7 @@ void MeshUpdateManager::updateBlock(Map *map, v3s16 p, bool ack_block_to_server,
static thread_local const bool many_neighbors =
g_settings->getBool("smooth_lighting")
&& !g_settings->getFlag("performance_tradeoffs");
if (!m_queue_in.addBlock(map, p, ack_block_to_server, urgent)) {
if (!m_queue_in.addBlock(map, p, ack_block_to_server, urgent, false)) {
warningstream << "Update requested for non-existent block at "
<< p << std::endl;
return;
@ -270,10 +279,10 @@ void MeshUpdateManager::updateBlock(Map *map, v3s16 p, bool ack_block_to_server,
if (update_neighbors) {
if (many_neighbors) {
for (v3s16 dp : g_26dirs)
m_queue_in.addBlock(map, p + dp, false, urgent);
m_queue_in.addBlock(map, p + dp, false, urgent, true);
} else {
for (v3s16 dp : g_6dirs)
m_queue_in.addBlock(map, p + dp, false, urgent);
m_queue_in.addBlock(map, p + dp, false, urgent, true);
}
}
deferUpdate();

View file

@ -27,6 +27,19 @@ struct QueuedMeshUpdate
QueuedMeshUpdate() = default;
~QueuedMeshUpdate();
/**
* Get blocks needed for this mesh update from the map.
* Blocks that were already loaded are skipped.
* @param map Map
* @param cell_size mesh grid cell size
*/
void retrieveBlocks(Map *map, u16 cell_size);
/**
* Drop block references.
* @note not done by destructor, since this is only safe on main thread
*/
void dropBlocks();
};
/*
@ -45,9 +58,16 @@ public:
~MeshUpdateQueue();
// Caches the block at p and its neighbors (if needed) and queues a mesh
// update for the block at p
bool addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
/**
* Caches the block at p and its neighbors (if needed) and queues a mesh
* update for the block p.
* @param map Map
* @param p block position
* @param ack_to_server Should be acked to server when done?
* @param urget High-priority?
* @param from_neighbor was this update only necessary due to a neighbor change?
*/
bool addBlock(Map *map, v3s16 p, bool ack_to_server, bool urgent, bool from_neighbor);
// Returned pointer must be deleted
// Returns NULL if queue is empty
@ -56,7 +76,7 @@ public:
// Marks a position as finished, unblocking the next update
void done(v3s16 pos);
u32 size()
size_t size()
{
MutexAutoLock lock(m_mutex);
return m_queue.size();
@ -117,6 +137,7 @@ public:
void updateBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent,
bool update_neighbors = false);
void putResult(const MeshUpdateResult &r);
/// @note caller needs to refDrop() the affected map_blocks
bool getNextResult(MeshUpdateResult &r);

View file

@ -713,9 +713,8 @@ void Minimap::updateActiveMarkers()
//// MinimapMapblock
////
void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos)
void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const NodeDefManager *nodedef, const v3s16 &pos)
{
for (s16 x = 0; x < MAP_BLOCKSIZE; x++)
for (s16 z = 0; z < MAP_BLOCKSIZE; z++) {
s16 air_count = 0;
@ -725,11 +724,12 @@ void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos
for (s16 y = MAP_BLOCKSIZE -1; y >= 0; y--) {
v3s16 p(x, y, z);
MapNode n = vmanip->getNodeNoEx(pos + p);
if (!surface_found && n.getContent() != CONTENT_AIR) {
const ContentFeatures &f = nodedef->get(n);
if (!surface_found && f.drawtype != NDT_AIRLIKE) {
mmpixel->height = y;
mmpixel->n = n;
surface_found = true;
} else if (n.getContent() == CONTENT_AIR) {
} else if (f.drawtype == NDT_AIRLIKE) {
air_count++;
}
}

View file

@ -66,7 +66,7 @@ struct MinimapPixel {
};
struct MinimapMapblock {
void getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos);
void getMinimapNodes(VoxelManipulator *vmanip, const NodeDefManager *nodedef, const v3s16 &pos);
MinimapPixel data[MAP_BLOCKSIZE * MAP_BLOCKSIZE];
};

View file

@ -187,6 +187,7 @@ void set_default_settings()
USEKEY2("keymap_fullscreen", "SYSTEM_SCANCODE_68", "KEY_F11");
USEKEY2("keymap_increase_viewing_range_min", "SYSTEM_SCANCODE_46", "+");
USEKEY2("keymap_decrease_viewing_range_min", "SYSTEM_SCANCODE_45", "-");
settings->setDefault("keymap_close_world", "");
USEKEY2("keymap_slot1", "SYSTEM_SCANCODE_30", "KEY_KEY_1");
USEKEY2("keymap_slot2", "SYSTEM_SCANCODE_31", "KEY_KEY_2");
USEKEY2("keymap_slot3", "SYSTEM_SCANCODE_32", "KEY_KEY_3");

View file

@ -31,9 +31,11 @@ public:
for (s16 y = bpmin.Y; y <= bpmax.Y; y++) {
MapBlock *block = getBlockNoCreateNoEx({x, y, z});
if (block) {
auto *data = block->getData();
for (size_t i = 0; i < MapBlock::nodecount; i++)
data[i] = n;
for (s16 zn=0; zn < MAP_BLOCKSIZE; zn++)
for (s16 yn=0; yn < MAP_BLOCKSIZE; yn++)
for (s16 xn=0; xn < MAP_BLOCKSIZE; xn++) {
block->setNodeNoCheck(xn, yn, zn, n);
}
block->expireIsAirCache();
}
}

View file

@ -20,7 +20,7 @@ inline struct tm mt_localtime()
#endif
});
struct tm ret;
struct tm ret{};
time_t t = time(NULL);
// TODO we should check if the function returns NULL, which would mean error
#ifdef _WIN32

View file

@ -33,6 +33,8 @@
#include "client/sound/sound_openal.h"
#endif
#include <csignal>
/******************************************************************************/
void TextDestGuiEngine::gotText(const StringMap &fields)
@ -104,7 +106,7 @@ GUIEngine::GUIEngine(JoystickController *joystick,
RenderingEngine *rendering_engine,
IMenuManager *menumgr,
MainMenuData *data,
bool &kill) :
volatile std::sig_atomic_t &kill) :
m_rendering_engine(rendering_engine),
m_parent(parent),
m_menumanager(menumgr),

View file

@ -14,6 +14,8 @@
#include "util/enriched_string.h"
#include "translation.h"
#include <csignal>
/******************************************************************************/
/* Structs and macros */
/******************************************************************************/
@ -124,7 +126,7 @@ public:
RenderingEngine *rendering_engine,
IMenuManager *menumgr,
MainMenuData *data,
bool &kill);
volatile std::sig_atomic_t &kill);
/** default destructor */
virtual ~GUIEngine();
@ -193,7 +195,7 @@ private:
irr_ptr<GUIFormSpecMenu> m_menu;
/** reference to kill variable managed by SIGINT handler */
bool &m_kill;
volatile std::sig_atomic_t &m_kill;
/** variable used to abort menu and return back to main game handling */
bool m_startgame = false;

View file

@ -2852,12 +2852,13 @@ void GUIFormSpecMenu::parseModel(parserData *data, const std::string &element)
e->enableContinuousRotation(inf_rotation);
e->enableMouseControl(mousectrl);
s32 frame_loop_begin = 0;
s32 frame_loop_end = 0x7FFFFFFF;
f32 frame_loop_begin = 0;
// This will be clamped to the animation duration.
f32 frame_loop_end = std::numeric_limits<f32>::infinity();
if (frame_loop.size() == 2) {
frame_loop_begin = stoi(frame_loop[0]);
frame_loop_end = stoi(frame_loop[1]);
frame_loop_begin = stof(frame_loop[0]);
frame_loop_end = stof(frame_loop[1]);
}
e->setFrameLoop(frame_loop_begin, frame_loop_end);
@ -4068,6 +4069,31 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
{
if (event.EventType==EET_KEY_INPUT_EVENT) {
KeyPress kp(event.KeyInput);
// Ctrl (+ Shift) + Tab: Select the (previous or) next tab of a TabControl instance.
bool shift = event.KeyInput.Shift;
bool ctrl = event.KeyInput.Control;
if (event.KeyInput.PressedDown && (event.KeyInput.Key == KEY_TAB && ctrl)) {
// Try to find a tab control among our elements
for (const FieldSpec &s : m_fields) {
if (s.ftype != f_TabHeader)
continue;
IGUIElement *element = getElementFromId(s.fid, true);
if (!element || element->getType() != gui::EGUIET_TAB_CONTROL)
continue;
gui::IGUITabControl *tabs = static_cast<gui::IGUITabControl *>(element);
s32 num_tabs = tabs->getTabCount();
if (num_tabs <= 1)
continue;
s32 active = tabs->getActiveTab();
// Shift: Previous tab, No shift: Next tab
active = (active + (shift ? -1 : 1) + num_tabs) % num_tabs;
tabs->setActiveTab(active);
return true; // handled
}
}
if (event.KeyInput.PressedDown && (
(kp == EscapeKey) ||
((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) {

View file

@ -205,6 +205,9 @@ bool GUIInventoryList::OnEvent(const SEvent &event)
bool ret = hovered->OnEvent(event);
// Set visible again *after* processing the event. Otherwise, hovered could
// be another GUIInventoryList, which will call this one again, resulting in
// an infinite loop.
IsVisible = was_visible;
return ret;

View file

@ -831,8 +831,10 @@ bool GUITable::OnEvent(const SEvent &event)
return true;
}
else if (event.KeyInput.Key == KEY_ESCAPE ||
event.KeyInput.Key == KEY_SPACE) {
event.KeyInput.Key == KEY_SPACE ||
(event.KeyInput.Key == KEY_TAB && event.KeyInput.Control)) {
// pass to parent
return IGUIElement::OnEvent(event);
}
else if (event.KeyInput.PressedDown && event.KeyInput.Char) {
// change selection based on text as it is typed

View file

@ -1138,7 +1138,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
return false;
}
ChatInterface iface;
bool &kill = *porting::signal_handler_killstatus();
volatile auto &kill = *porting::signal_handler_killstatus();
try {
// Create server
@ -1181,7 +1181,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
server.start();
// Run server
bool &kill = *porting::signal_handler_killstatus();
volatile auto &kill = *porting::signal_handler_killstatus();
dedicated_server_loop(server, kill);
} catch (const ModError &e) {
@ -1226,7 +1226,7 @@ static bool migrate_map_database(const GameParams &game_params, const Settings &
u32 count = 0;
u64 last_update_time = 0;
bool &kill = *porting::signal_handler_killstatus();
volatile auto &kill = *porting::signal_handler_killstatus();
std::vector<v3s16> blocks;
old_db->listAllLoadableBlocks(blocks);
@ -1280,7 +1280,7 @@ static bool recompress_map_database(const GameParams &game_params, const Setting
u32 count = 0;
u64 last_update_time = 0;
bool &kill = *porting::signal_handler_killstatus();
volatile auto &kill = *porting::signal_handler_killstatus();
const u8 serialize_as_ver = SER_FMT_VER_HIGHEST_WRITE;
const s16 map_compression_level = rangelim(g_settings->getS16("map_compression_level_disk"), -1, 9);

View file

@ -12,7 +12,6 @@
#include "gamedef.h"
#include "irrlicht_changes/printing.h"
#include "log.h"
#include "nameidmapping.h"
#include "content_mapnode.h" // For legacy name-id mapping
#include "content_nodemeta.h" // For legacy deserialization
#include "serialization.h"
@ -258,7 +257,7 @@ void MapBlock::expireIsAirCache()
// Renumbers the content IDs (starting at 0 and incrementing)
// Note that there's no technical reason why we *have to* renumber the IDs,
// but we do it anyway as it also helps compressability.
static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
void MapBlock::getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
const NodeDefManager *nodedef)
{
IdIdMapping &mapping = IdIdMapping::giveClearedThreadLocalInstance();
@ -288,7 +287,7 @@ static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
// Correct ids in the block to match nodedef based on names.
// Unknown ones are added to nodedef.
// Will not update itself to match id-name pairs in nodedef.
static void correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes,
void MapBlock::correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes,
IGameDef *gamedef)
{
const NodeDefManager *nodedef = gamedef->ndef();

View file

@ -21,6 +21,7 @@ class NodeMetadataList;
class IGameDef;
class MapBlockMesh;
class VoxelManipulator;
class NameIdMapping;
#define BLOCK_TIMESTAMP_UNDEFINED 0xffffffff
@ -80,11 +81,6 @@ public:
raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_REALLOCATE);
}
MapNode* getData()
{
return data;
}
////
//// Modification tracking methods
////
@ -427,18 +423,23 @@ public:
// clearObject and return removed objects count
u32 clearObjects();
private:
static const u32 ystride = MAP_BLOCKSIZE;
static const u32 zstride = MAP_BLOCKSIZE * MAP_BLOCKSIZE;
static const u32 nodecount = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE;
private:
/*
Private methods
*/
void deSerialize_pre22(std::istream &is, u8 version, bool disk);
static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
const NodeDefManager *nodedef);
static void correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes,
IGameDef *gamedef);
/*
* PLEASE NOTE: When adding something here be mindful of position and size
* of member variables! This is also the reason for the weird public-private

View file

@ -902,6 +902,7 @@ void Client::handleCommand_InventoryFormSpec(NetworkPacket* pkt)
// Store formspec in LocalPlayer
player->inventory_formspec = pkt->readLongString();
player->inventory_formspec_override.clear();
}
void Client::handleCommand_DetachedInventory(NetworkPacket* pkt)

View file

@ -178,6 +178,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
{ "TOCLIENT_STOP_SOUND", 0, true }, // 0x40
{ "TOCLIENT_PRIVILEGES", 0, true }, // 0x41
{ "TOCLIENT_INVENTORY_FORMSPEC", 0, true }, // 0x42
// ^ `channel` MUST be the same as TOCLIENT_SHOW_FORMSPEC
{ "TOCLIENT_DETACHED_INVENTORY", 0, true }, // 0x43
{ "TOCLIENT_SHOW_FORMSPEC", 0, true }, // 0x44
{ "TOCLIENT_MOVEMENT", 0, true }, // 0x45

View file

@ -4,7 +4,6 @@
#include "nodedef.h"
#include "SAnimatedMesh.h"
#include "itemdef.h"
#if CHECK_CLIENT_BUILD()
#include "client/mesh.h"
@ -964,13 +963,6 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc
// Note: By freshly reading, we get an unencumbered mesh.
if (scene::IMesh *src_mesh = client->getMesh(mesh)) {
bool apply_bs = false;
// For frame-animated meshes, always get the first frame,
// which holds a model for which we can eventually get the static pose.
while (auto *src_meshes = dynamic_cast<scene::SAnimatedMesh *>(src_mesh)) {
src_mesh = src_meshes->getMesh(0.0f);
src_mesh->grab();
src_meshes->drop();
}
if (auto *skinned_mesh = dynamic_cast<scene::SkinnedMesh *>(src_mesh)) {
// Compatibility: Animated meshes, as well as static gltf meshes, are not scaled by BS.
// See https://github.com/luanti-org/luanti/pull/16112#issuecomment-2881860329

View file

@ -60,6 +60,7 @@
#include "util/string.h"
#include "util/tracy_wrapper.h"
#include <vector>
#include <csignal>
#include <cstdarg>
#include <cstdio>
#include <signal.h>
@ -81,31 +82,28 @@ namespace porting
Signal handler (grabs Ctrl-C on POSIX systems)
*/
static bool g_killed = false;
volatile static std::sig_atomic_t g_killed = false;
bool *signal_handler_killstatus()
volatile std::sig_atomic_t *signal_handler_killstatus()
{
return &g_killed;
}
#if !defined(_WIN32) // POSIX
#define STDERR_FILENO 2
static void signal_handler(int sig)
{
if (!g_killed) {
if (sig == SIGINT) {
dstream << "INFO: signal_handler(): "
<< "Ctrl-C pressed, shutting down." << std::endl;
const char *dbg_text{"INFO: signal_handler(): "
"Ctrl-C pressed, shutting down.\n"};
write(STDERR_FILENO, dbg_text, strlen(dbg_text));
} else if (sig == SIGTERM) {
dstream << "INFO: signal_handler(): "
<< "got SIGTERM, shutting down." << std::endl;
const char *dbg_text{"INFO: signal_handler(): "
"got SIGTERM, shutting down.\n"};
write(STDERR_FILENO, dbg_text, strlen(dbg_text));
}
// Comment out for less clutter when testing scripts
/*dstream << "INFO: sigint_handler(): "
<< "Printing debug stacks" << std::endl;
debug_stacks_print();*/
g_killed = true;
} else {
(void)signal(sig, SIG_DFL);

View file

@ -13,6 +13,7 @@
#endif
// Be mindful of what you include here!
#include <csignal>
#include <string>
#include "config.h"
#include "irrlichttypes.h" // u64
@ -77,7 +78,7 @@ namespace porting
void signal_handler_init();
// Returns a pointer to a bool.
// When the bool is true, program should quit.
[[nodiscard]] bool *signal_handler_killstatus();
[[nodiscard]] volatile std::sig_atomic_t *signal_handler_killstatus();
/*
Path of static data directory.

View file

@ -17,7 +17,7 @@ ScopeProfiler::ScopeProfiler(Profiler *profiler, const std::string &name,
m_time1 = porting::getTime(prec);
}
ScopeProfiler::~ScopeProfiler()
void ScopeProfiler::stop() noexcept
{
if (!m_profiler)
return;
@ -38,6 +38,8 @@ ScopeProfiler::~ScopeProfiler()
m_profiler->max(m_name, duration);
break;
}
m_profiler = nullptr; // don't stop a second time
}
Profiler::Profiler()

View file

@ -13,6 +13,8 @@
#include "threading/mutex_auto_lock.h"
#include "util/timetaker.h"
#include "util/numeric.h" // paging()
/* FIXME: ^ move this to the .cpp file, it's not needed here */
#include "util/basic_macros.h"
// Global profiler
class Profiler;
@ -108,7 +110,12 @@ public:
ScopeProfiler(Profiler *profiler, const std::string &name,
ScopeProfilerType type = SPT_ADD,
TimePrecision precision = PRECISION_MILLI);
~ScopeProfiler();
inline ~ScopeProfiler() { stop(); }
// End profiled scope early
void stop() noexcept;
DISABLE_CLASS_COPY(ScopeProfiler)
private:
Profiler *m_profiler = nullptr;

View file

@ -121,6 +121,8 @@ public:
u16 protocol_version = 0;
u16 formspec_version = 0;
bool inventory_formspec_overridden = false;
/// returns PEER_ID_INEXISTENT when PlayerSAO is not ready
session_t getPeerId() const { return m_peer_id; }

Some files were not shown because too many files have changed in this diff Show more