1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00

Merge remote-tracking branch 'upstream/master' into Visuals-Vol-2

This commit is contained in:
Gefüllte Taubenbrust 2024-10-11 20:44:12 +02:00
commit b6c099073f
183 changed files with 3919 additions and 1642 deletions

2
.gitattributes vendored
View file

@ -3,3 +3,5 @@
*.cpp diff=cpp *.cpp diff=cpp
*.h diff=cpp *.h diff=cpp
*.gltf binary

View file

@ -88,7 +88,7 @@ jobs:
- name: Install deps - name: Install deps
run: | run: |
source ./util/ci/common.sh source ./util/ci/common.sh
install_linux_deps clang-7 llvm install_linux_deps clang-7 llvm-7
- name: Build - name: Build
run: | run: |
@ -102,6 +102,11 @@ jobs:
run: | run: |
./bin/minetest --run-unittests ./bin/minetest --run-unittests
# Do this here because we have ASan and error paths are sensitive to dangling pointers
- name: Test error cases
run: |
./util/test_error_cases.sh
# Current clang version # Current clang version
clang_18: clang_18:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View file

@ -29,8 +29,8 @@ on:
jobs: jobs:
build: build:
# use lowest possible macOS running on x86_64 to support more users # use lowest possible macOS running on x86_64 supported by brew to support more users
runs-on: macos-12 runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install deps - name: Install deps

3
.gitignore vendored
View file

@ -26,6 +26,7 @@ tags
!tags/ !tags/
gtags.files gtags.files
.idea .idea
.qtcreator/
# Codelite # Codelite
*.project *.project
# Visual Studio Code & plugins # Visual Studio Code & plugins
@ -109,6 +110,8 @@ src/cmake_config_githash.h
*.layout *.layout
*.o *.o
*.a *.a
*.dump
*.dmp
*.ninja *.ninja
.ninja* .ninja*
*.gch *.gch

View file

@ -57,12 +57,10 @@ srifqi:
textures/base/pack/minimap_btn.png textures/base/pack/minimap_btn.png
Zughy: Zughy:
textures/base/pack/cdb_add.png
textures/base/pack/cdb_downloading.png textures/base/pack/cdb_downloading.png
textures/base/pack/cdb_queued.png textures/base/pack/cdb_queued.png
textures/base/pack/cdb_update.png textures/base/pack/cdb_update.png
textures/base/pack/cdb_update_cropped.png textures/base/pack/cdb_update_cropped.png
textures/base/pack/cdb_viewonline.png
textures/base/pack/settings_btn.png textures/base/pack/settings_btn.png
textures/base/pack/settings_info.png textures/base/pack/settings_info.png
textures/base/pack/settings_reset.png textures/base/pack/settings_reset.png
@ -79,7 +77,6 @@ kilbith:
textures/base/pack/progress_bar_bg.png textures/base/pack/progress_bar_bg.png
SmallJoker: SmallJoker:
textures/base/pack/cdb_clear.png
textures/base/pack/server_favorite_delete.png (based on server_favorite.png) textures/base/pack/server_favorite_delete.png (based on server_favorite.png)
DS: DS:

View file

@ -166,20 +166,19 @@ function core.is_colored_paramtype(ptype)
end end
function core.strip_param2_color(param2, paramtype2) function core.strip_param2_color(param2, paramtype2)
if not core.is_colored_paramtype(paramtype2) then if paramtype2 == "color" then
return param2
elseif paramtype2 == "colorfacedir" then
return math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
return math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
return math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
return math.floor(param2 / 32) * 32
else
return nil return nil
end end
if paramtype2 == "colorfacedir" then
param2 = math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
param2 = math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
param2 = math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
param2 = math.floor(param2 / 32) * 32
end
-- paramtype2 == "color" requires no modification.
return param2
end end
-- Content ID caching -- Content ID caching

View file

@ -235,6 +235,16 @@ function core.formspec_escape(text)
end end
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
function core.hypertext_escape(text)
return text and text:gsub("[\\<>]", hypertext_escapes)
end
function core.wrap_text(text, max_length, as_table) function core.wrap_text(text, max_length, as_table)
local result = {} local result = {}
local line = {} local line = {}

View file

@ -66,13 +66,13 @@ local function get_formspec(self)
local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize)
local ENABLE_TOUCH = core.settings:get_bool("enable_touch") local TOUCH_GUI = core.settings:get_bool("touch_gui")
local orig_tsize = tab.tabsize or { width = self.width, height = self.height } local orig_tsize = tab.tabsize or { width = self.width, height = self.height }
local tsize = { width = orig_tsize.width, height = orig_tsize.height } local tsize = { width = orig_tsize.width, height = orig_tsize.height }
tsize.height = tsize.height tsize.height = tsize.height
+ TABHEADER_H -- tabheader included in formspec size + TABHEADER_H -- tabheader included in formspec size
+ (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
+ GAMEBAR_H -- gamebar included in formspec size + GAMEBAR_H -- gamebar included in formspec size
if self.parent == nil and not prepend then if self.parent == nil and not prepend then

View file

@ -44,6 +44,7 @@ core.features = {
override_item_remove_fields = true, override_item_remove_fields = true,
hotbar_hud_element = true, hotbar_hud_element = true,
bulk_lbms = true, bulk_lbms = true,
abm_without_neighbors = true,
} }
function core.has_feature(arg) function core.has_feature(arg)

View file

@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id)
end end
function contentdb.calculate_package_id(type, author, name)
local id = author:lower() .. "/"
if (type == nil or type == "game") and #name > 5 and name:sub(#name - 4) == "_game" then
id = id .. name:sub(1, #name - 5)
else
id = id .. name
end
return id
end
function contentdb.get_package_by_info(author, name)
local id = contentdb.calculate_package_id(nil, author, name)
return contentdb.package_by_id[id]
end
-- Create a coroutine from `fn` and provide results to `callback` when complete (dead). -- Create a coroutine from `fn` and provide results to `callback` when complete (dead).
-- Returns a resumer function. -- Returns a resumer function.
local function make_callback_coroutine(fn, callback) local function make_callback_coroutine(fn, callback)
@ -415,15 +432,7 @@ local function fetch_pkgs(params)
local aliases = {} local aliases = {}
for _, package in pairs(packages) do for _, package in pairs(packages) do
local name_len = #package.name package.id = params.calculate_package_id(package.type, package.author, package.name)
-- This must match what contentdb.update_paths() does!
package.id = package.author:lower() .. "/"
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
package.id = package.id .. package.name:sub(1, name_len - 5)
else
package.id = package.id .. package.name
end
package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name)
if package.aliases then if package.aliases then
@ -443,7 +452,7 @@ end
function contentdb.fetch_pkgs(callback) function contentdb.fetch_pkgs(callback)
contentdb.loading = true contentdb.loading = true
core.handle_async(fetch_pkgs, nil, function(result) core.handle_async(fetch_pkgs, { calculate_package_id = contentdb.calculate_package_id }, function(result)
if result then if result then
contentdb.load_ok = true contentdb.load_ok = true
contentdb.load_error = false contentdb.load_error = false
@ -581,3 +590,78 @@ function contentdb.filter_packages(query, by_type)
end end
end end
end end
function contentdb.get_full_package_info(package, callback)
assert(package)
if package.full_info then
callback(package.full_info)
return
end
local function fetch(params)
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local languages
local current_language = core.get_language()
if current_language ~= "" then
languages = { current_language, "en;q=0.8" }
else
languages = { "en" }
end
local url = base_url ..
"/api/packages/" .. params.package.url_part .. "/for-client/?" ..
"protocol_version=" .. core.urlencode(core.get_max_supp_proto()) ..
"&engine_version=" .. core.urlencode(version.string) ..
"&formspec_version=" .. core.urlencode(core.get_formspec_version()) ..
"&include_images=false"
local http = core.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
"Accept-Language: " .. table.concat(languages, ", ")
},
})
if not response.succeeded then
return nil
end
return core.parse_json(response.data)
end
local function my_callback(value)
package.full_info = value
callback(value)
end
if not core.handle_async(fetch, { package = package }, my_callback) then
core.log("error", "ERROR: async event failed")
callback(nil)
end
end
function contentdb.get_formspec_padding()
-- Padding is increased on Android to account for notches
-- TODO: use Android API to determine size of cut outs
return { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 }
end
function contentdb.get_formspec_size()
local window = core.get_window_info()
local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
-- Minimum formspec size
local min_x = 15.5
local min_y = 10
if size.x < min_x or size.y < min_y then
local scale = math.max(min_x / size.x, min_y / size.y)
size.x = size.x * scale
size.y = size.y * scale
end
return size
end

View file

@ -26,68 +26,20 @@ end
-- Filter -- Filter
local search_string = "" local search_string = ""
local cur_page = 1 local cur_page = 1
local num_per_page = 5 local filter_type
local filter_type = 1
local filter_types_titles = {
fgettext("All packages"),
fgettext("Games"),
fgettext("Mods"),
fgettext("Texture packs"),
}
-- Automatic package installation -- Automatic package installation
local auto_install_spec = nil local auto_install_spec = nil
local filter_types_type = {
nil, local filter_type_names = {
"game", { "type_all", nil },
"mod", { "type_game", "game" },
"txp", { "type_mod", "mod" },
{ "type_txp", "txp" },
} }
local function install_or_update_package(this, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
elseif package.type == "game" then
install_parent = core.get_gamepath()
elseif package.type == "txp" then
install_parent = core.get_texturepath()
else
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local dlg = create_install_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:show()
dlg:load_deps()
end
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(this)
this:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(this)
this:hide()
dlg:show()
else
on_confirm()
end
end
-- Resolves the package specification stored in auto_install_spec into an actual package. -- Resolves the package specification stored in auto_install_spec into an actual package.
-- May only be called after the package list has been loaded successfully. -- May only be called after the package list has been loaded successfully.
local function resolve_auto_install_spec() local function resolve_auto_install_spec()
@ -145,7 +97,7 @@ end
local function sort_and_filter_pkgs() local function sort_and_filter_pkgs()
contentdb.update_paths() contentdb.update_paths()
contentdb.sort_packages() contentdb.sort_packages()
contentdb.filter_packages(search_string, filter_types_type[filter_type]) contentdb.filter_packages(search_string, filter_type)
local auto_install_pkg = resolve_auto_install_spec() local auto_install_pkg = resolve_auto_install_spec()
if auto_install_pkg then if auto_install_pkg then
@ -176,72 +128,151 @@ local function load()
end end
local function get_info_formspec(text) local function get_info_formspec(size, padding, text)
local H = 9.5
return table.concat({ return table.concat({
"formspec_version[6]", "formspec_version[6]",
"size[15.75,9.5]", "size[", size.x, ",", size.y, "]",
core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]", "padding[0,0]",
"bgcolor[;true]",
"label[4,4.35;", text, "]", "label[", padding.x + 3.625, ",4.35;", text, "]",
"container[0,", H - 0.8 - 0.375, "]", "container[", padding.x, ",", size.y - 0.8 - padding.y, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", "button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container_end[]", "container_end[]",
}) })
end end
-- Determines how to fit `num_per_page` into `size` space
local function fit_cells(num_per_page, size)
local cell_spacing = 0.5
local columns = 1
local cell_w, cell_h
-- Fit cells into the available height
while true do
cell_w = (size.x - (columns-1)*cell_spacing) / columns
cell_h = cell_w / 4
local required_height = math.ceil(num_per_page / columns) * (cell_h + cell_spacing) - cell_spacing
-- Add 0.1 to be more lenient
if required_height <= size.y + 0.1 then
break
end
columns = columns + 1
end
return cell_spacing, columns, cell_w, cell_h
end
local function calculate_num_per_page()
local size = contentdb.get_formspec_size()
local padding = contentdb.get_formspec_padding()
local window = core.get_window_info()
size.x = size.x - padding.x * 2
size.y = size.y - padding.y * 2 - 1.425 - 0.25 - 0.8
local coordToPx = window.size.x / window.max_formspec_size.x / window.real_gui_scaling
local num_per_page = 12
while num_per_page > 2 do
local _, _, cell_w, _ = fit_cells(num_per_page, size)
if cell_w * coordToPx > 350 then
break
end
num_per_page = num_per_page - 1
end
return num_per_page
end
local function get_formspec(dlgdata) local function get_formspec(dlgdata)
local window_padding = contentdb.get_formspec_padding()
local size = contentdb.get_formspec_size()
if contentdb.loading then if contentdb.loading then
return get_info_formspec(fgettext("Loading...")) return get_info_formspec(size, window_padding, fgettext("Loading..."))
end end
if contentdb.load_error then if contentdb.load_error then
return get_info_formspec(fgettext("No packages could be retrieved")) return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved"))
end end
assert(contentdb.load_ok) assert(contentdb.load_ok)
contentdb.update_paths() contentdb.update_paths()
local num_per_page = dlgdata.num_per_page
dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1) dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1)
if cur_page > dlgdata.pagemax then if cur_page > dlgdata.pagemax then
cur_page = 1 cur_page = 1
end end
local W = 15.75 local W = size.x - window_padding.x * 2
local H = 9.5 local H = size.y - window_padding.y * 2
local category_x = 0
local number_category_buttons = 4
local max_button_w = (W - 0.375 - 0.25 - 7) / number_category_buttons
local category_button_w = math.min(max_button_w, 3)
local function make_category_button(name, label, selected)
category_x = category_x + 1
local color = selected and mt_color_green or ""
return ("style[%s;bgcolor=%s]button[%f,0;%f,0.8;%s;%s]"):format(name, color,
(category_x - 1) * category_button_w, category_button_w, name, label)
end
local selected_type = filter_type
local search_box_width = W - 0.375 - 0.25 - 2*0.8
- number_category_buttons * category_button_w
local formspec = { local formspec = {
"formspec_version[6]", "formspec_version[7]",
"size[15.75,9.5]", "size[", size.x, ",", size.y, "]",
core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "position[0.5,0.55]", "padding[0,0]",
"bgcolor[;true]",
"style[status,downloading,queued;border=false]", "container[", window_padding.x, ",", window_padding.y, "]",
"container[0.375,0.375]", -- Top-left: categories
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", make_category_button("type_all", fgettext("All"), selected_type == nil),
make_category_button("type_game", fgettext("Games"), selected_type == "game"),
make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"),
make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"),
-- Top-right: Search
"container[", W - search_box_width - 0.8*2, ",0]",
"field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]",
"field_enter_after_edit[search_string;true]", "field_enter_after_edit[search_string;true]",
"image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", "image_button[", search_box_width, ",0;0.8,0.8;",
"image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
"dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", "image_button[", search_box_width + 0.8, ",0;0.8,0.8;",
core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
"container_end[]", "container_end[]",
-- Page nav buttons -- Bottom strip start
"container[0,", H - 0.8 - 0.375, "]", "container[0,", H - 0.8, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]", "button[0,0;2,0.8;back;", fgettext("Back"), "]",
"container[", W - 0.375 - 0.8*4 - 2, ",0]", -- Bottom-center: Page nav buttons
"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", "container[", (W - 1*4 - 2) / 2, ",0]",
"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", "image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
"image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
"style[pagenum;border=false]", "style[pagenum;border=false]",
"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", "button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", "image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", "image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
"container_end[]", "container_end[]", -- page nav end
"container_end[]", -- Bottom-right: updating
"container[", W - 3, ",0]",
"style[status,downloading,queued;border=false]",
} }
if contentdb.number_downloading > 0 then if contentdb.number_downloading > 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;" formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;"
if #contentdb.download_queue > 0 then if #contentdb.download_queue > 0 then
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued",
contentdb.number_downloading, #contentdb.download_queue) contentdb.number_downloading, #contentdb.download_queue)
@ -260,16 +291,19 @@ local function get_formspec(dlgdata)
end end
if num_avail_updates == 0 then if num_avail_updates == 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;" formspec[#formspec + 1] = "button[0,0;3,0.8;status;"
formspec[#formspec + 1] = fgettext("No updates") formspec[#formspec + 1] = fgettext("No updates")
formspec[#formspec + 1] = "]" formspec[#formspec + 1] = "]"
else else
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;" formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;"
formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates) formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
formspec[#formspec + 1] = "]" formspec[#formspec + 1] = "]"
end end
end end
formspec[#formspec + 1] = "container_end[]" -- updating end
formspec[#formspec + 1] = "container_end[]" -- bottom strip end
if #contentdb.packages == 0 then if #contentdb.packages == 0 then
formspec[#formspec + 1] = "label[4,4.75;" formspec[#formspec + 1] = "label[4,4.75;"
formspec[#formspec + 1] = fgettext("No results") formspec[#formspec + 1] = fgettext("No results")
@ -281,80 +315,84 @@ local function get_formspec(dlgdata)
formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
formspec[#formspec + 1] = "container[0,1.425]"
local cell_spacing, columns, cell_w, cell_h = fit_cells(num_per_page, {
x = W,
y = H - 1.425 - 0.25 - 0.8
})
local img_w = cell_h * 3 / 2
local start_idx = (cur_page - 1) * num_per_page + 1 local start_idx = (cur_page - 1) * num_per_page + 1
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i] local package = contentdb.packages[i]
local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
formspec[#formspec + 1] = "container[0.375,"
formspec[#formspec + 1] = container_y
formspec[#formspec + 1] = "]"
-- image table.insert_all(formspec, {
formspec[#formspec + 1] = "image[0,0;1.5,1;" "container[",
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package)) (cell_w + cell_spacing) * ((i - start_idx) % columns),
formspec[#formspec + 1] = "]" ",",
(cell_h + cell_spacing) * math.floor((i - start_idx) / columns),
"]",
-- title "box[0,0;", cell_w, ",", cell_h, ";#ffffff11]",
formspec[#formspec + 1] = "label[1.875,0.1;"
formspec[#formspec + 1] = core.formspec_escape( -- image,
"image[0,0;", img_w, ",", cell_h, ";",
core.formspec_escape(get_screenshot(package, package.thumbnail, 2)), "]",
"label[", img_w + 0.25 + 0.05, ",0.5;",
core.formspec_escape(
core.colorize(mt_color_green, package.title) .. core.colorize(mt_color_green, package.title) ..
core.colorize("#BFBFBF", " by " .. package.author)) core.colorize("#BFBFBF", " by " .. package.author)), "]",
formspec[#formspec + 1] = "]"
-- buttons "textarea[", img_w + 0.25, ",0.75;", cell_w - img_w - 0.25, ",", cell_h - 0.75, ";;;",
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15 core.formspec_escape(package.short_description), "]",
local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) "style[view_", i, ";border=false]",
local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) "style[view_", i, ":hovered;bgimg=", core.formspec_escape(defaulttexturedir .. "button_hover_semitrans.png"), "]",
formspec[#formspec + 1] = "container[" "style[view_", i, ":pressed;bgimg=", core.formspec_escape(defaulttexturedir .. "button_press_semitrans.png"), "]",
formspec[#formspec + 1] = W - 0.375*2 "button[0,0;", cell_w, ",", cell_h, ";view_", i, ";]",
formspec[#formspec + 1] = ",0.1]" })
if package.featured then
table.insert_all(formspec, {
"tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]",
"image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]",
})
end
table.insert_all(formspec, {
"container[", cell_w - 0.625,",", 0.25, "]",
})
if package.downloading then if package.downloading then
formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;" table.insert_all(formspec, {
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) "animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]",
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" })
elseif package.queued then elseif package.queued then
formspec[#formspec + 1] = second_base table.insert_all(formspec, {
formspec[#formspec + 1] = "cdb_queued.png;queued;]" "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]",
elseif not package.path then })
local elem_name = "install_" .. i .. ";" elseif package.path then
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
else
if package.installed_release < package.release then if package.installed_release < package.release then
-- The install_ action also handles updating table.insert_all(formspec, {
local elem_name = "install_" .. i .. ";" "image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]",
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]" })
formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]" else
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors table.insert_all(formspec, {
"image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]",
description_width = description_width - 0.7 - 0.15 })
end
end end
local elem_name = "uninstall_" .. i .. ";" table.insert_all(formspec, {
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" "container_end[]",
formspec[#formspec + 1] = second_base .. "cdb_clear.png;" .. elem_name .. "]" "container_end[]",
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors })
end end
local web_elem_name = "view_" .. i .. ";"
formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" ..
core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. web_elem_name ..
fgettext("View more information in a web browser") .. tooltip_colors
formspec[#formspec + 1] = "container_end[]"
-- description
formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;"
formspec[#formspec + 1] = core.formspec_escape(package.short_description)
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "container_end[]" formspec[#formspec + 1] = "container_end[]"
end formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec) return table.concat(formspec)
end end
@ -364,14 +402,14 @@ local function handle_submit(this, fields)
if fields.search or fields.key_enter_field == "search_string" then if fields.search or fields.key_enter_field == "search_string" then
search_string = fields.search_string:trim() search_string = fields.search_string:trim()
cur_page = 1 cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type]) contentdb.filter_packages(search_string, filter_type)
return true return true
end end
if fields.clear then if fields.clear then
search_string = "" search_string = ""
cur_page = 1 cur_page = 1
contentdb.filter_packages("", filter_types_type[filter_type]) contentdb.filter_packages("", filter_type)
return true return true
end end
@ -407,12 +445,11 @@ local function handle_submit(this, fields)
return true return true
end end
if fields.type then for _, pair in ipairs(filter_type_names) do
local new_type = table.indexof(filter_types_titles, fields.type) if fields[pair[1]] then
if new_type ~= filter_type then filter_type = pair[2]
filter_type = new_type
cur_page = 1 cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type]) contentdb.filter_packages(search_string, filter_type)
return true return true
end end
end end
@ -428,32 +465,20 @@ local function handle_submit(this, fields)
return true return true
end end
local num_per_page = this.data.num_per_page
local start_idx = (cur_page - 1) * num_per_page + 1 local start_idx = (cur_page - 1) * num_per_page + 1
assert(start_idx ~= nil) assert(start_idx ~= nil)
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i] local package = contentdb.packages[i]
assert(package) assert(package)
if fields["install_" .. i] then if fields["view_" .. i] or fields["title_" .. i] or fields["author_" .. i] then
install_or_update_package(this, package) local dlg = create_package_dialog(package)
return true
end
if fields["uninstall_" .. i] then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this) dlg:set_parent(this)
this:hide() this:hide()
dlg:show() dlg:show()
return true return true
end end
if fields["view_" .. i] then
local url = ("%s/packages/%s?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
end end
return false return false
@ -462,8 +487,8 @@ end
local function handle_events(event) local function handle_events(event)
if event == "DialogShow" then if event == "DialogShow" then
-- On touchscreen, don't show the "MINETEST" header behind the dialog. -- Don't show the "MINETEST" header behind the dialog.
mm_game_theme.set_engine(core.settings:get_bool("touch_gui")) mm_game_theme.set_engine(true)
-- If ContentDB is already loaded, auto-install packages here. -- If ContentDB is already loaded, auto-install packages here.
do_auto_install() do_auto_install()
@ -471,6 +496,11 @@ local function handle_events(event)
return true return true
end end
if event == "WindowInfoChange" then
ui.update()
return true
end
return false return false
end end
@ -485,17 +515,7 @@ end
function create_contentdb_dlg(type, install_spec) function create_contentdb_dlg(type, install_spec)
search_string = "" search_string = ""
cur_page = 1 cur_page = 1
if type then filter_type = type
-- table.indexof does not work on tables that contain `nil`
for i, v in pairs(filter_types_type) do
if v == type then
filter_type = i
break
end
end
else
filter_type = 1
end
-- Keep the old auto_install_spec if the caller doesn't specify one. -- Keep the old auto_install_spec if the caller doesn't specify one.
if install_spec then if install_spec then
@ -504,8 +524,10 @@ function create_contentdb_dlg(type, install_spec)
load() load()
return dialog_create("contentdb", local dlg = dialog_create("contentdb",
get_formspec, get_formspec,
handle_submit, handle_submit,
handle_events) handle_events)
dlg.data.num_per_page = calculate_num_per_page()
return dlg
end end

View file

@ -244,3 +244,45 @@ function create_install_dialog(package)
return dlg return dlg
end end
function install_or_update_package(parent, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
elseif package.type == "game" then
install_parent = core.get_gamepath()
elseif package.type == "txp" then
install_parent = core.get_texturepath()
else
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local dlg = create_install_dialog(package)
dlg:set_parent(parent)
parent:hide()
dlg:show()
dlg:load_deps()
end
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(parent)
parent:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(parent)
parent:hide()
dlg:show()
else
on_confirm()
end
end

View file

@ -0,0 +1,333 @@
--Minetest
--Copyright (C) 2018-24 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local function get_info_formspec(size, padding, text)
return table.concat({
"formspec_version[6]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"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[]",
})
end
local function get_formspec(data)
local window_padding = contentdb.get_formspec_padding()
local size = contentdb.get_formspec_size()
size.x = math.min(size.x, 20)
local W = size.x - window_padding.x * 2
local H = size.y - window_padding.y * 2
if not data.info then
if not data.loading and not data.loading_error then
data.loading = true
contentdb.get_full_package_info(data.package, function(info)
data.loading = false
if info == nil then
data.loading_error = true
ui.update()
return
end
if info.forums then
info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums
end
assert(data.package.name == info.name)
data.info = info
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("No packages could be retrieved"))
end
return get_info_formspec(size, window_padding, fgettext("Loading..."))
end
end
-- Check installation status
contentdb.update_paths()
local info = data.info
local info_line =
fgettext("by $1 — $2 downloads — +$3 / $4 / -$5",
info.author, info.downloads,
info.reviews.positive, info.reviews.neutral, info.reviews.negative)
local bottom_buttons_y = H - 0.8
local formspec = {
"formspec_version[7]",
"size[", size.x, ",", size.y, "]",
"padding[0,0]",
"bgcolor[;true]",
"container[", window_padding.x, ",", window_padding.y, "]",
"button[0,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]",
"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), "]",
"style_type[label;font_size=;font=]",
"label[0,1.2;", core.formspec_escape(info_line), "]",
}
table.insert_all(formspec, {
"container[", W - 6, ",0]"
})
local left_button_rect = "0,0;2.875,1"
local right_button_rect = "3.125,0;2.875,1"
if data.package.downloading then
formspec[#formspec + 1] = "animated_image[5,0;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
elseif data.package.queued then
formspec[#formspec + 1] = "style[queued;border=false]"
formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not data.package.path then
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] = "]"
else
if data.package.installed_release < data.package.release then
-- The install_ action also handles updating
formspec[#formspec + 1] = "style[install;bgcolor=#28ccdf]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = left_button_rect
formspec[#formspec + 1] = ";install;"
formspec[#formspec + 1] = fgettext("Update")
formspec[#formspec + 1] = "]"
end
formspec[#formspec + 1] = "style[uninstall;bgcolor=#a93b3b]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] = ";uninstall;"
formspec[#formspec + 1] = fgettext("Uninstall")
formspec[#formspec + 1] = "]"
end
local current_tab = data.current_tab or 1
local tab_titles = {
fgettext("Description"),
fgettext("Information"),
}
local tab_body_height = bottom_buttons_y - 2.8
table.insert_all(formspec, {
"container_end[]",
"box[0,2.55;", W, ",", tab_body_height, ";#ffffff11]",
"tabheader[0,2.55;", W, ",0.8;tabs;",
table.concat(tab_titles, ","), ";", current_tab, ";true;true]",
"container[0,2.8]",
})
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(data.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"), "forums")
hypertext = hypertext .. "\n\n" .. info.long_description.body
hypertext = hypertext:gsub("<img name=\"?blank.png\"? ",
"<img name=\"" .. core.hypertext_escape(defaulttexturedir) .. "blank.png\" ")
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";desc;", core.formspec_escape(hypertext), "]",
})
elseif current_tab == 2 then
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), "]",
})
else
error("Unknown tab " .. current_tab)
end
formspec[#formspec + 1] = "container_end[]"
formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec)
end
local function handle_hypertext_event(this, event, hypertext_object)
if not (event and event:sub(1, 7) == "action:") then
return
end
for i, ss in ipairs(this.data.info.screenshots) do
if event == "action:ss_" .. i then
core.open_url(ss.url)
return true
end
end
local base_url = core.settings:get("contentdb_url"):gsub("(%W)", "%%%1")
for key, url in pairs(hypertext_object.links) do
if event == "action:" .. key then
local author, name = url:match("^" .. base_url .. "/?packages/([A-Za-z0-9 _-]+)/([a-z0-9_]+)/?$")
if author and name then
local package2 = contentdb.get_package_by_info(author, name)
if package2 then
local dlg = create_package_dialog(package2)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
end
core.open_url_dialog(url)
return true
end
end
end
local function handle_submit(this, fields)
local info = this.data.info
local package = this.data.package
if fields.back then
this:delete()
return true
end
if not info then
return false
end
if fields.open_contentdb then
local url = ("%s/packages/%s/?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
if fields.install then
install_or_update_package(this, package)
return true
end
if fields.uninstall then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields.tabs then
this.data.current_tab = tonumber(fields.tabs)
return true
end
if handle_hypertext_event(this, fields.desc, info.long_description) or
handle_hypertext_event(this, fields.info, info.info_hypertext) then
return true
end
end
local function handle_events(event)
if event == "WindowInfoChange" then
ui.update()
return true
end
return false
end
function create_package_dialog(package)
assert(package)
local dlg = dialog_create("package_dialog_" .. package.id,
get_formspec,
handle_submit,
handle_events)
local data = dlg.data
data.package = package
data.info = nil
data.loading = false
data.loading_error = nil
data.current_tab = 1
return dlg
end

View file

@ -23,4 +23,5 @@ dofile(path .. DIR_DELIM .. "update_detector.lua")
dofile(path .. DIR_DELIM .. "screenshots.lua") dofile(path .. DIR_DELIM .. "screenshots.lua")
dofile(path .. DIR_DELIM .. "dlg_install.lua") dofile(path .. DIR_DELIM .. "dlg_install.lua")
dofile(path .. DIR_DELIM .. "dlg_overwrite.lua") dofile(path .. DIR_DELIM .. "dlg_overwrite.lua")
dofile(path .. DIR_DELIM .. "dlg_package.lua")
dofile(path .. DIR_DELIM .. "dlg_contentdb.lua") dofile(path .. DIR_DELIM .. "dlg_contentdb.lua")

View file

@ -23,23 +23,40 @@ local screenshot_downloading = {}
local screenshot_downloaded = {} local screenshot_downloaded = {}
local function get_filename(path)
local parts = path:split("/")
return parts[#parts]
end
local function get_file_extension(path) local function get_file_extension(path)
local parts = path:split(".") local parts = path:split(".")
return parts[#parts] return parts[#parts]
end end
function get_screenshot(package) function get_screenshot(package, screenshot_url, level)
if not package.thumbnail then if not screenshot_url then
return defaulttexturedir .. "no_screenshot.png" return defaulttexturedir .. "no_screenshot.png"
elseif screenshot_downloading[package.thumbnail] then end
-- Minetest only supports png and jpg
local ext = get_file_extension(screenshot_url)
if ext ~= "png" and ext ~= "jpg" then
screenshot_url = screenshot_url:sub(0, -#ext - 1) .. "png"
end
-- Set thumbnail level
screenshot_url = screenshot_url:gsub("/thumbnails/[0-9]+/", "/thumbnails/" .. level .. "/")
screenshot_url = screenshot_url:gsub("/uploads/", "/thumbnails/" .. level .. "/")
if screenshot_downloading[screenshot_url] then
return defaulttexturedir .. "loading_screenshot.png" return defaulttexturedir .. "loading_screenshot.png"
end end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM .. local filepath = screenshot_dir .. DIR_DELIM ..
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext) ("%s-%s-%s-l%d-%s"):format(package.type, package.author, package.name,
level, get_filename(screenshot_url))
-- Return if already downloaded -- Return if already downloaded
local file = io.open(filepath, "r") local file = io.open(filepath, "r")
@ -49,7 +66,7 @@ function get_screenshot(package)
end end
-- Show error if we've failed to download before -- Show error if we've failed to download before
if screenshot_downloaded[package.thumbnail] then if screenshot_downloaded[screenshot_url] then
return defaulttexturedir .. "error_screenshot.png" return defaulttexturedir .. "error_screenshot.png"
end end
@ -59,16 +76,16 @@ function get_screenshot(package)
return core.download_file(params.url, params.dest) return core.download_file(params.url, params.dest)
end end
local function callback(success) local function callback(success)
screenshot_downloading[package.thumbnail] = nil screenshot_downloading[screenshot_url] = nil
screenshot_downloaded[package.thumbnail] = true screenshot_downloaded[screenshot_url] = true
if not success then if not success then
core.log("warning", "Screenshot download failed for some reason") core.log("warning", "Screenshot download failed for some reason")
end end
ui.update() ui.update()
end end
if core.handle_async(download_screenshot, if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then { dest = filepath, url = screenshot_url }, callback) then
screenshot_downloading[package.thumbnail] = true screenshot_downloading[screenshot_url] = true
else else
core.log("error", "ERROR: async event failed") core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png" return defaulttexturedir .. "error_screenshot.png"

View file

@ -67,6 +67,19 @@ function make.heading(text)
end end
function make.note(text)
return {
full_width = true,
get_formspec = function(self, avail_w)
-- Assuming label height 0.4:
-- Position at y=0 to eat 0.2 of the padding above, leave 0.05.
-- The returned used_height doesn't include padding.
return ("label[0,0;%s]"):format(core.colorize("#bbb", core.formspec_escape(text))), 0.2
end,
}
end
--- Used for string and numeric style fields --- Used for string and numeric style fields
--- ---
--- @param converter Function to coerce values from strings. --- @param converter Function to coerce values from strings.

View file

@ -152,9 +152,24 @@ local function load()
table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys) table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys)
do do
local content = page_by_id.graphics_and_audio_shaders.content local content = page_by_id.graphics_and_audio_effects.content
local idx = table.indexof(content, "enable_dynamic_shadows") local idx = table.indexof(content, "enable_dynamic_shadows")
table.insert(content, idx, shadows_component) table.insert(content, idx, shadows_component)
idx = table.indexof(content, "enable_auto_exposure") + 1
local note = component_funcs.note(fgettext_ne("(The game will need to enable automatic exposure as well)"))
note.requires = get_setting_info("enable_auto_exposure").requires
table.insert(content, idx, note)
idx = table.indexof(content, "enable_bloom") + 1
note = component_funcs.note(fgettext_ne("(The game will need to enable bloom as well)"))
note.requires = get_setting_info("enable_bloom").requires
table.insert(content, idx, note)
idx = table.indexof(content, "enable_volumetric_lighting") + 1
note = component_funcs.note(fgettext_ne("(The game will need to enable volumetric lighting as well)"))
note.requires = get_setting_info("enable_volumetric_lighting").requires
table.insert(content, idx, note)
end end
-- These must not be translated, as they need to show in the local -- These must not be translated, as they need to show in the local
@ -222,6 +237,12 @@ local function load()
zh_CN = "中文 (简体) [zh_CN]", zh_CN = "中文 (简体) [zh_CN]",
zh_TW = "正體中文 (繁體) [zh_TW]", zh_TW = "正體中文 (繁體) [zh_TW]",
} }
get_setting_info("touch_controls").option_labels = {
["auto"] = fgettext_ne("Auto"),
["true"] = fgettext_ne("Enabled"),
["false"] = fgettext_ne("Disabled"),
}
end end
@ -321,9 +342,14 @@ local function check_requirements(name, requires)
local video_driver = core.get_active_driver() local video_driver = core.get_active_driver()
local shaders_support = video_driver == "opengl" or video_driver == "opengl3" or video_driver == "ogles2" local shaders_support = video_driver == "opengl" or video_driver == "opengl3" or video_driver == "ogles2"
local touch_controls = core.settings:get("touch_controls")
local special = { local special = {
android = PLATFORM == "Android", android = PLATFORM == "Android",
desktop = PLATFORM ~= "Android", desktop = PLATFORM ~= "Android",
-- When touch_controls is "auto", we don't which input method will be used,
-- so we show settings for both.
touchscreen = touch_controls == "auto" or core.is_yes(touch_controls),
keyboard_mouse = touch_controls == "auto" or not core.is_yes(touch_controls),
shaders_support = shaders_support, shaders_support = shaders_support,
shaders = core.settings:get_bool("enable_shaders") and shaders_support, shaders = core.settings:get_bool("enable_shaders") and shaders_support,
opengl = video_driver == "opengl", opengl = video_driver == "opengl",
@ -433,19 +459,6 @@ local function build_page_components(page)
end end
--- Creates a scrollbaroptions for a scroll_container
--
-- @param visible_l the length of the scroll_container and scrollbar
-- @param total_l length of the scrollable area
-- @param scroll_factor as passed to scroll_container
local function make_scrollbaroptions_for_scroll_container(visible_l, total_l, scroll_factor)
assert(total_l >= visible_l)
local max = total_l - visible_l
local thumb_size = (visible_l / total_l) * max
return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor)
end
local formspec_show_hack = false local formspec_show_hack = false
@ -507,8 +520,8 @@ local function get_formspec(dialogdata)
"tooltip[search;", fgettext("Search"), "]", "tooltip[search;", fgettext("Search"), "]",
"tooltip[search_clear;", fgettext("Clear"), "]", "tooltip[search_clear;", fgettext("Clear"), "]",
"container_end[]", "container_end[]",
"scroll_container[0.25,1.25;", tostring(left_pane_width), ",", ("scroll_container[0.25,1.25;%f,%f;leftscroll;vertical;0.1;0]"):format(
tostring(tabsize.height - 1.5), ";leftscroll;vertical;0.1]", left_pane_width, tabsize.height - 1.5),
"style_type[button;border=false;bgcolor=#3333]", "style_type[button;border=false;bgcolor=#3333]",
"style_type[button:hover;border=false;bgcolor=#6663]", "style_type[button:hover;border=false;bgcolor=#6663]",
} }
@ -538,7 +551,6 @@ local function get_formspec(dialogdata)
fs[#fs + 1] = "scroll_container_end[]" fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height - 1.25 then if y >= tabsize.height - 1.25 then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height - 1.5, y, 0.1)
fs[#fs + 1] = ("scrollbar[%f,1.25;%f,%f;vertical;leftscroll;%f]"):format( fs[#fs + 1] = ("scrollbar[%f,1.25;%f,%f;vertical;leftscroll;%f]"):format(
left_pane_width + 0.25, scrollbar_w, tabsize.height - 1.5, dialogdata.leftscroll or 0) left_pane_width + 0.25, scrollbar_w, tabsize.height - 1.5, dialogdata.leftscroll or 0)
end end
@ -550,7 +562,7 @@ local function get_formspec(dialogdata)
end end
local right_pane_width = tabsize.width - left_pane_width - 0.375 - 2*scrollbar_w - 0.25 local right_pane_width = tabsize.width - left_pane_width - 0.375 - 2*scrollbar_w - 0.25
fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1]"):format( fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1;0.25]"):format(
tabsize.width - right_pane_width - scrollbar_w, right_pane_width, tabsize.height) tabsize.width - right_pane_width - scrollbar_w, right_pane_width, tabsize.height)
y = 0.25 y = 0.25
@ -606,7 +618,6 @@ local function get_formspec(dialogdata)
fs[#fs + 1] = "scroll_container_end[]" fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height then if y >= tabsize.height then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height, y + 0.375, 0.1)
fs[#fs + 1] = ("scrollbar[%f,0;%f,%f;vertical;rightscroll;%f]"):format( fs[#fs + 1] = ("scrollbar[%f,0;%f,%f;vertical;rightscroll;%f]"):format(
tabsize.width - scrollbar_w, scrollbar_w, tabsize.height, dialogdata.rightscroll or 0) tabsize.width - scrollbar_w, scrollbar_w, tabsize.height, dialogdata.rightscroll or 0)
end end
@ -624,6 +635,18 @@ function write_settings_early()
end end
end end
local function regenerate_page_list(dialogdata)
local suggested_page_id = update_filtered_pages(dialogdata.query)
dialogdata.components = nil
if not filtered_page_by_id[dialogdata.page_id] then
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = suggested_page_id
end
end
local function buttonhandler(this, fields) local function buttonhandler(this, fields)
local dialogdata = this.data local dialogdata = this.data
@ -648,27 +671,7 @@ local function buttonhandler(this, fields)
local value = core.is_yes(fields.show_advanced) local value = core.is_yes(fields.show_advanced)
core.settings:set_bool("show_advanced", value) core.settings:set_bool("show_advanced", value)
write_settings_early() write_settings_early()
end regenerate_page_list(dialogdata)
-- touch_controls is a checkbox in a setting component. We handle this
-- setting differently so we can hide/show pages using the next if-statement
if fields.touch_controls ~= nil then
local value = core.is_yes(fields.touch_controls)
core.settings:set_bool("touch_controls", value)
write_settings_early()
end
if fields.show_advanced ~= nil or fields.touch_controls ~= nil then
local suggested_page_id = update_filtered_pages(dialogdata.query)
dialogdata.components = nil
if not filtered_page_by_id[dialogdata.page_id] then
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = suggested_page_id
end
return true return true
end end
@ -701,20 +704,26 @@ local function buttonhandler(this, fields)
end end
end end
for i, comp in ipairs(dialogdata.components) do local function after_setting_change(comp)
if comp.on_submit and comp:on_submit(fields, this) then
write_settings_early() write_settings_early()
if comp.setting.name == "touch_controls" then
-- Changing the "touch_controls" setting may result in a different
-- page list.
regenerate_page_list(dialogdata)
else
-- Clear components so they regenerate -- Clear components so they regenerate
dialogdata.components = nil dialogdata.components = nil
end
end
for i, comp in ipairs(dialogdata.components) do
if comp.on_submit and comp:on_submit(fields, this) then
after_setting_change(comp)
return true return true
end end
if comp.setting and fields["reset_" .. i] then if comp.setting and fields["reset_" .. i] then
core.settings:remove(comp.setting.name) core.settings:remove(comp.setting.name)
write_settings_early() after_setting_change(comp)
-- Clear components so they regenerate
dialogdata.components = nil
return true return true
end end
end end

View file

@ -19,12 +19,7 @@
local function prepare_credits(dest, source) local function prepare_credits(dest, source)
local string = table.concat(source, "\n") .. "\n" local string = table.concat(source, "\n") .. "\n"
local hypertext_escapes = { string = core.hypertext_escape(string)
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
string = string:gsub("[\\<>]", hypertext_escapes)
string = string:gsub("%[.-%]", "<gray>%1</gray>") string = string:gsub("%[.-%]", "<gray>%1</gray>")
table.insert(dest, string) table.insert(dest, string)

View file

@ -92,11 +92,11 @@ function singleplayer_refresh_gamebar()
end end
end end
local ENABLE_TOUCH = core.settings:get_bool("enable_touch") local TOUCH_GUI = core.settings:get_bool("touch_gui")
local gamebar_pos_y = MAIN_TAB_H local gamebar_pos_y = MAIN_TAB_H
+ TABHEADER_H -- tabheader included in formspec size + TABHEADER_H -- tabheader included in formspec size
+ (ENABLE_TOUCH and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP) + (TOUCH_GUI and GAMEBAR_OFFSET_TOUCH or GAMEBAR_OFFSET_DESKTOP)
local btnbar = buttonbar_create( local btnbar = buttonbar_create(
"game_button_bar", "game_button_bar",

View file

@ -61,7 +61,7 @@
# #
# # This is a comment # # This is a comment
# # # #
# # Requires: shaders, enable_dynamic_shadows, !touch_controls # # Requires: shaders, enable_dynamic_shadows, !enable_waving_leaves
# name (Readable name) type type_args # name (Readable name) type type_args
# #
# A requirement can be the name of a boolean setting or an engine-defined value. # A requirement can be the name of a boolean setting or an engine-defined value.
@ -72,6 +72,7 @@
# * shaders_support (a video driver that supports shaders, may not be enabled) # * shaders_support (a video driver that supports shaders, may not be enabled)
# * shaders (both enable_shaders and shaders_support) # * shaders (both enable_shaders and shaders_support)
# * desktop / android # * desktop / android
# * touchscreen / keyboard_mouse
# * opengl / gles # * opengl / gles
# * You can negate any requirement by prepending with ! # * You can negate any requirement by prepending with !
# #
@ -91,7 +92,7 @@ camera_smoothing (Camera smoothing) float 0.0 0.0 0.99
# Smooths rotation of camera when in cinematic mode, 0 to disable. Enter cinematic mode by using the key set in Controls. # Smooths rotation of camera when in cinematic mode, 0 to disable. Enter cinematic mode by using the key set in Controls.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
cinematic_camera_smoothing (Camera smoothing in cinematic mode) float 0.7 0.0 0.99 cinematic_camera_smoothing (Camera smoothing in cinematic mode) float 0.7 0.0 0.99
# If enabled, you can place nodes at the position (feet + eye level) where you stand. # If enabled, you can place nodes at the position (feet + eye level) where you stand.
@ -112,8 +113,8 @@ always_fly_fast (Always fly fast) bool true
# The time in seconds it takes between repeated node placements when holding # The time in seconds it takes between repeated node placements when holding
# the place button. # the place button.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
repeat_place_time (Place repetition interval) float 0.25 0.16 2.0 repeat_place_time (Place repetition interval) float 0.25 0.15 2.0
# The minimum time in seconds it takes between digging nodes when holding # The minimum time in seconds it takes between digging nodes when holding
# the dig button. # the dig button.
@ -131,60 +132,62 @@ safe_dig_and_place (Safe digging and placing) bool false
# Invert vertical mouse movement. # Invert vertical mouse movement.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
invert_mouse (Invert mouse) bool false invert_mouse (Invert mouse) bool false
# Mouse sensitivity multiplier. # Mouse sensitivity multiplier.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0 mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0
# Enable mouse wheel (scroll) for item selection in hotbar. # Enable mouse wheel (scroll) for item selection in hotbar.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
enable_hotbar_mouse_wheel (Hotbar: Enable mouse wheel for selection) bool true enable_hotbar_mouse_wheel (Hotbar: Enable mouse wheel for selection) bool true
# Invert mouse wheel (scroll) direction for item selection in hotbar. # Invert mouse wheel (scroll) direction for item selection in hotbar.
# #
# Requires: !touch_controls # Requires: keyboard_mouse
invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false
[*Touchscreen] [*Touchscreen]
# Enables the touchscreen controls, allowing you to play the game with a touchscreen. # Enables the touchscreen controls, allowing you to play the game with a touchscreen.
touch_controls (Enable touchscreen controls) bool true # "auto" means that the touchscreen controls will be enabled and disabled
# automatically depending on the last used input method.
touch_controls (Touchscreen controls) enum auto auto,true,false
# Touchscreen sensitivity multiplier. # Touchscreen sensitivity multiplier.
# #
# Requires: touch_controls # Requires: touchscreen
touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0 touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0
# The length in pixels after which a touch interaction is considered movement. # The length in pixels after which a touch interaction is considered movement.
# #
# Requires: touch_controls # Requires: touchscreen
touchscreen_threshold (Movement threshold) int 20 0 100 touchscreen_threshold (Movement threshold) int 20 0 100
# The delay in milliseconds after which a touch interaction is considered a long tap. # The delay in milliseconds after which a touch interaction is considered a long tap.
# #
# Requires: touch_controls # Requires: touchscreen
touch_long_tap_delay (Threshold for long taps) int 400 100 1000 touch_long_tap_delay (Threshold for long taps) int 400 100 1000
# Use crosshair to select object instead of whole screen. # Use crosshair to select object instead of whole screen.
# If enabled, a crosshair will be shown and will be used for selecting object. # If enabled, a crosshair will be shown and will be used for selecting object.
# #
# Requires: touch_controls # Requires: touchscreen
touch_use_crosshair (Use crosshair for touch screen) bool false touch_use_crosshair (Use crosshair for touch screen) bool false
# Fixes the position of virtual joystick. # Fixes the position of virtual joystick.
# If disabled, virtual joystick will center to first-touch's position. # If disabled, virtual joystick will center to first-touch's position.
# #
# Requires: touch_controls # Requires: touchscreen
fixed_virtual_joystick (Fixed virtual joystick) bool false fixed_virtual_joystick (Fixed virtual joystick) bool false
# Use virtual joystick to trigger "Aux1" button. # Use virtual joystick to trigger "Aux1" button.
# If enabled, virtual joystick will also tap "Aux1" button when out of main circle. # If enabled, virtual joystick will also tap "Aux1" button when out of main circle.
# #
# Requires: touch_controls # Requires: touchscreen
virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false
# The gesture for punching players/entities. # The gesture for punching players/entities.
@ -197,7 +200,7 @@ virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool fals
# Known from the classic Minetest mobile controls. # Known from the classic Minetest mobile controls.
# Combat is more or less impossible. # Combat is more or less impossible.
# #
# Requires: touch_controls # Requires: touchscreen
touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap
@ -262,31 +265,6 @@ viewing_range (Viewing range) int 190 20 4000
# Higher values result in a less detailed image. # Higher values result in a less detailed image.
undersampling (Undersampling) int 1 1 8 undersampling (Undersampling) int 1 1 8
[**Graphics Effects]
# Allows liquids to be translucent.
translucent_liquids (Translucent liquids) bool true
# Leaves style:
# - Fancy: all faces visible
# - Simple: only outer faces, if defined special_tiles are used
# - Opaque: disable transparency
leaves_style (Leaves style) enum fancy fancy,simple,opaque
# Connects glass if supported by node.
connected_glass (Connect glass) bool false
# Enable smooth lighting with simple ambient occlusion.
# Disable for speed or for different looks.
smooth_lighting (Smooth lighting) bool true
# Enables tradeoffs that reduce CPU load or increase rendering performance
# at the expense of minor visual glitches that do not impact game playability.
performance_tradeoffs (Tradeoffs for performance) bool false
# Adds particles when digging a node.
enable_particles (Digging particles) bool true
[**3D] [**3D]
# 3D support. # 3D support.
@ -466,13 +444,29 @@ enable_raytraced_culling (Enable Raytraced Culling) bool true
[*Shaders] [*Effects]
# Shaders allow advanced visual effects and may increase performance on some video # Allows liquids to be translucent.
# cards. translucent_liquids (Translucent liquids) bool true
#
# Requires: shaders_support # Leaves style:
enable_shaders (Shaders) bool true # - Fancy: all faces visible
# - Simple: only outer faces
# - Opaque: disable transparency
leaves_style (Leaves style) enum fancy fancy,simple,opaque
# Connects glass if supported by node.
connected_glass (Connect glass) bool false
# Enable smooth lighting with simple ambient occlusion.
smooth_lighting (Smooth lighting) bool true
# Enables tradeoffs that reduce CPU load or increase rendering performance
# at the expense of minor visual glitches that do not impact game playability.
performance_tradeoffs (Tradeoffs for performance) bool false
# Adds particles when digging a node.
enable_particles (Digging particles) bool true
[**Waving Nodes] [**Waving Nodes]
@ -649,42 +643,12 @@ enable_vignette (Vignette) bool false
# Requires: shaders, enable_post_processing # Requires: shaders, enable_post_processing
debanding (Enable Debanding) bool true debanding (Enable Debanding) bool true
[**Bloom]
# Set to true to enable bloom effect. # Set to true to enable bloom effect.
# Bright colors will bleed over the neighboring objects. # Bright colors will bleed over the neighboring objects.
# #
# Requires: shaders, enable_post_processing # Requires: shaders, enable_post_processing
enable_bloom (Enable Bloom) bool false enable_bloom (Enable Bloom) bool false
# Set to true to render debugging breakdown of the bloom effect.
# In debug mode, the screen is split into 4 quadrants:
# top-left - processed base image, top-right - final image
# bottom-left - raw base image, bottom-right - bloom texture.
#
# Requires: shaders, enable_post_processing, enable_bloom
enable_bloom_debug (Enable Bloom Debug) bool false
# Defines how much bloom is applied to the rendered image
# Smaller values make bloom more subtle
# Range: from 0.01 to 1.0, default: 0.05
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_intensity (Bloom Intensity) float 0.05 0.01 1.0
# Defines the magnitude of bloom overexposure.
# Range: from 0.1 to 10.0, default: 1.0
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_strength_factor (Bloom Strength Factor) float 1.0 0.1 10.0
# Logical value that controls how far the bloom effect spreads
# from the bright objects.
# Range: from 0.1 to 8, default: 1
#
# Requires: shaders, enable_post_processing, enable_bloom
bloom_radius (Bloom Radius) float 1 0.1 8
# Set to true to enable volumetric lighting effect (a.k.a. "Godrays"). # Set to true to enable volumetric lighting effect (a.k.a. "Godrays").
# #
# Requires: shaders, enable_post_processing, enable_bloom # Requires: shaders, enable_post_processing, enable_bloom
@ -713,6 +677,11 @@ enable_translucent_foliage (Translucent foliage) bool false
# Requires: shaders, enable_dynamic_shadows # Requires: shaders, enable_dynamic_shadows
enable_node_specular (Node specular) bool false enable_node_specular (Node specular) bool false
# When enabled, liquid reflections are simulated.
#
# Requires: shaders, enable_waving_water, enable_dynamic_shadows
enable_water_reflections (Liquid reflections) bool false
[*Audio] [*Audio]
# Volume of all sounds. # Volume of all sounds.
@ -950,8 +919,13 @@ default_privs (Default privileges) string interact, shout
# Privileges that players with basic_privs can grant # Privileges that players with basic_privs can grant
basic_privs (Basic privileges) string interact, shout basic_privs (Basic privileges) string interact, shout
# If enabled, disable cheat prevention in multiplayer. # Server anticheat configuration.
disable_anticheat (Disable anticheat) bool false # Flags are positive. Uncheck the flag to disable corresponding anticheat module.
anticheat_flags (Anticheat flags) flags digging,interaction,movement digging,interaction,movement
# Tolerance of movement cheat detector.
# Increase the value if players experience stuttery movement.
anticheat_movement_tolerance (Anticheat movement tolerance) float 1.0 1.0
# If enabled, actions are recorded for rollback. # If enabled, actions are recorded for rollback.
# This option is only read when server starts. # This option is only read when server starts.
@ -1896,6 +1870,11 @@ ignore_world_load_errors (Ignore world errors) bool false
[**Graphics] [**Graphics]
# Shaders are a fundamental part of rendering and enable advanced visual effects.
#
# Requires: shaders_support
enable_shaders (Shaders) bool true
# Path to shader directory. If no path is defined, default location will be used. # Path to shader directory. If no path is defined, default location will be used.
# #
# Requires: shaders # Requires: shaders
@ -1919,6 +1898,7 @@ cloud_radius (Cloud radius) int 12 1 62
desynchronize_mapblock_texture_animation (Desynchronize block animation) bool false desynchronize_mapblock_texture_animation (Desynchronize block animation) bool false
# Enables caching of facedir rotated meshes. # Enables caching of facedir rotated meshes.
# This is only effective with shaders disabled.
enable_mesh_cache (Mesh cache) bool false enable_mesh_cache (Mesh cache) bool false
# Delay between mesh updates on the client in ms. Increasing this will slow # Delay between mesh updates on the client in ms. Increasing this will slow
@ -1970,6 +1950,14 @@ client_mesh_chunk (Client Mesh Chunksize) int 1 1 16
# Enables debug and error-checking in the OpenGL driver. # Enables debug and error-checking in the OpenGL driver.
opengl_debug (OpenGL debug) bool false opengl_debug (OpenGL debug) bool false
# Set to true to render debugging breakdown of the bloom effect.
# In debug mode, the screen is split into 4 quadrants:
# top-left - processed base image, top-right - final image
# bottom-left - raw base image, bottom-right - bloom texture.
#
# Requires: shaders, enable_post_processing, enable_bloom
enable_bloom_debug (Enable Bloom Debug) bool false
[**Sound] [**Sound]
# Comma-separated list of AL and ALC extensions that should not be used. # Comma-separated list of AL and ALC extensions that should not be used.
# Useful for testing. See al_extensions.[h,cpp] for details. # Useful for testing. See al_extensions.[h,cpp] for details.

View file

@ -8,11 +8,7 @@ void main(void)
{ {
gl_Position = mWorldViewProj * inVertexPosition; gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor; vec4 color = inVertexColor;
#endif
color *= materialColor; color *= materialColor;
varColor = color; varColor = color;

View file

@ -3,9 +3,5 @@ varying lowp vec4 varColor;
void main(void) void main(void)
{ {
gl_Position = mWorldViewProj * inVertexPosition; gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor; varColor = inVertexColor;
#endif
} }

View file

@ -7,9 +7,5 @@ void main(void)
{ {
varTexCoord = inTexCoord0.st; varTexCoord = inTexCoord0.st;
gl_Position = mWorldViewProj * inVertexPosition; gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor; varColor = inVertexColor;
#endif
} }

View file

@ -211,15 +211,11 @@ void main(void)
vNormal = inVertexNormal; vNormal = inVertexNormal;
// Calculate color. // Calculate color.
vec4 color = inVertexColor;
// Red, green and blue components are pre-multiplied with // Red, green and blue components are pre-multiplied with
// the brightness, so now we have to multiply these // the brightness, so now we have to multiply these
// colors with the color of the incoming light. // colors with the color of the incoming light.
// The pre-baked colors are halved to prevent overflow. // The pre-baked colors are halved to prevent overflow.
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor;
#endif
// The alpha gives the ratio of sunlight in the incoming light. // The alpha gives the ratio of sunlight in the incoming light.
nightRatio = 1.0 - color.a; nightRatio = 1.0 - color.a;
color.rgb = color.rgb * (color.a * dayLight.rgb + color.rgb = color.rgb * (color.a * dayLight.rgb +

View file

@ -124,11 +124,7 @@ void main(void)
: directional_ambient(normalize(inVertexNormal)); : directional_ambient(normalize(inVertexNormal));
#endif #endif
#ifdef GL_ES
vec4 color = inVertexColor.bgra;
#else
vec4 color = inVertexColor; vec4 color = inVertexColor;
#endif
color *= materialColor; color *= materialColor;

View file

@ -6,9 +6,5 @@ void main(void)
varTexCoord = inTexCoord0.st; varTexCoord = inTexCoord0.st;
gl_Position = mWorldViewProj * inVertexPosition; gl_Position = mWorldViewProj * inVertexPosition;
#ifdef GL_ES
varColor = inVertexColor.bgra;
#else
varColor = inVertexColor; varColor = inVertexColor;
#endif
} }

View file

@ -274,7 +274,7 @@ Accepted formats are:
images: .png, .jpg, .tga, (deprecated:) .bmp images: .png, .jpg, .tga, (deprecated:) .bmp
sounds: .ogg vorbis sounds: .ogg vorbis
models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer) models: .x, .b3d, .obj, (since version 5.10:) .gltf, .glb
Other formats won't be sent to the client (e.g. you can store .blend files Other formats won't be sent to the client (e.g. you can store .blend files
in a folder for convenience, without the risk that such files are transferred) in a folder for convenience, without the risk that such files are transferred)
@ -294,7 +294,7 @@ depends on by supplying a file with an equal name.
Only a subset of model file format features is supported: Only a subset of model file format features is supported:
Simple textured meshes (with multiple textures), optionally with normals. Simple textured meshes (with multiple textures), optionally with normals.
The .x and .b3d formats additionally support skeletal animation. The .x, .b3d and .gltf formats additionally support (a single) animation.
#### glTF #### glTF
@ -302,9 +302,15 @@ The glTF model file format for now only serves as a
more modern alternative to the other static model file formats; more modern alternative to the other static model file formats;
it unlocks no special rendering features. it unlocks no special rendering features.
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
due to their space savings.
This means that many glTF features are not supported *yet*, including: This means that many glTF features are not supported *yet*, including:
* Animation * Animations
* Only a single animation is supported,
use frame ranges within this animation.
* Only integer frames are supported.
* Cameras * Cameras
* Materials * Materials
* Only base color textures are supported * Only base color textures are supported
@ -490,6 +496,11 @@ to let the client generate textures on-the-fly.
The modifiers are applied directly in sRGB colorspace, The modifiers are applied directly in sRGB colorspace,
i.e. without gamma-correction. i.e. without gamma-correction.
### Notes
* `TEXMOD_UPSCALE`: The texture with the lower resolution will be automatically
upscaled to the higher resolution texture.
### Texture overlaying ### Texture overlaying
Textures can be overlaid by putting a `^` between them. Textures can be overlaid by putting a `^` between them.
@ -503,8 +514,9 @@ Example:
default_dirt.png^default_grass_side.png default_dirt.png^default_grass_side.png
`default_grass_side.png` is overlaid over `default_dirt.png`. `default_grass_side.png` is overlaid over `default_dirt.png`.
The texture with the lower resolution will be automatically upscaled to
the higher resolution texture. *See notes: `TEXMOD_UPSCALE`*
### Texture grouping ### Texture grouping
@ -701,6 +713,8 @@ Apply a mask to the base image.
The mask is applied using binary AND. The mask is applied using binary AND.
*See notes: `TEXMOD_UPSCALE`*
#### `[sheet:<w>x<h>:<x>,<y>` #### `[sheet:<w>x<h>:<x>,<y>`
Retrieves a tile at position x, y (in tiles, 0-indexed) Retrieves a tile at position x, y (in tiles, 0-indexed)
@ -798,6 +812,8 @@ in GIMP. Overlay is the same as Hard light but with the role of the two
textures swapped, see the `[hardlight` modifier description for more detail textures swapped, see the `[hardlight` modifier description for more detail
about these blend modes. about these blend modes.
*See notes: `TEXMOD_UPSCALE`*
#### `[hardlight:<file>` #### `[hardlight:<file>`
Applies a Hard light blend with the two textures, like the Hard light layer Applies a Hard light blend with the two textures, like the Hard light layer
@ -813,6 +829,8 @@ increase contrast without clipping.
Hard light is the same as Overlay but with the roles of the two textures Hard light is the same as Overlay but with the roles of the two textures
swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png` swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png`
*See notes: `TEXMOD_UPSCALE`*
#### `[png:<base64>` #### `[png:<base64>`
Embed a base64 encoded PNG image in the texture string. Embed a base64 encoded PNG image in the texture string.
@ -831,6 +849,8 @@ In particular consider `minetest.dynamic_add_media` and test whether
using other texture modifiers could result in a shorter string than using other texture modifiers could result in a shorter string than
embedding a whole image, this may vary by use case. embedding a whole image, this may vary by use case.
*See notes: `TEXMOD_UPSCALE`*
Hardware coloring Hardware coloring
----------------- -----------------
@ -1394,16 +1414,19 @@ The function of `param2` is determined by `paramtype2` in node definition.
The palette should have 256 pixels. The palette should have 256 pixels.
* `paramtype2 = "colorfacedir"` * `paramtype2 = "colorfacedir"`
* Same as `facedir`, but with colors. * Same as `facedir`, but with colors.
* The first three bits of `param2` tells which color is picked from the * The three most significant bits of `param2` tells which color is picked from the
palette. The palette should have 8 pixels. palette. The palette should have 8 pixels.
* The five least significant bits contain the `facedir` value.
* `paramtype2 = "color4dir"` * `paramtype2 = "color4dir"`
* Same as `facedir`, but with colors. * Same as `4dir`, but with colors.
* The first six bits of `param2` tells which color is picked from the * The six most significant bits of `param2` tells which color is picked from the
palette. The palette should have 64 pixels. palette. The palette should have 64 pixels.
* The two least significant bits contain the `4dir` rotation.
* `paramtype2 = "colorwallmounted"` * `paramtype2 = "colorwallmounted"`
* Same as `wallmounted`, but with colors. * Same as `wallmounted`, but with colors.
* The first five bits of `param2` tells which color is picked from the * The five most significant bits of `param2` tells which color is picked from the
palette. The palette should have 32 pixels. palette. The palette should have 32 pixels.
* The three least significant bits contain the `wallmounted` value.
* `paramtype2 = "glasslikeliquidlevel"` * `paramtype2 = "glasslikeliquidlevel"`
* Only valid for "glasslike_framed" or "glasslike_framed_optional" * Only valid for "glasslike_framed" or "glasslike_framed_optional"
drawtypes. "glasslike_framed_optional" nodes are only affected if the drawtypes. "glasslike_framed_optional" nodes are only affected if the
@ -1417,9 +1440,9 @@ The function of `param2` is determined by `paramtype2` in node definition.
* Liquid texture is defined using `special_tiles = {"modname_tilename.png"}` * Liquid texture is defined using `special_tiles = {"modname_tilename.png"}`
* `paramtype2 = "colordegrotate"` * `paramtype2 = "colordegrotate"`
* Same as `degrotate`, but with colors. * Same as `degrotate`, but with colors.
* The first (most-significant) three bits of `param2` tells which color * The three most significant bits of `param2` tells which color is picked
is picked from the palette. The palette should have 8 pixels. from the palette. The palette should have 8 pixels.
* Remaining 5 bits store rotation in range 023 (i.e. in 15° steps) * The five least significant bits store rotation in range 023 (i.e. in 15° steps)
* `paramtype2 = "none"` * `paramtype2 = "none"`
* `param2` will not be used by the engine and can be used to store * `param2` will not be used by the engine and can be used to store
an arbitrary value an arbitrary value
@ -1470,7 +1493,8 @@ Look for examples in `games/devtest` or `games/minetest_game`.
'Connected Glass'. 'Connected Glass'.
* `allfaces` * `allfaces`
* Often used for partially-transparent nodes. * Often used for partially-transparent nodes.
* External and internal sides of textures are visible. * External sides of textures, and unlike other drawtypes, the external sides
of other blocks, are visible from the inside.
* `allfaces_optional` * `allfaces_optional`
* Often used for leaves nodes. * Often used for leaves nodes.
* This switches between `normal`, `glasslike` and `allfaces` according to * This switches between `normal`, `glasslike` and `allfaces` according to
@ -2729,6 +2753,8 @@ Version History
* Formspec version 7 (5.8.0): * Formspec version 7 (5.8.0):
* style[]: Add focused state for buttons * style[]: Add focused state for buttons
* Add field_enter_after_edit[] (experimental) * Add field_enter_after_edit[] (experimental)
* Formspec version 8 (5.10.0)
* scroll_container[]: content padding parameter
Elements Elements
-------- --------
@ -2812,7 +2838,7 @@ Elements
* End of a container, following elements are no longer relative to this * End of a container, following elements are no longer relative to this
container. container.
### `scroll_container[<X>,<Y>;<W>,<H>;<scrollbar name>;<orientation>;<scroll factor>]` ### `scroll_container[<X>,<Y>;<W>,<H>;<scrollbar name>;<orientation>;<scroll factor>;<content padding>]`
* Start of a scroll_container block. All contained elements will ... * Start of a scroll_container block. All contained elements will ...
* take the scroll_container coordinate as position origin, * take the scroll_container coordinate as position origin,
@ -2821,6 +2847,12 @@ Elements
* be clipped to the rectangle defined by `X`, `Y`, `W` and `H`. * be clipped to the rectangle defined by `X`, `Y`, `W` and `H`.
* `orientation`: possible values are `vertical` and `horizontal`. * `orientation`: possible values are `vertical` and `horizontal`.
* `scroll factor`: optional, defaults to `0.1`. * `scroll factor`: optional, defaults to `0.1`.
* `content padding`: (optional), in formspec coordinate units
* If specified, the scrollbar properties `max` and `thumbsize` are calculated automatically
based on the content size plus `content padding` at the end of the container. `min` is set to 0.
* Negative `scroll factor` is not supported.
* When active, `scrollbaroptions[]` has no effect on the affected properties.
* Defaults to empty value (= disabled).
* Nesting is possible. * Nesting is possible.
* Some elements might work a little different if they are in a scroll_container. * Some elements might work a little different if they are in a scroll_container.
* Note: If you want the scroll_container to actually work, you also need to add a * Note: If you want the scroll_container to actually work, you also need to add a
@ -5524,6 +5556,8 @@ Utilities
hotbar_hud_element = true, hotbar_hud_element = true,
-- Bulk LBM support (5.10.0) -- Bulk LBM support (5.10.0)
bulk_lbms = true, bulk_lbms = true,
-- ABM supports field without_neighbors (5.10.0)
abm_without_neighbors = true,
} }
``` ```
@ -5847,8 +5881,13 @@ Call these functions only at load time!
* `clicker`: ObjectRef - Object that acted upon `player`, may or may not be a player * `clicker`: ObjectRef - Object that acted upon `player`, may or may not be a player
* `minetest.register_on_player_hpchange(function(player, hp_change, reason), modifier)` * `minetest.register_on_player_hpchange(function(player, hp_change, reason), modifier)`
* Called when the player gets damaged or healed * Called when the player gets damaged or healed
* When `hp == 0`, damage doesn't trigger this callback.
* When `hp == hp_max`, healing does still trigger this callback.
* `player`: ObjectRef of the player * `player`: ObjectRef of the player
* `hp_change`: the amount of change. Negative when it is damage. * `hp_change`: the amount of change. Negative when it is damage.
* Historically, the new HP value was clamped to [0, 65535] before
calculating the HP change. This clamping has been removed as of
Minetest 5.10.0
* `reason`: a PlayerHPChangeReason table. * `reason`: a PlayerHPChangeReason table.
* The `type` field will have one of the following values: * The `type` field will have one of the following values:
* `set_hp`: A mod or the engine called `set_hp` without * `set_hp`: A mod or the engine called `set_hp` without
@ -6556,6 +6595,9 @@ Formspec
* `minetest.formspec_escape(string)`: returns a string * `minetest.formspec_escape(string)`: returns a string
* escapes the characters "[", "]", "\", "," and ";", which cannot be used * escapes the characters "[", "]", "\", "," and ";", which cannot be used
in formspecs. in formspecs.
* `minetest.hypertext_escape(string)`: returns a string
* escapes the characters "\", "<", and ">" to show text in a hypertext element.
* not safe for use with tag attributes.
* `minetest.explode_table_event(string)`: returns a table * `minetest.explode_table_event(string)`: returns a table
* returns e.g. `{type="CHG", row=1, column=2}` * returns e.g. `{type="CHG", row=1, column=2}`
* `type` is one of: * `type` is one of:
@ -6813,17 +6855,6 @@ This allows you easy interoperability for delegating work to jobs.
* Register a path to a Lua file to be imported when an async environment * Register a path to a Lua file to be imported when an async environment
is initialized. You can use this to preload code which you can then call is initialized. You can use this to preload code which you can then call
later using `minetest.handle_async()`. later using `minetest.handle_async()`.
* `minetest.register_portable_metatable(name, mt)`:
* Register a metatable that should be preserved when data is transferred
between the main thread and the async environment.
* `name` is a string that identifies the metatable. It is recommended to
follow the `modname:name` convention for this identifier.
* `mt` is the metatable to register.
* Note that it is allowed to register the same metatable under multiple
names, but it is not allowed to register multiple metatables under the
same name.
* You must register the metatable in both the main environment
and the async environment for this mechanism to work.
### List of APIs available in an async environment ### List of APIs available in an async environment
@ -6853,7 +6884,8 @@ Functions:
* Standalone helpers such as logging, filesystem, encoding, * Standalone helpers such as logging, filesystem, encoding,
hashing or compression APIs hashing or compression APIs
* `minetest.register_portable_metatable` (see above) * `minetest.register_portable_metatable`
* IPC
Variables: Variables:
@ -6931,6 +6963,7 @@ Functions:
* `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`, * `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`,
`spawn_tree` and similar `spawn_tree` and similar
* these only operate on the current chunk (if inside a callback) * these only operate on the current chunk (if inside a callback)
* IPC
Variables: Variables:
@ -7008,6 +7041,52 @@ Server
this can make transfer of bigger files painless (if set up). Nevertheless this can make transfer of bigger files painless (if set up). Nevertheless
it is advised not to use dynamic media for big media files. it is advised not to use dynamic media for big media files.
IPC
---
The engine provides a generalized mechanism to enable sharing data between the
different Lua environments (main, mapgen and async).
It is essentially a shared in-memory key-value store.
* `minetest.ipc_get(key)`:
* Read a value from the shared data area.
* `key`: string, should use the `"modname:thing"` convention to avoid conflicts.
* returns an arbitrary Lua value, or `nil` if this key does not exist
* `minetest.ipc_set(key, value)`:
* Write a value to the shared data area.
* `key`: as above
* `value`: an arbitrary Lua value, cannot be or contain userdata.
Interacting with the shared data will perform an operation comparable to
(de)serialization on each access.
For that reason modifying references will not have any effect, as in this example:
```lua
minetest.ipc_set("test:foo", {})
minetest.ipc_get("test:foo").subkey = "value" -- WRONG!
minetest.ipc_get("test:foo") -- returns an empty table
```
**Advanced**:
* `minetest.ipc_cas(key, old_value, new_value)`:
* Write a value to the shared data area, but only if the previous value
equals what was given.
This operation is called Compare-and-Swap and can be used to implement
synchronization between threads.
* `key`: as above
* `old_value`: value compared to using `==` (`nil` compares equal for non-existing keys)
* `new_value`: value that will be set
* returns: true on success, false otherwise
* `minetest.ipc_poll(key, timeout)`:
* Do a blocking wait until a value (other than `nil`) is present at the key.
* **IMPORTANT**: You usually don't need this function. Use this as a last resort
if nothing else can satisfy your use case! None of the Lua environments the
engine has are safe to block for extended periods, especially on the main
thread any delays directly translate to lag felt by players.
* `key`: as above
* `timeout`: maximum wait time, in milliseconds (positive values only)
* returns: true on success, false on timeout
Bans Bans
---- ----
@ -7407,6 +7486,17 @@ Misc.
* `minetest.global_exists(name)` * `minetest.global_exists(name)`
* Checks if a global variable has been set, without triggering a warning. * Checks if a global variable has been set, without triggering a warning.
* `minetest.register_portable_metatable(name, mt)`:
* Register a metatable that should be preserved when Lua data is transferred
between environments (via IPC or `handle_async`).
* `name` is a string that identifies the metatable. It is recommended to
follow the `modname:name` convention for this identifier.
* `mt` is the metatable to register.
* Note that the same metatable can be registered under multiple names,
but multiple metatables must not be registered under the same name.
* You must register the metatable in both the main environment
and the async environment for this mechanism to work.
Global objects Global objects
-------------- --------------
@ -8017,8 +8107,7 @@ child will follow movement and rotation of that bone.
* Animation interpolates towards the end frame but stops when it is reached * Animation interpolates towards the end frame but stops when it is reached
* If looped, there is no interpolation back to the start frame * If looped, there is no interpolation back to the start frame
* If looped, the model should look identical at start and end * If looped, the model should look identical at start and end
* Only integer numbers are supported * default: `{x=1.0, y=1.0}`
* default: `{x=1, y=1}`
* `frame_speed`: How fast the animation plays, in frames per second (number) * `frame_speed`: How fast the animation plays, in frames per second (number)
* default: `15.0` * default: `15.0`
* `frame_blend`: number, default: `0.0` * `frame_blend`: number, default: `0.0`
@ -8241,12 +8330,18 @@ child will follow movement and rotation of that bone.
bgcolor[], any non-style elements (eg: label) may result in weird behavior. bgcolor[], any non-style elements (eg: label) may result in weird behavior.
* Only affects formspecs shown after this is called. * Only affects formspecs shown after this is called.
* `get_formspec_prepend()`: returns a formspec string. * `get_formspec_prepend()`: returns a formspec string.
* `get_player_control()`: returns table with player pressed keys * `get_player_control()`: returns table with player input
* The table consists of fields with the following boolean values * The table contains the following boolean fields representing the pressed
representing the pressed keys: `up`, `down`, `left`, `right`, `jump`, keys: `up`, `down`, `left`, `right`, `jump`, `aux1`, `sneak`, `dig`,
`aux1`, `sneak`, `dig`, `place`, `LMB`, `RMB`, and `zoom`. `place`, `LMB`, `RMB` and `zoom`.
* The fields `LMB` and `RMB` are equal to `dig` and `place` respectively, * The fields `LMB` and `RMB` are equal to `dig` and `place` respectively,
and exist only to preserve backwards compatibility. and exist only to preserve backwards compatibility.
* The table also contains the fields `movement_x` and `movement_y`.
* They represent the movement of the player. Values are numbers in the
range [-1.0,+1.0].
* They take both keyboard and joystick input into account.
* You should prefer them over `up`, `down`, `left` and `right` to
support different input methods correctly.
* Returns an empty table `{}` if the object is not a player. * Returns an empty table `{}` if the object is not a player.
* `get_player_control_bits()`: returns integer with bit packed player pressed * `get_player_control_bits()`: returns integer with bit packed player pressed
keys. keys.
@ -8574,23 +8669,43 @@ child will follow movement and rotation of that bone.
* values < 0 cause an effect similar to inversion, * values < 0 cause an effect similar to inversion,
but keeping original luma and being symmetrical in terms of saturation but keeping original luma and being symmetrical in terms of saturation
(eg. -1 and 1 is the same saturation and luma, but different hues) (eg. -1 and 1 is the same saturation and luma, but different hues)
* This value has no effect on clients who have shaders or post-processing disabled.
* `shadows` is a table that controls ambient shadows * `shadows` is a table that controls ambient shadows
* This has no effect on clients who have the "Dynamic Shadows" effect disabled.
* `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness) * `intensity` sets the intensity of the shadows from 0 (no shadows, default) to 1 (blackness)
* This value has no effect on clients who have the "Dynamic Shadows" shader disabled.
* `tint` tints the shadows with the provided color, with RGB values ranging from 0 to 255. * `tint` tints the shadows with the provided color, with RGB values ranging from 0 to 255.
(default `{r=0, g=0, b=0}`) (default `{r=0, g=0, b=0}`)
* This value has no effect on clients who have the "Dynamic Shadows" shader disabled.
* `exposure` is a table that controls automatic exposure. * `exposure` is a table that controls automatic exposure.
The basic exposure factor equation is `e = 2^exposure_correction / clamp(luminance, 2^luminance_min, 2^luminance_max)` The basic exposure factor equation is `e = 2^exposure_correction / clamp(luminance, 2^luminance_min, 2^luminance_max)`
* This has no effect on clients who have the "Automatic Exposure" effect disabled.
* `luminance_min` set the lower luminance boundary to use in the calculation (default: `-3.0`) * `luminance_min` set the lower luminance boundary to use in the calculation (default: `-3.0`)
* `luminance_max` set the upper luminance boundary to use in the calculation (default: `-3.0`) * `luminance_max` set the upper luminance boundary to use in the calculation (default: `-3.0`)
* `exposure_correction` correct observed exposure by the given EV value (default: `0.0`) * `exposure_correction` correct observed exposure by the given EV value (default: `0.0`)
* `speed_dark_bright` set the speed of adapting to bright light (default: `1000.0`) * `speed_dark_bright` set the speed of adapting to bright light (default: `1000.0`)
* `speed_bright_dark` set the speed of adapting to dark scene (default: `1000.0`) * `speed_bright_dark` set the speed of adapting to dark scene (default: `1000.0`)
* `center_weight_power` set the power factor for center-weighted luminance measurement (default: `1.0`) * `center_weight_power` set the power factor for center-weighted luminance measurement (default: `1.0`)
* `bloom` is a table that controls bloom.
* This has no effect on clients with protocol version < 46 or clients who
have the "Bloom" effect disabled.
* `intensity` defines much bloom is applied to the rendered image.
* Recommended range: from 0.0 to 1.0, default: 0.05
* If set to 0, bloom is disabled.
* The default value is to be changed from 0.05 to 0 in the future.
If you wish to keep the current default value, you should set it
explicitly.
* `strength_factor` defines the magnitude of bloom overexposure.
* Recommended range: from 0.1 to 10.0, default: 1.0
* `radius` is a logical value that controls how far the bloom effect
spreads from the bright objects.
* Recommended range: from 0.1 to 8.0, default: 1.0
* The behavior of values outside the recommended range is unspecified.
* `volumetric_light`: is a table that controls volumetric light (a.k.a. "godrays") * `volumetric_light`: is a table that controls volumetric light (a.k.a. "godrays")
* `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest) * This has no effect on clients who have the "Volumetric Lighting" or "Bloom" effects disabled.
* This value has no effect on clients who have the "Volumetric Lighting" or "Bloom" shaders disabled. * `strength`: sets the strength of the volumetric light effect from 0 (off, default) to 1 (strongest).
* `0.2` is a reasonable standard value.
* Currently, bloom `intensity` and `strength_factor` affect volumetric
lighting `strength` and vice versa. This behavior is to be changed
in the future, do not rely on it.
* `get_lighting()`: returns the current state of lighting for the player. * `get_lighting()`: returns the current state of lighting for the player.
* Result is a table with the same fields as `light_definition` in `set_lighting`. * Result is a table with the same fields as `light_definition` in `set_lighting`.
@ -9106,6 +9221,11 @@ Used by `minetest.register_abm`.
-- If left out or empty, any neighbor will do. -- If left out or empty, any neighbor will do.
-- `group:groupname` can also be used here. -- `group:groupname` can also be used here.
without_neighbors = {"default:lava_source", "default:lava_flowing"},
-- Only apply `action` to nodes that have no one of these neighbors.
-- If left out or empty, it has no effect.
-- `group:groupname` can also be used here.
interval = 10.0, interval = 10.0,
-- Operation interval in seconds -- Operation interval in seconds
@ -9515,12 +9635,18 @@ Used by `minetest.register_node`.
use_texture_alpha = ..., use_texture_alpha = ...,
-- Specifies how the texture's alpha channel will be used for rendering. -- Specifies how the texture's alpha channel will be used for rendering.
-- possible values: -- Possible values:
-- * "opaque": Node is rendered opaque regardless of alpha channel -- * "opaque":
-- * "clip": A given pixel is either fully see-through or opaque -- Node is rendered opaque regardless of alpha channel.
-- depending on the alpha channel being below/above 50% in value -- * "clip":
-- * "blend": The alpha channel specifies how transparent a given pixel -- A given pixel is either fully see-through or opaque
-- of the rendered node is -- depending on the alpha channel being below/above 50% in value.
-- Use this for nodes with fully transparent and fully opaque areas.
-- * "blend":
-- The alpha channel specifies how transparent a given pixel
-- of the rendered node is. This comes at a performance cost.
-- Only use this when correct rendering
-- among semitransparent nodes is necessary.
-- The default is "opaque" for drawtypes normal, liquid and flowingliquid, -- The default is "opaque" for drawtypes normal, liquid and flowingliquid,
-- mesh and nodebox or "clip" otherwise. -- mesh and nodebox or "clip" otherwise.
-- If set to a boolean value (deprecated): true either sets it to blend -- If set to a boolean value (deprecated): true either sets it to blend

View file

@ -57,7 +57,10 @@ Functions
* returns the maximum supported network protocol version * returns the maximum supported network protocol version
* `core.open_url(url)` * `core.open_url(url)`
* opens the URL in a web browser, returns false on failure. * opens the URL in a web browser, returns false on failure.
* Must begin with http:// or https:// * `url` must begin with http:// or https://
* `core.open_url_dialog(url)`
* shows a dialog to allow the user to choose whether to open a URL.
* `url` must begin with http:// or https://
* `core.open_dir(path)` * `core.open_dir(path)`
* opens the path in the system file browser/explorer, returns false on failure. * opens the path in the system file browser/explorer, returns false on failure.
* Must be an existing directory. * Must be an existing directory.
@ -65,6 +68,8 @@ Functions
* Android only. Shares file using the share popup * Android only. Shares file using the share popup
* `core.get_version()` (possible in async calls) * `core.get_version()` (possible in async calls)
* returns current core version * returns current core version
* `core.get_formspec_version()`
* returns maximum supported formspec version

View file

@ -1,4 +1,4 @@
glTF test model (and corresponding texture) licenses: The glTF test models (and corresponding textures) in this mod are all licensed freely:
* Spider (`gltf_spider.gltf`, `gltf_spider.png`): * Spider (`gltf_spider.gltf`, `gltf_spider.png`):
* By [archfan7411](https://github.com/archfan7411) * By [archfan7411](https://github.com/archfan7411)

View file

@ -18,13 +18,57 @@ do
register_entity("blender_cube", cube_textures) register_entity("blender_cube", cube_textures)
register_entity("blender_cube_scaled", cube_textures) register_entity("blender_cube_scaled", cube_textures)
register_entity("blender_cube_matrix_transform", cube_textures) register_entity("blender_cube_matrix_transform", cube_textures)
minetest.register_entity("gltf:blender_cube_glb", {
initial_properties = {
visual = "mesh",
mesh = "gltf_blender_cube.glb",
textures = cube_textures,
backface_culling = true,
},
})
end end
register_entity("snow_man", {"gltf_snow_man.png"}) register_entity("snow_man", {"gltf_snow_man.png"})
register_entity("spider", {"gltf_spider.png"}) register_entity("spider", {"gltf_spider.png"})
-- Note: Model has an animation, but we can use it as a static test nevertheless
minetest.register_entity("gltf:spider_animated", {
initial_properties = {
visual = "mesh",
mesh = "gltf_spider_animated.gltf",
textures = {"gltf_spider.png"},
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 140}, 1)
end
})
minetest.register_entity("gltf:simple_skin", {
initial_properties = {
visual = "mesh",
visual_size = vector.new(5, 5, 5),
mesh = "gltf_simple_skin.gltf",
textures = {},
backface_culling = false
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 5.5}, 1)
end
})
-- The claws rendering incorrectly from one side is expected behavior: -- The claws rendering incorrectly from one side is expected behavior:
-- They use an unsupported double-sided material. -- They use an unsupported double-sided material.
register_entity("frog", {"gltf_frog.png"}, false) minetest.register_entity("gltf:frog", {
initial_properties = {
visual = "mesh",
mesh = "gltf_frog.gltf",
textures = {"gltf_frog.png"},
backface_culling = false
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 0.75}, 1)
end
})
minetest.register_node("gltf:frog", { minetest.register_node("gltf:frog", {
description = "glTF frog, but it's a node", description = "glTF frog, but it's a node",

Binary file not shown.

View file

@ -0,0 +1 @@
{"scene":0,"scenes":[{"nodes":[0,1]}],"nodes":[{"skin":0,"mesh":0},{"children":[2]},{"translation":[0.0,1.0,0.0],"rotation":[0.0,0.0,0.0,1.0]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteLength":320,"byteStride":16},{"buffer":2,"byteLength":128},{"buffer":3,"byteLength":240}],"accessors":[{"bufferView":0,"componentType":5123,"count":24,"type":"SCALAR"},{"bufferView":1,"componentType":5126,"count":10,"type":"VEC3","max":[0.5,2.0,0.0],"min":[-0.5,0.0,0.0]},{"bufferView":2,"componentType":5123,"count":10,"type":"VEC4"},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4"},{"bufferView":3,"componentType":5126,"count":2,"type":"MAT4"},{"bufferView":4,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0.0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0.0,0.0,0.707,1.0],"min":[0.0,0.0,-0.707,0.707]}],"asset":{"version":"2.0"}}

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,21 @@ local lighting_sections = {
{n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"}, {n = "speed_bright_dark", d = "Dark scene adaptation speed", min = -10, max = 10, type="log2"},
{n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10}, {n = "center_weight_power", d = "Power factor for center-weighting", min = 0.1, max = 10},
} }
} },
{
n = "bloom", d = "Bloom",
entries = {
{n = "intensity", d = "Intensity", min = 0, max = 1},
{n = "strength_factor", d = "Strength Factor", min = 0.1, max = 10},
{n = "radius", d = "Radius", min = 0.1, max = 8},
},
},
{
n = "volumetric_light", d = "Volumetric Lighting",
entries = {
{n = "strength", d = "Strength", min = 0, max = 1},
},
},
} }
local function dump_lighting(lighting) local function dump_lighting(lighting)
@ -59,38 +73,40 @@ minetest.register_chatcommand("set_lighting", {
local lighting = player:get_lighting() local lighting = player:get_lighting()
local exposure = lighting.exposure or {} local exposure = lighting.exposure or {}
local form = { local content = {}
"formspec_version[2]",
"size[15,30]",
"position[0.99,0.15]",
"anchor[1,0]",
"padding[0.05,0.1]",
"no_prepend[]"
};
local line = 1 local line = 1
for _,section in ipairs(lighting_sections) do for _,section in ipairs(lighting_sections) do
local parameters = section.entries or {} local parameters = section.entries or {}
local state = lighting[section.n] or {} local state = lighting[section.n] or {}
table.insert(form, "label[1,"..line..";"..section.d.."]") table.insert(content, "label[1,"..line..";"..section.d.."]")
line = line + 1 line = line + 1
for _,v in ipairs(parameters) do for _,v in ipairs(parameters) do
table.insert(form, "label[2,"..line..";"..v.d.."]") table.insert(content, "label[2,"..line..";"..v.d.."]")
table.insert(form, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]") table.insert(content, "scrollbaroptions[min=0;max=1000;smallstep=10;largestep=100;thumbsize=10]")
local value = state[v.n] local value = state[v.n]
if v.type == "log2" then if v.type == "log2" then
value = math.log(value or 1) / math.log(2) value = math.log(value or 1) / math.log(2)
end end
local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min)) local sb_scale = math.floor(1000 * (math.max(v.min, value or 0) - v.min) / (v.max - v.min))
table.insert(form, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]") table.insert(content, "scrollbar[2,"..(line+0.7)..";12,1;horizontal;"..section.n.."."..v.n..";"..sb_scale.."]")
line = line + 2.7 line = line + 2.7
end end
line = line + 1 line = line + 1
end end
local form = {
"formspec_version[2]",
"size[15,", line, "]",
"position[0.99,0.15]",
"anchor[1,0]",
"padding[0.05,0.1]",
"no_prepend[]",
}
table.insert_all(form, content)
minetest.show_formspec(player_name, "lighting", table.concat(form)) minetest.show_formspec(player_name, "lighting", table.concat(form))
local debug_value = dump_lighting(lighting) local debug_value = dump_lighting(lighting)
local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF}) local debug_ui = player:hud_add({type="text", position={x=0.1, y=0.3}, scale={x=1,y=1}, alignment = {x=1, y=1}, text=debug_value, number=0xFFFFFF})

View file

@ -0,0 +1,6 @@
# Test ABMs
This mod contains a nodes and related ABM actions.
By placing these nodes, you can test basic ABM behaviours.
There are separate tests for ABM `chance`, `interval`, `min_y`, `max_y`, `neighbor` and `without_neighbor` fields.

View file

@ -0,0 +1,12 @@
local S = minetest.get_translator("testnodes")
-- After ABM node
minetest.register_node("testabms:after_abm", {
description = S("After ABM processed node."),
drawtype = "normal",
tiles = { "testabms_after_node.png" },
groups = { dig_immediate = 3 },
})

View file

@ -0,0 +1,56 @@
-- test ABMs with different chances
local S = minetest.get_translator("testnodes")
-- ABM chance 5 node
minetest.register_node("testabms:chance_5", {
description = S("Node for test ABM chance_5"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:chance_5")
end,
})
minetest.register_abm({
label = "testabms:chance_5",
nodenames = "testabms:chance_5",
interval = 10,
chance = 5,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:chance_5 changed this node.")
end
})
-- ABM chance 20 node
minetest.register_node("testabms:chance_20", {
description = S("Node for test ABM chance_20"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:chance_20")
end,
})
minetest.register_abm({
label = "testabms:chance_20",
nodenames = "testabms:chance_20",
interval = 10,
chance = 20,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:chance_20 changed this node.")
end
})

View file

@ -0,0 +1,7 @@
local path = minetest.get_modpath(minetest.get_current_modname())
dofile(path.."/after_node.lua")
dofile(path.."/chances.lua")
dofile(path.."/intervals.lua")
dofile(path.."/min_max.lua")
dofile(path.."/neighbors.lua")

View file

@ -0,0 +1,56 @@
-- test ABMs with different interval
local S = minetest.get_translator("testnodes")
-- ABM inteval 1 node
minetest.register_node("testabms:interval_1", {
description = S("Node for test ABM interval_1"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:interval_1")
end,
})
minetest.register_abm({
label = "testabms:interval_1",
nodenames = "testabms:interval_1",
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:interval_1 changed this node.")
end
})
-- ABM interval 60 node
minetest.register_node("testabms:interval_60", {
description = S("Node for test ABM interval_60"),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:interval_60")
end,
})
minetest.register_abm({
label = "testabms:interval_60",
nodenames = "testabms:interval_60",
interval = 60,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:interval_60 changed this node.")
end
})

View file

@ -0,0 +1,58 @@
-- test ABMs with min_y and max_y
local S = minetest.get_translator("testnodes")
-- ABM min_y node
minetest.register_node("testabms:min_y", {
description = S("Node for test ABM min_y."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:min_y at y "..pos.y.." with min_y = 0")
end,
})
minetest.register_abm({
label = "testabms:min_y",
nodenames = "testabms:min_y",
interval = 10,
chance = 1,
min_y = 0,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:min_y changed this node.")
end
})
-- ABM max_y node
minetest.register_node("testabms:max_y", {
description = S("Node for test ABM max_y."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Waiting for ABM testabms:max_y at y "..pos.y.." with max_y = 0")
end,
})
minetest.register_abm({
label = "testabms:max_y",
nodenames = "testabms:max_y",
interval = 10,
chance = 1,
max_y = 0,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ABM testabsm:max_y changed this node.")
end
})

View file

@ -0,0 +1,2 @@
name = testabms
description = Contains some nodes for test ABMs.

View file

@ -0,0 +1,99 @@
-- test ABMs with neighbor and without_neighbor
local S = minetest.get_translator("testnodes")
-- ABM required neighbor
minetest.register_node("testabms:required_neighbor", {
description = S("Node for test ABM required_neighbor.") .. "\n"
.. S("Sensitive neighbor node is testnodes:normal."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:required_neighbor "
.. "(normal drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:required_neighbor",
nodenames = "testabms:required_neighbor",
neighbors = {"testnodes:normal"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:required_neighbor changed this node.")
end
})
-- ABM missing neighbor node
minetest.register_node("testabms:missing_neighbor", {
description = S("Node for test ABM missing_neighbor.") .. "\n"
.. S("Sensitive neighbor node is testnodes:normal."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:missing_neighbor"
.. " (normal drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:missing_neighbor",
nodenames = "testabms:missing_neighbor",
without_neighbors = {"testnodes:normal"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:missing_neighbor changed this node.")
end
})
-- ABM required and missing neighbor node
minetest.register_node("testabms:required_missing_neighbor", {
description = S("Node for test ABM required_missing_neighbor.") .. "\n"
.. S("Sensitive neighbor nodes are testnodes:normal and testnodes:glasslike."),
drawtype = "normal",
tiles = { "testabms_wait_node.png" },
groups = { dig_immediate = 3 },
on_construct = function (pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"Waiting for ABM testabms:required_missing_neighbor"
.. " (wint normal drawtype testnode and no glasslike"
.. " drawtype testnode sensitive)")
end,
})
minetest.register_abm({
label = "testabms:required_missing_neighbor",
nodenames = "testabms:required_missing_neighbor",
neighbors = {"testnodes:normal"},
without_neighbors = {"testnodes:glasslike"},
interval = 1,
chance = 1,
action = function (pos)
minetest.swap_node(pos, {name="testabms:after_abm"})
local meta = minetest.get_meta(pos)
meta:set_string("infotext",
"ABM testabsm:required_missing_neighbor changed this node.")
end
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

View file

@ -299,7 +299,18 @@ local scroll_fs =
"scrollbaroptions[max=170]".. -- lowest seen pos is: 0.1*170+6=23 (factor*max+height) "scrollbaroptions[max=170]".. -- lowest seen pos is: 0.1*170+6=23 (factor*max+height)
"scrollbar[7.5,0;0.3,4;vertical;scrbar;0]".. "scrollbar[7.5,0;0.3,4;vertical;scrbar;0]"..
"scrollbar[8,0;0.3,4;vertical;scrbarhmmm;0]".. "scrollbar[8,0;0.3,4;vertical;scrbarhmmm;0]"..
"dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]" "dropdown[0,6;2;hmdrpdwnnn;Outside,of,container;1]"..
"scroll_container[0,8;10,4;scrbar420;vertical;0.1;2]"..
"button[0.5,0.5;10,1;;Container with padding=2]"..
"list[current_player;main;0,5;8,4;]"..
"scroll_container_end[]"..
"scrollbar[10.1,8;0.5,4;vertical;scrbar420;0]"..
-- Buttons for scale comparison
"button[11,8;1,1;;0]"..
"button[11,9;1,1;;1]"..
"button[11,10;1,1;;2]"..
"button[11,11;1,1;;3]"..
"button[11,12;1,1;;4]"
--style_type[label;textcolor=green] --style_type[label;textcolor=green]
--label[0,0;Green] --label[0,0;Green]
@ -462,7 +473,7 @@ mouse control = true]
]], ]],
-- Scroll containers -- Scroll containers
"formspec_version[3]size[12,13]" .. "formspec_version[7]size[12,13]" ..
scroll_fs, scroll_fs,
-- Sound -- Sound

View file

@ -98,6 +98,23 @@ minetest.register_node("testnodes:allfaces", {
groups = { dig_immediate = 3 }, groups = { dig_immediate = 3 },
}) })
minetest.register_node("testnodes:allfaces_6", {
description = S("\"allfaces 6 Textures\" Drawtype Test Node").."\n"..
S("Transparent node with visible internal backfaces"),
drawtype = "allfaces",
paramtype = "light",
tiles = {
"testnodes_allfaces.png^[colorize:red",
"testnodes_allfaces.png^[colorize:orange",
"testnodes_allfaces.png^[colorize:yellow",
"testnodes_allfaces.png^[colorize:green",
"testnodes_allfaces.png^[colorize:blue",
"testnodes_allfaces.png^[colorize:purple"
},
groups = { dig_immediate = 3 },
})
local allfaces_optional_tooltip = "".. local allfaces_optional_tooltip = ""..
S("Rendering depends on 'leaves_style' setting:").."\n".. S("Rendering depends on 'leaves_style' setting:").."\n"..
S("* 'fancy': transparent with visible internal backfaces").."\n".. S("* 'fancy': transparent with visible internal backfaces").."\n"..

View file

@ -52,6 +52,12 @@ minetest.register_node("testnodes:fill_positioning_reference", {
groups = {dig_immediate = 3}, groups = {dig_immediate = 3},
}) })
minetest.register_node("testnodes:modifier_mask", {
description = S("[mask Modifier Test Node"),
tiles = {"testnodes_128x128_rgb.png^[mask:testnodes_mask_WRGBKW.png"},
groups = {dig_immediate = 3},
})
-- Node texture transparency test -- Node texture transparency test
local alphas = { 64, 128, 191 } local alphas = { 64, 128, 191 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

View file

@ -22,9 +22,11 @@ local function do_tests()
assert(core.registered_items["unittests:description_test"].on_place == true) assert(core.registered_items["unittests:description_test"].on_place == true)
end end
-- there's no (usable) communcation path between mapgen and the regular env -- first thread to get here runs the tests
-- so we just run the test unconditionally if core.ipc_cas("unittests:mg_once", nil, true) then
do_tests() -- this is checked from the main env
core.ipc_set("unittests:mg", { pcall(do_tests) })
end
core.register_on_generated(function(vm, pos1, pos2, blockseed) core.register_on_generated(function(vm, pos1, pos2, blockseed)
local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1 local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1

View file

@ -254,3 +254,43 @@ local function test_gennotify_api()
assert(#custom == 0, "custom ids not empty") assert(#custom == 0, "custom ids not empty")
end end
unittests.register("test_gennotify_api", test_gennotify_api) unittests.register("test_gennotify_api", test_gennotify_api)
-- <=> inside_mapgen_env.lua
local function test_mapgen_env(cb)
-- emerge threads start delayed so this can take a second
local res = core.ipc_get("unittests:mg")
if res == nil then
return core.after(0, test_mapgen_env, cb)
end
-- handle error status
if res[1] then
cb()
else
cb(res[2])
end
end
unittests.register("test_mapgen_env", test_mapgen_env, {async=true})
local function test_ipc_vector_preserve(cb)
-- the IPC also uses register_portable_metatable
core.ipc_set("unittests:v", vector.new(4, 0, 4))
local v = core.ipc_get("unittests:v")
assert(type(v) == "table")
assert(vector.check(v))
end
unittests.register("test_ipc_vector_preserve", test_ipc_vector_preserve)
local function test_ipc_poll(cb)
core.ipc_set("unittests:flag", nil)
assert(core.ipc_poll("unittests:flag", 1) == false)
-- Note that unlike the async result callback - which has to wait for the
-- next server step - the IPC is instant
local t0 = core.get_us_time()
core.handle_async(function()
core.ipc_set("unittests:flag", true)
end, function() end)
assert(core.ipc_poll("unittests:flag", 1000) == true, "Wait failed (or slow machine?)")
print("delta: " .. (core.get_us_time() - t0) .. "us")
end
unittests.register("test_ipc_poll", test_ipc_poll)

View file

@ -42,41 +42,97 @@ unittests.register("test_hpchangereason", run_hpchangereason_tests, {player=true
-- --
local expected_diff = nil local expected_diff = nil
local hpchange_counter = 0
local die_counter = 0
core.register_on_player_hpchange(function(player, hp_change, reason) core.register_on_player_hpchange(function(player, hp_change, reason)
if expected_diff then if expected_diff then
assert(hp_change == expected_diff) assert(hp_change == expected_diff)
hpchange_counter = hpchange_counter + 1
end end
end) end)
core.register_on_dieplayer(function()
die_counter = die_counter + 1
end)
local function hp_diference_test(player, hp_max)
assert(hp_max >= 22)
local function run_hp_difference_tests(player)
local old_hp = player:get_hp() local old_hp = player:get_hp()
local old_hp_max = player:get_properties().hp_max local old_hp_max = player:get_properties().hp_max
expected_diff = nil hpchange_counter = 0
player:set_properties({hp_max = 30}) die_counter = 0
player:set_hp(22)
-- final HP value is clamped to >= 0 before difference calculation expected_diff = nil
expected_diff = -22 player:set_properties({hp_max = hp_max})
player:set_hp(22)
assert(player:get_hp() == 22)
assert(hpchange_counter == 0)
assert(die_counter == 0)
-- HP difference is not clamped
expected_diff = -25
player:set_hp(-3) player:set_hp(-3)
-- and actual final HP value is clamped to >= 0 too -- actual final HP value is clamped to >= 0
assert(player:get_hp() == 0) assert(player:get_hp() == 0)
assert(hpchange_counter == 1)
assert(die_counter == 1)
expected_diff = 22 expected_diff = 22
player:set_hp(22) player:set_hp(22)
assert(player:get_hp() == 22) assert(player:get_hp() == 22)
assert(hpchange_counter == 2)
assert(die_counter == 1)
-- final HP value is clamped to <= U16_MAX before difference calculation -- Integer overflow is prevented
expected_diff = 65535 - 22 -- so result is S32_MIN, not S32_MIN - 22
expected_diff = -2147483648
player:set_hp(-2147483648)
-- actual final HP value is clamped to >= 0
assert(player:get_hp() == 0)
assert(hpchange_counter == 3)
assert(die_counter == 2)
-- Damage is ignored if player is already dead (hp == 0)
expected_diff = "never equal"
player:set_hp(-11)
assert(player:get_hp() == 0)
-- no on_player_hpchange or on_dieplayer call expected
assert(hpchange_counter == 3)
assert(die_counter == 2)
expected_diff = 11
player:set_hp(11)
assert(player:get_hp() == 11)
assert(hpchange_counter == 4)
assert(die_counter == 2)
-- HP difference is not clamped
expected_diff = 1000000 - 11
player:set_hp(1000000) player:set_hp(1000000)
-- and actual final HP value is clamped to <= hp_max -- actual final HP value is clamped to <= hp_max
assert(player:get_hp() == 30) assert(player:get_hp() == hp_max)
assert(hpchange_counter == 5)
assert(die_counter == 2)
-- "Healing" is not ignored when hp == hp_max
expected_diff = 80000 - hp_max
player:set_hp(80000)
assert(player:get_hp() == hp_max)
-- on_player_hpchange_call expected
assert(hpchange_counter == 6)
assert(die_counter == 2)
expected_diff = nil expected_diff = nil
player:set_properties({hp_max = old_hp_max}) player:set_properties({hp_max = old_hp_max})
player:set_hp(old_hp) player:set_hp(old_hp)
core.close_formspec(player:get_player_name(), "") -- hide death screen core.close_formspec(player:get_player_name(), "") -- hide death screen
end end
local function run_hp_difference_tests(player)
hp_diference_test(player, 22)
hp_diference_test(player, 30)
hp_diference_test(player, 65535) -- U16_MAX
end
unittests.register("test_hp_difference", run_hp_difference_tests, {player=true}) unittests.register("test_hp_difference", run_hp_difference_tests, {player=true})
-- --

View file

@ -19,11 +19,8 @@ irr::scene::SMeshBuffer etc. */
class IAnimatedMesh : public IMesh class IAnimatedMesh : public IMesh
{ {
public: public:
//! Gets the frame count of the animated mesh. //! Gets the maximum frame number, 0 if the mesh is static.
/** Note that the play-time is usually getFrameCount()-1 as it stops as soon as the last frame-key is reached. virtual f32 getMaxFrameNumber() const = 0;
\return The amount of frames. If the amount is 1,
it is a static, non animated mesh. */
virtual u32 getFrameCount() const = 0;
//! Gets the animation speed of the animated mesh. //! Gets the animation speed of the animated mesh.
/** \return The number of frames per second to play the /** \return The number of frames per second to play the
@ -39,19 +36,10 @@ public:
virtual void setAnimationSpeed(f32 fps) = 0; virtual void setAnimationSpeed(f32 fps) = 0;
//! Returns the IMesh interface for a frame. //! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index. The maximum /** \param frame: Frame number, >= 0, <= getMaxFrameNumber()
frame number is getFrameCount() - 1; Linear interpolation is used if this is between two frames.
\param detailLevel: Level of detail. 0 is the lowest, 255 the \return Returns the animated mesh for the given frame */
highest level of detail. Most meshes will ignore the detail level. virtual IMesh *getMesh(f32 frame) = 0;
\param startFrameLoop: Because some animated meshes (.MD2) are
blended between 2 static frames, and maybe animated in a loop,
the startFrameLoop and the endFrameLoop have to be defined, to
prevent the animation to be blended between frames which are
outside of this loop.
If startFrameLoop and endFrameLoop are both -1, they are ignored.
\param endFrameLoop: see startFrameLoop.
\return Returns the animated mesh based on a detail level. */
virtual IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) = 0;
//! Returns the type of the animated mesh. //! Returns the type of the animated mesh.
/** In most cases it is not necessary to use this method. /** In most cases it is not necessary to use this method.

View file

@ -63,7 +63,7 @@ public:
virtual void setCurrentFrame(f32 frame) = 0; virtual void setCurrentFrame(f32 frame) = 0;
//! Sets the frame numbers between the animation is looped. //! Sets the frame numbers between the animation is looped.
/** The default is 0 to getFrameCount()-1 of the mesh. /** The default is 0 to getMaxFrameNumber() of the mesh.
Number of played frames is end-start. Number of played frames is end-start.
It interpolates toward the last frame but stops when it is reached. It interpolates toward the last frame but stops when it is reached.
It does not interpolate back to start even when looping. It does not interpolate back to start even when looping.
@ -71,7 +71,7 @@ public:
\param begin: Start frame number of the loop. \param begin: Start frame number of the loop.
\param end: End frame number of the loop. \param end: End frame number of the loop.
\return True if successful, false if not. */ \return True if successful, false if not. */
virtual bool setFrameLoop(s32 begin, s32 end) = 0; virtual bool setFrameLoop(f32 begin, f32 end) = 0;
//! Sets the speed with which the animation is played. //! Sets the speed with which the animation is played.
/** \param framesPerSecond: Frames per second played. */ /** \param framesPerSecond: Frames per second played. */
@ -108,9 +108,9 @@ public:
//! Returns the currently displayed frame number. //! Returns the currently displayed frame number.
virtual f32 getFrameNr() const = 0; virtual f32 getFrameNr() const = 0;
//! Returns the current start frame number. //! Returns the current start frame number.
virtual s32 getStartFrame() const = 0; virtual f32 getStartFrame() const = 0;
//! Returns the current end frame number. //! Returns the current end frame number.
virtual s32 getEndFrame() const = 0; virtual f32 getEndFrame() const = 0;
//! Sets looping mode which is on by default. //! Sets looping mode which is on by default.
/** If set to false, animations will not be played looped. */ /** If set to false, animations will not be played looped. */

View file

@ -347,6 +347,9 @@ struct SEvent
//! Type of mouse event //! Type of mouse event
EMOUSE_INPUT_EVENT Event; EMOUSE_INPUT_EVENT Event;
//! Is this a simulated mouse event generated by Minetest itself?
bool Simulated;
}; };
//! Any kind of keyboard event. //! Any kind of keyboard event.
@ -538,6 +541,11 @@ struct SEvent
struct SUserEvent UserEvent; struct SUserEvent UserEvent;
struct SApplicationEvent ApplicationEvent; struct SApplicationEvent ApplicationEvent;
}; };
SEvent() {
// would be left uninitialized in many places otherwise
MouseInput.Simulated = false;
}
}; };
//! Interface of an object which can receive events. //! Interface of an object which can receive events.

View file

@ -159,15 +159,17 @@ public:
core::array<SWeight> Weights; core::array<SWeight> Weights;
//! Unnecessary for loaders, will be overwritten on finalize //! Unnecessary for loaders, will be overwritten on finalize
core::matrix4 GlobalMatrix; core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data.
core::matrix4 GlobalAnimatedMatrix; core::matrix4 GlobalAnimatedMatrix;
core::matrix4 LocalAnimatedMatrix; core::matrix4 LocalAnimatedMatrix;
//! These should be set by loaders.
core::vector3df Animatedposition; core::vector3df Animatedposition;
core::vector3df Animatedscale; core::vector3df Animatedscale;
core::quaternion Animatedrotation; core::quaternion Animatedrotation;
core::matrix4 GlobalInversedMatrix; // the x format pre-calculates this // The .x and .gltf formats pre-calculate this
std::optional<core::matrix4> GlobalInversedMatrix;
private: private:
//! Internal members used by CSkinnedMesh //! Internal members used by CSkinnedMesh
friend class CSkinnedMesh; friend class CSkinnedMesh;

View file

@ -36,11 +36,9 @@ struct SAnimatedMesh final : public IAnimatedMesh
mesh->drop(); mesh->drop();
} }
//! Gets the frame count of the animated mesh. f32 getMaxFrameNumber() const override
/** \return Amount of frames. If the amount is 1, it is a static, non animated mesh. */
u32 getFrameCount() const override
{ {
return static_cast<u32>(Meshes.size()); return static_cast<f32>(Meshes.size() - 1);
} }
//! Gets the default animation speed of the animated mesh. //! Gets the default animation speed of the animated mesh.
@ -59,19 +57,14 @@ struct SAnimatedMesh final : public IAnimatedMesh
} }
//! Returns the IMesh interface for a frame. //! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index. The maximum frame number is /** \param frame: Frame number as zero based index.
getFrameCount() - 1; \return The animated mesh based for the given frame */
\param detailLevel: Level of detail. 0 is the lowest, IMesh *getMesh(f32 frame) override
255 the highest level of detail. Most meshes will ignore the detail level.
\param startFrameLoop: start frame
\param endFrameLoop: end frame
\return The animated mesh based on a detail level. */
IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override
{ {
if (Meshes.empty()) if (Meshes.empty())
return 0; return nullptr;
return Meshes[frame]; return Meshes[static_cast<s32>(frame)];
} }
//! adds a Mesh //! adds a Mesh

View file

@ -410,6 +410,7 @@ public:
{ {
bool different = bool different =
MaterialType != b.MaterialType || MaterialType != b.MaterialType ||
ColorParam != b.ColorParam ||
MaterialTypeParam != b.MaterialTypeParam || MaterialTypeParam != b.MaterialTypeParam ||
Thickness != b.Thickness || Thickness != b.Thickness ||
Wireframe != b.Wireframe || Wireframe != b.Wireframe ||

View file

@ -63,7 +63,7 @@ inline io::path &getFileNameExtension(io::path &dest, const io::path &source)
} }
//! delete path from filename //! delete path from filename
inline io::path &deletePathFromFilename(io::path &filename) inline io::path deletePathFromFilename(const io::path &filename)
{ {
// delete path from filename // delete path from filename
const fschar_t *s = filename.c_str(); const fschar_t *s = filename.c_str();
@ -73,11 +73,10 @@ inline io::path &deletePathFromFilename(io::path &filename)
while (*p != '/' && *p != '\\' && p != s) while (*p != '/' && *p != '\\' && p != s)
p--; p--;
if (p != s) { if (p != s)
++p; ++p;
filename = p;
} return p;
return filename;
} }
//! trim paths //! trim paths

View file

@ -173,13 +173,24 @@ public:
return *this; return *this;
} }
// no longer allowed! if constexpr (sizeof(T) != sizeof(B)) {
_IRR_DEBUG_BREAK_IF((void *)c == (void *)c_str()); _IRR_DEBUG_BREAK_IF(
(uintptr_t)c >= (uintptr_t)(str.data()) &&
(uintptr_t)c < (uintptr_t)(str.data() + str.size()));
}
if ((void *)c == (void *)c_str())
return *this;
u32 len = calclen(c); u32 len = calclen(c);
// In case `c` is a pointer to our own buffer, we may not resize first
// or it can become invalid.
if (len > str.size())
str.resize(len); str.resize(len);
for (u32 l = 0; l < len; ++l) for (u32 l = 0; l < len; ++l)
str[l] = (T)c[l]; str[l] = static_cast<T>(c[l]);
if (len < str.size())
str.resize(len);
return *this; return *this;
} }

View file

@ -24,7 +24,12 @@ namespace core
{ {
//! 4x4 matrix. Mostly used as transformation matrix for 3d calculations. //! 4x4 matrix. Mostly used as transformation matrix for 3d calculations.
/** The matrix is a D3D style matrix, row major with translations in the 4th row. */ /** Conventions: Matrices are considered to be in row-major order.
* Multiplication of a matrix A with a row vector v is the premultiplication vA.
* Translations are thus in the 4th row.
* The matrix product AB yields a matrix C such that vC = (vB)A:
* B is applied first, then A.
*/
template <class T> template <class T>
class CMatrix4 class CMatrix4
{ {
@ -242,17 +247,11 @@ public:
//! Translate a vector by the inverse of the translation part of this matrix. //! Translate a vector by the inverse of the translation part of this matrix.
void inverseTranslateVect(vector3df &vect) const; void inverseTranslateVect(vector3df &vect) const;
//! Rotate a vector by the inverse of the rotation part of this matrix. //! Scale a vector, then rotate by the inverse of the rotation part of this matrix.
void inverseRotateVect(vector3df &vect) const; [[nodiscard]] vector3d<T> scaleThenInvRotVect(const vector3d<T> &vect) const;
//! Rotate a vector by the rotation part of this matrix. //! Rotate and scale a vector. Applies both rotation & scale part of the matrix.
void rotateVect(vector3df &vect) const; [[nodiscard]] vector3d<T> rotateAndScaleVect(const vector3d<T> &vect) const;
//! An alternate transform vector method, writing into a second vector
void rotateVect(core::vector3df &out, const core::vector3df &in) const;
//! An alternate transform vector method, writing into an array of 3 floats
void rotateVect(T *out, const core::vector3df &in) const;
//! Transforms the vector by this matrix //! Transforms the vector by this matrix
/** This operation is performed as if the vector was 4d with the 4th component =1 */ /** This operation is performed as if the vector was 4d with the 4th component =1 */
@ -1154,39 +1153,23 @@ inline bool CMatrix4<T>::isIdentity_integer_base() const
} }
template <class T> template <class T>
inline void CMatrix4<T>::rotateVect(vector3df &vect) const inline vector3d<T> CMatrix4<T>::rotateAndScaleVect(const vector3d<T> &v) const
{ {
vector3d<T> tmp(static_cast<T>(vect.X), static_cast<T>(vect.Y), static_cast<T>(vect.Z)); return {
vect.X = static_cast<f32>(tmp.X * M[0] + tmp.Y * M[4] + tmp.Z * M[8]); v.X * M[0] + v.Y * M[4] + v.Z * M[8],
vect.Y = static_cast<f32>(tmp.X * M[1] + tmp.Y * M[5] + tmp.Z * M[9]); v.X * M[1] + v.Y * M[5] + v.Z * M[9],
vect.Z = static_cast<f32>(tmp.X * M[2] + tmp.Y * M[6] + tmp.Z * M[10]); v.X * M[2] + v.Y * M[6] + v.Z * M[10]
} };
//! An alternate transform vector method, writing into a second vector
template <class T>
inline void CMatrix4<T>::rotateVect(core::vector3df &out, const core::vector3df &in) const
{
out.X = in.X * M[0] + in.Y * M[4] + in.Z * M[8];
out.Y = in.X * M[1] + in.Y * M[5] + in.Z * M[9];
out.Z = in.X * M[2] + in.Y * M[6] + in.Z * M[10];
}
//! An alternate transform vector method, writing into an array of 3 floats
template <class T>
inline void CMatrix4<T>::rotateVect(T *out, const core::vector3df &in) const
{
out[0] = in.X * M[0] + in.Y * M[4] + in.Z * M[8];
out[1] = in.X * M[1] + in.Y * M[5] + in.Z * M[9];
out[2] = in.X * M[2] + in.Y * M[6] + in.Z * M[10];
} }
template <class T> template <class T>
inline void CMatrix4<T>::inverseRotateVect(vector3df &vect) const inline vector3d<T> CMatrix4<T>::scaleThenInvRotVect(const vector3d<T> &v) const
{ {
vector3d<T> tmp(static_cast<T>(vect.X), static_cast<T>(vect.Y), static_cast<T>(vect.Z)); return {
vect.X = static_cast<f32>(tmp.X * M[0] + tmp.Y * M[1] + tmp.Z * M[2]); v.X * M[0] + v.Y * M[1] + v.Z * M[2],
vect.Y = static_cast<f32>(tmp.X * M[4] + tmp.Y * M[5] + tmp.Z * M[6]); v.X * M[4] + v.Y * M[5] + v.Z * M[6],
vect.Z = static_cast<f32>(tmp.X * M[8] + tmp.Y * M[9] + tmp.Z * M[10]); v.X * M[8] + v.Y * M[9] + v.Z * M[10]
};
} }
template <class T> template <class T>
@ -1247,8 +1230,7 @@ inline void CMatrix4<T>::transformPlane(core::plane3d<f32> &plane) const
// Transform the normal by the transposed inverse of the matrix // Transform the normal by the transposed inverse of the matrix
CMatrix4<T> transposedInverse(*this, EM4CONST_INVERSE_TRANSPOSED); CMatrix4<T> transposedInverse(*this, EM4CONST_INVERSE_TRANSPOSED);
vector3df normal = plane.Normal; vector3df normal = transposedInverse.rotateAndScaleVect(plane.Normal);
transposedInverse.rotateVect(normal);
plane.setPlane(member, normal.normalize()); plane.setPlane(member, normal.normalize());
} }

View file

@ -8,6 +8,7 @@
#include "dimension2d.h" #include "dimension2d.h"
#include <functional> #include <functional>
#include <array>
namespace irr namespace irr
{ {
@ -34,6 +35,15 @@ public:
constexpr vector2d(const dimension2d<T> &other) : constexpr vector2d(const dimension2d<T> &other) :
X(other.Width), Y(other.Height) {} X(other.Width), Y(other.Height) {}
explicit constexpr vector2d(const std::array<T, 2> &arr) :
X(arr[0]), Y(arr[1]) {}
template <class U>
constexpr static vector2d<T> from(const vector2d<U> &other)
{
return {static_cast<T>(other.X), static_cast<T>(other.Y)};
}
// operators // operators
vector2d<T> operator-() const { return vector2d<T>(-X, -Y); } vector2d<T> operator-() const { return vector2d<T>(-X, -Y); }

View file

@ -16,6 +16,7 @@
#include "IAnimatedMesh.h" #include "IAnimatedMesh.h"
#include "IFileSystem.h" #include "IFileSystem.h"
#include "quaternion.h" #include "quaternion.h"
#include <algorithm>
namespace irr namespace irr
{ {
@ -80,7 +81,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
} }
if (StartFrame == EndFrame) { if (StartFrame == EndFrame) {
CurrentFrameNr = (f32)StartFrame; // Support for non animated meshes CurrentFrameNr = StartFrame; // Support for non animated meshes
} else if (Looping) { } else if (Looping) {
// play animation looped // play animation looped
CurrentFrameNr += timeMs * FramesPerSecond; CurrentFrameNr += timeMs * FramesPerSecond;
@ -89,26 +90,26 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
// the last frame must be identical to first one with our current solution. // the last frame must be identical to first one with our current solution.
if (FramesPerSecond > 0.f) { // forwards... if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame) if (CurrentFrameNr > EndFrame)
CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, (f32)(EndFrame - StartFrame)); CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame);
} else // backwards... } else // backwards...
{ {
if (CurrentFrameNr < StartFrame) if (CurrentFrameNr < StartFrame)
CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, (f32)(EndFrame - StartFrame)); CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame);
} }
} else { } else {
// play animation non looped // play animation non looped
CurrentFrameNr += timeMs * FramesPerSecond; CurrentFrameNr += timeMs * FramesPerSecond;
if (FramesPerSecond > 0.f) { // forwards... if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > (f32)EndFrame) { if (CurrentFrameNr > EndFrame) {
CurrentFrameNr = (f32)EndFrame; CurrentFrameNr = EndFrame;
if (LoopCallBack) if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this); LoopCallBack->OnAnimationEnd(this);
} }
} else // backwards... } else // backwards...
{ {
if (CurrentFrameNr < (f32)StartFrame) { if (CurrentFrameNr < StartFrame) {
CurrentFrameNr = (f32)StartFrame; CurrentFrameNr = StartFrame;
if (LoopCallBack) if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this); LoopCallBack->OnAnimationEnd(this);
} }
@ -159,9 +160,7 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode()
IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame() IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame()
{ {
if (Mesh->getMeshType() != EAMT_SKINNED) { if (Mesh->getMeshType() != EAMT_SKINNED) {
s32 frameNr = (s32)getFrameNr(); return Mesh->getMesh(getFrameNr());
s32 frameBlend = (s32)(core::fract(getFrameNr()) * 1000.f);
return Mesh->getMesh(frameNr, frameBlend, StartFrame, EndFrame);
} else { } else {
// As multiple scene nodes may be sharing the same skinned mesh, we have to // 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. // re-animate it every frame to ensure that this node gets the mesh that it needs.
@ -331,33 +330,33 @@ void CAnimatedMeshSceneNode::render()
} }
//! Returns the current start frame number. //! Returns the current start frame number.
s32 CAnimatedMeshSceneNode::getStartFrame() const f32 CAnimatedMeshSceneNode::getStartFrame() const
{ {
return StartFrame; return StartFrame;
} }
//! Returns the current start frame number. //! Returns the current start frame number.
s32 CAnimatedMeshSceneNode::getEndFrame() const f32 CAnimatedMeshSceneNode::getEndFrame() const
{ {
return EndFrame; return EndFrame;
} }
//! sets the frames between the animation is looped. //! sets the frames between the animation is looped.
//! the default is 0 - MaximalFrameCount of the mesh. //! the default is 0 - MaximalFrameCount of the mesh.
bool CAnimatedMeshSceneNode::setFrameLoop(s32 begin, s32 end) bool CAnimatedMeshSceneNode::setFrameLoop(f32 begin, f32 end)
{ {
const s32 maxFrameCount = Mesh->getFrameCount() - 1; const f32 maxFrame = Mesh->getMaxFrameNumber();
if (end < begin) { if (end < begin) {
StartFrame = core::s32_clamp(end, 0, maxFrameCount); StartFrame = std::clamp<f32>(end, 0, maxFrame);
EndFrame = core::s32_clamp(begin, StartFrame, maxFrameCount); EndFrame = std::clamp<f32>(begin, StartFrame, maxFrame);
} else { } else {
StartFrame = core::s32_clamp(begin, 0, maxFrameCount); StartFrame = std::clamp<f32>(begin, 0, maxFrame);
EndFrame = core::s32_clamp(end, StartFrame, maxFrameCount); EndFrame = std::clamp<f32>(end, StartFrame, maxFrame);
} }
if (FramesPerSecond < 0) if (FramesPerSecond < 0)
setCurrentFrame((f32)EndFrame); setCurrentFrame(EndFrame);
else else
setCurrentFrame((f32)StartFrame); setCurrentFrame(StartFrame);
return true; return true;
} }
@ -532,7 +531,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get materials and bounding box // get materials and bounding box
Box = Mesh->getBoundingBox(); Box = Mesh->getBoundingBox();
IMesh *m = Mesh->getMesh(0, 0); IMesh *m = Mesh->getMesh(0);
if (m) { if (m) {
Materials.clear(); Materials.clear();
Materials.reallocate(m->getMeshBufferCount()); Materials.reallocate(m->getMeshBufferCount());
@ -554,7 +553,7 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get start and begin time // get start and begin time
setAnimationSpeed(Mesh->getAnimationSpeed()); // NOTE: This had been commented out (but not removed!) in r3526. Which caused meshloader-values for speed to be ignored unless users specified explicitly. Missing a test-case where this could go wrong so I put the code back in. setAnimationSpeed(Mesh->getAnimationSpeed()); // NOTE: This had been commented out (but not removed!) in r3526. Which caused meshloader-values for speed to be ignored unless users specified explicitly. Missing a test-case where this could go wrong so I put the code back in.
setFrameLoop(0, Mesh->getFrameCount() - 1); setFrameLoop(0, Mesh->getMaxFrameNumber());
} }
//! updates the absolute position based on the relative and the parents position //! updates the absolute position based on the relative and the parents position

View file

@ -45,7 +45,7 @@ public:
//! sets the frames between the animation is looped. //! sets the frames between the animation is looped.
//! the default is 0 - MaximalFrameCount of the mesh. //! the default is 0 - MaximalFrameCount of the mesh.
//! NOTE: setMesh will also change this value and set it to the full range of animations of the mesh //! NOTE: setMesh will also change this value and set it to the full range of animations of the mesh
bool setFrameLoop(s32 begin, s32 end) override; bool setFrameLoop(f32 begin, f32 end) override;
//! Sets looping mode which is on by default. If set to false, //! Sets looping mode which is on by default. If set to false,
//! animations will not be looped. //! animations will not be looped.
@ -93,9 +93,9 @@ public:
//! Returns the current displayed frame number. //! Returns the current displayed frame number.
f32 getFrameNr() const override; f32 getFrameNr() const override;
//! Returns the current start frame number. //! Returns the current start frame number.
s32 getStartFrame() const override; f32 getStartFrame() const override;
//! Returns the current end frame number. //! Returns the current end frame number.
s32 getEndFrame() const override; f32 getEndFrame() const override;
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style. //! 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 causing all mesh scene nodes /* In this way it is possible to change the materials a mesh causing all mesh scene nodes
@ -148,8 +148,8 @@ private:
core::aabbox3d<f32> Box; core::aabbox3d<f32> Box;
IAnimatedMesh *Mesh; IAnimatedMesh *Mesh;
s32 StartFrame; f32 StartFrame;
s32 EndFrame; f32 EndFrame;
f32 FramesPerSecond; f32 FramesPerSecond;
f32 CurrentFrameNr; f32 CurrentFrameNr;

View file

@ -389,7 +389,8 @@ bool CB3DMeshFileLoader::readChunkVRTS(CSkinnedMesh::SJoint *inJoint)
// Transform the Vertex position by nested node... // Transform the Vertex position by nested node...
inJoint->GlobalMatrix.transformVect(Vertex.Pos); inJoint->GlobalMatrix.transformVect(Vertex.Pos);
inJoint->GlobalMatrix.rotateVect(Vertex.Normal); Vertex.Normal = inJoint->GlobalMatrix.rotateAndScaleVect(Vertex.Normal);
Vertex.Normal.normalize(); // renormalize: normal might have been skewed by scaling
// Add it... // Add it...
BaseVertices.push_back(Vertex); BaseVertices.push_back(Vertex);

View file

@ -80,7 +80,7 @@ u32 CFileList::addItem(const io::path &fullPath, u32 offset, u32 size, bool isDi
entry.FullName = entry.Name; entry.FullName = entry.Name;
core::deletePathFromFilename(entry.Name); entry.Name = core::deletePathFromFilename(entry.Name);
if (IgnorePaths) if (IgnorePaths)
entry.FullName = entry.Name; entry.FullName = entry.Name;
@ -140,7 +140,7 @@ s32 CFileList::findFile(const io::path &filename, bool isDirectory = false) cons
entry.FullName.make_lower(); entry.FullName.make_lower();
if (IgnorePaths) if (IgnorePaths)
core::deletePathFromFilename(entry.FullName); entry.FullName = core::deletePathFromFilename(entry.FullName);
return Files.binary_search(entry); return Files.binary_search(entry);
} }

View file

@ -3,19 +3,19 @@
#include "CGLTFMeshFileLoader.h" #include "CGLTFMeshFileLoader.h"
#include "SMaterialLayer.h"
#include "coreutil.h" #include "coreutil.h"
#include "CSkinnedMesh.h" #include "CSkinnedMesh.h"
#include "ISkinnedMesh.h" #include "IAnimatedMesh.h"
#include "irrTypes.h"
#include "IReadFile.h" #include "IReadFile.h"
#include "irrTypes.h"
#include "matrix4.h" #include "matrix4.h"
#include "path.h" #include "path.h"
#include "quaternion.h" #include "quaternion.h"
#include "vector2d.h"
#include "vector3d.h" #include "vector3d.h"
#include "os.h" #include "os.h"
#include "tiniergltf.hpp"
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
@ -23,9 +23,11 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <stdexcept> #include <stdexcept>
#include <tuple>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector> #include <vector>
#include <iostream>
namespace irr { namespace irr {
@ -51,6 +53,28 @@ core::vector3df convertHandedness(const core::vector3df &p)
return core::vector3df(p.X, p.Y, -p.Z); return core::vector3df(p.X, p.Y, -p.Z);
} }
template <>
core::quaternion convertHandedness(const core::quaternion &q)
{
return core::quaternion(q.X, q.Y, -q.Z, q.W);
}
template <>
core::matrix4 convertHandedness(const core::matrix4 &mat)
{
// Base transformation between left & right handed coordinate systems.
static const core::matrix4 invertZ = core::matrix4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1);
// Convert from left-handed to right-handed,
// then apply mat,
// then convert from right-handed to left-handed.
// Both conversions just invert Z.
return invertZ * mat * invertZ;
}
namespace scene { namespace scene {
using SelfType = CGLTFMeshFileLoader; using SelfType = CGLTFMeshFileLoader;
@ -196,6 +220,8 @@ ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT)
ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT)
ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT)
ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT)
ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT)
template <class T> template <class T>
T SelfType::Accessor<T>::get(std::size_t i) const T SelfType::Accessor<T>::get(std::size_t i) const
@ -303,13 +329,11 @@ std::array<f32, N> SelfType::getNormalizedValues(
return values; return values;
} }
/**
* The most basic portion of the code base. This tells irllicht if this file has a .gltf extension.
*/
bool SelfType::isALoadableFileExtension( bool SelfType::isALoadableFileExtension(
const io::path& filename) const const io::path& filename) const
{ {
return core::hasFileExtension(filename, "gltf"); return core::hasFileExtension(filename, "gltf") ||
core::hasFileExtension(filename, "glb");
} }
/** /**
@ -324,6 +348,11 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file)
if (!model.has_value()) { if (!model.has_value()) {
return nullptr; return nullptr;
} }
if (model->extensionsRequired) {
os::Printer::log("glTF loader",
"model requires extensions, but we support none", ELL_ERROR);
return nullptr;
}
if (!(model->buffers.has_value() if (!(model->buffers.has_value()
&& model->bufferViews.has_value() && model->bufferViews.has_value()
@ -337,7 +366,7 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file)
auto *mesh = new CSkinnedMesh(); auto *mesh = new CSkinnedMesh();
MeshExtractor parser(std::move(model.value()), mesh); MeshExtractor parser(std::move(model.value()), mesh);
try { try {
parser.loadNodes(); parser.load();
} catch (std::runtime_error &e) { } catch (std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR); os::Printer::log("glTF loader", e.what(), ELL_ERROR);
mesh->drop(); mesh->drop();
@ -354,8 +383,7 @@ static void transformVertices(std::vector<video::S3DVertex> &vertices, const cor
// Apply scaling, rotation and rotation (in that order) to the position. // Apply scaling, rotation and rotation (in that order) to the position.
transform.transformVect(vertex.Pos); transform.transformVect(vertex.Pos);
// For the normal, we do not want to apply the translation. // For the normal, we do not want to apply the translation.
// TODO note that this also applies scaling; the Irrlicht method is misnamed. vertex.Normal = transform.rotateAndScaleVect(vertex.Normal);
transform.rotateVect(vertex.Normal);
// Renormalize (length might have been affected by scaling). // Renormalize (length might have been affected by scaling).
vertex.Normal.normalize(); vertex.Normal.normalize();
} }
@ -381,23 +409,33 @@ static std::vector<u16> generateIndices(const std::size_t nVerts)
return indices; return indices;
} }
/** using Wrap = tiniergltf::Sampler::Wrap;
* Load up the rawest form of the model. The vertex positions and indices. static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) {
* Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes switch (wrap) {
* If material is undefined, then a default material MUST be used. case Wrap::REPEAT:
*/ return video::ETC_REPEAT;
void SelfType::MeshExtractor::loadMesh( case Wrap::CLAMP_TO_EDGE:
const std::size_t meshIdx, return video::ETC_CLAMP_TO_EDGE;
ISkinnedMesh::SJoint *parent) const case Wrap::MIRRORED_REPEAT:
return video::ETC_MIRROR;
default:
throw std::runtime_error("invalid sampler wrapping mode");
}
}
void SelfType::MeshExtractor::addPrimitive(
const tiniergltf::MeshPrimitive &primitive,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent)
{ {
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
auto vertices = getVertices(primitive); auto vertices = getVertices(primitive);
if (!vertices.has_value()) if (!vertices.has_value())
continue; // "When positions are not specified, client implementations SHOULD skip primitives rendering" return; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
const auto n_vertices = vertices->size();
// Excludes the max value for consistency. // Excludes the max value for consistency.
if (vertices->size() >= std::numeric_limits<u16>::max()) if (n_vertices >= std::numeric_limits<u16>::max())
throw std::runtime_error("too many vertices"); throw std::runtime_error("too many vertices");
// Apply the global transform along the parent chain. // Apply the global transform along the parent chain.
@ -415,18 +453,104 @@ void SelfType::MeshExtractor::loadMesh(
m_irr_model->addMeshBuffer( m_irr_model->addMeshBuffer(
new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); new SSkinMeshBuffer(std::move(*vertices), std::move(indices)));
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr);
if (primitive.material.has_value()) { if (primitive.material.has_value()) {
const auto &material = m_gltf_model.materials->at(*primitive.material); const auto &material = m_gltf_model.materials->at(*primitive.material);
if (material.pbrMetallicRoughness.has_value()) { if (material.pbrMetallicRoughness.has_value()) {
const auto &texture = material.pbrMetallicRoughness->baseColorTexture; const auto &texture = material.pbrMetallicRoughness->baseColorTexture;
if (texture.has_value()) { if (texture.has_value()) {
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index)); m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index));
const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler;
if (samplerIdx.has_value()) {
auto &sampler = m_gltf_model.samplers->at(*samplerIdx);
auto &layer = meshbuf->getMaterial().TextureLayers[0];
layer.TextureWrapU = convertTextureWrap(sampler.wrapS);
layer.TextureWrapV = convertTextureWrap(sampler.wrapT);
} }
} }
} }
} }
if (!skinIdx.has_value()) {
// No skin => all vertices belong entirely to their parent
for (std::size_t v = 0; v < n_vertices; ++v) {
auto *weight = m_irr_model->addWeight(parent);
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = 1.0f;
}
return;
}
const auto &skin = m_gltf_model.skins->at(*skinIdx);
const auto &attrs = primitive.attributes;
const auto &joints = attrs.joints;
if (!joints.has_value())
return;
const auto &weights = attrs.weights;
for (std::size_t set = 0; set < joints->size(); ++set) {
const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> {
const auto idx = joints->at(set);
const auto &acc = m_gltf_model.accessors->at(idx);
switch (acc.componentType) {
case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE:
return Accessor<std::array<u8, 4>>::make(m_gltf_model, idx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT:
return Accessor<std::array<u16, 4>>::make(m_gltf_model, idx);
default:
throw std::runtime_error("invalid component type");
}
})();
const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set));
for (std::size_t v = 0; v < n_vertices; ++v) {
std::array<u16, 4> jointIdxs;
if (std::holds_alternative<Accessor<std::array<u8, 4>>>(jointAccessor)) {
const auto jointIdxsU8 = std::get<Accessor<std::array<u8, 4>>>(jointAccessor).get(v);
jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]};
} else if (std::holds_alternative<Accessor<std::array<u16, 4>>>(jointAccessor)) {
jointIdxs = std::get<Accessor<std::array<u16, 4>>>(jointAccessor).get(v);
}
std::array<f32, 4> strengths = getNormalizedValues(weightAccessor, v);
// 4 joints per set
for (std::size_t in_set = 0; in_set < 4; ++in_set) {
u16 jointIdx = jointIdxs[in_set];
f32 strength = strengths[in_set];
if (strength == 0)
continue;
CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx)));
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = strength;
}
}
}
}
/**
* Load up the rawest form of the model. The vertex positions and indices.
* Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes
* If material is undefined, then a default material MUST be used.
*/
void SelfType::MeshExtractor::deferAddMesh(
const std::size_t meshIdx,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent)
{
m_mesh_loaders.emplace_back([=] {
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
addPrimitive(primitive, skinIdx, parent);
}
});
} }
// Base transformation between left & right handed coordinate systems. // Base transformation between left & right handed coordinate systems.
@ -439,51 +563,75 @@ static const core::matrix4 leftToRight = core::matrix4(
); );
static const core::matrix4 rightToLeft = leftToRight; static const core::matrix4 rightToLeft = leftToRight;
static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint)
{ {
// Note: Under the hood, this casts these doubles to floats. // Note: Under the hood, this casts these doubles to floats.
return core::matrix4( core::matrix4 mat = convertHandedness(core::matrix4(
m[0], m[1], m[2], m[3], m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7], m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11], m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]); m[12], m[13], m[14], m[15]));
// Decompose the matrix into translation, scale, and rotation.
joint->Animatedposition = mat.getTranslation();
auto scale = mat.getScale();
joint->Animatedscale = scale;
core::matrix4 inverseScale;
inverseScale.setScale(core::vector3df(
scale.X == 0 ? 0 : 1 / scale.X,
scale.Y == 0 ? 0 : 1 / scale.Y,
scale.Z == 0 ? 0 : 1 / scale.Z));
core::matrix4 axisNormalizedMat = inverseScale * mat;
joint->Animatedrotation = axisNormalizedMat.getRotationDegrees();
// Invert the rotation because it is applied using `getMatrix_transposed`,
// which again inverts.
joint->Animatedrotation.makeInverse();
return mat;
} }
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, CSkinnedMesh::SJoint *joint)
{ {
const auto &trans = trs.translation; const auto &trans = trs.translation;
const auto &rot = trs.rotation; const auto &rot = trs.rotation;
const auto &scale = trs.scale; const auto &scale = trs.scale;
core::matrix4 transMat; core::matrix4 transMat;
transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2]));
core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); 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; core::matrix4 scaleMat;
scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); scaleMat.setScale(joint->Animatedscale);
return transMat * rotMat * scaleMat; return transMat * rotMat * scaleMat;
} }
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform) { static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform,
CSkinnedMesh::SJoint *joint) {
if (!transform.has_value()) { if (!transform.has_value()) {
return core::matrix4(); return core::matrix4();
} }
core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform);
return rightToLeft * mat * leftToRight;
} }
void SelfType::MeshExtractor::loadNode( void SelfType::MeshExtractor::loadNode(
const std::size_t nodeIdx, const std::size_t nodeIdx,
ISkinnedMesh::SJoint *parent) const CSkinnedMesh::SJoint *parent)
{ {
const auto &node = m_gltf_model.nodes->at(nodeIdx); const auto &node = m_gltf_model.nodes->at(nodeIdx);
auto *joint = m_irr_model->addJoint(parent); auto *joint = m_irr_model->addJoint(parent);
const core::matrix4 transform = loadTransform(node.transform); const core::matrix4 transform = loadTransform(node.transform, joint);
joint->LocalMatrix = transform; joint->LocalMatrix = transform;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix;
if (node.name.has_value()) { if (node.name.has_value()) {
joint->Name = node.name->c_str(); joint->Name = node.name->c_str();
} }
m_loaded_nodes[nodeIdx] = joint;
if (node.mesh.has_value()) { if (node.mesh.has_value()) {
loadMesh(*node.mesh, joint); deferAddMesh(*node.mesh, node.skin, joint);
} }
if (node.children.has_value()) { if (node.children.has_value()) {
for (const auto &child : *node.children) { for (const auto &child : *node.children) {
@ -492,8 +640,10 @@ void SelfType::MeshExtractor::loadNode(
} }
} }
void SelfType::MeshExtractor::loadNodes() const void SelfType::MeshExtractor::loadNodes()
{ {
m_loaded_nodes = std::vector<CSkinnedMesh::SJoint *>(m_gltf_model.nodes->size());
std::vector<bool> isChild(m_gltf_model.nodes->size()); std::vector<bool> isChild(m_gltf_model.nodes->size());
for (const auto &node : *m_gltf_model.nodes) { for (const auto &node : *m_gltf_model.nodes) {
if (!node.children.has_value()) if (!node.children.has_value())
@ -511,6 +661,92 @@ void SelfType::MeshExtractor::loadNodes() const
} }
} }
void SelfType::MeshExtractor::loadSkins()
{
if (!m_gltf_model.skins.has_value())
return;
for (const auto &skin : *m_gltf_model.skins) {
if (!skin.inverseBindMatrices.has_value())
continue;
const auto accessor = Accessor<core::matrix4>::make(m_gltf_model, *skin.inverseBindMatrices);
if (accessor.getCount() < skin.joints.size())
throw std::runtime_error("accessor contains too few matrices");
for (std::size_t i = 0; i < skin.joints.size(); ++i) {
m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i));
}
}
}
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);
if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR)
throw std::runtime_error("unsupported interpolation");
const auto inputAccessor = Accessor<f32>::make(m_gltf_model, sampler.input);
const auto n_frames = inputAccessor.getCount();
if (!channel.target.node.has_value())
throw std::runtime_error("no animated node");
const auto &joint = m_loaded_nodes.at(*channel.target.node);
switch (channel.target.path) {
case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addPositionKey(joint);
key->frame = inputAccessor.get(i);
key->position = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::ROTATION: {
const auto outputAccessor = Accessor<core::quaternion>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addRotationKey(joint);
key->frame = inputAccessor.get(i);
key->rotation = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::SCALE: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addScaleKey(joint);
key->frame = inputAccessor.get(i);
key->scale = outputAccessor.get(i);
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::WEIGHTS:
throw std::runtime_error("no support for morph animations");
}
}
}
void SelfType::MeshExtractor::load()
{
loadNodes();
for (const auto &load_mesh : m_mesh_loaders) {
load_mesh();
}
loadSkins();
// Load the first animation, if there is one.
if (m_gltf_model.animations.has_value()) {
if (m_gltf_model.animations->size() > 1) {
os::Printer::log("glTF loader",
"multiple animations are not supported", ELL_WARNING);
}
loadAnimation(0);
m_irr_model->setAnimationSpeed(1);
}
m_irr_model->finalize();
}
/** /**
* Extracts GLTF mesh indices. * Extracts GLTF mesh indices.
*/ */
@ -650,11 +886,19 @@ void SelfType::MeshExtractor::copyTCoords(
const std::size_t accessorIdx, const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const std::vector<video::S3DVertex>& vertices) const
{ {
const auto componentType = m_gltf_model.accessors->at(accessorIdx).componentType;
if (componentType == tiniergltf::Accessor::ComponentType::FLOAT) {
// If floats are used, they need not be normalized: Wrapping may take effect.
const auto accessor = Accessor<std::array<f32, 2>>::make(m_gltf_model, accessorIdx);
for (std::size_t i = 0; i < accessor.getCount(); ++i) {
vertices[i].TCoords = core::vector2d<f32>(accessor.get(i));
}
} else {
const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx); const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx);
const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor);
for (std::size_t i = 0; i < count; ++i) { for (std::size_t i = 0; i < count; ++i) {
const auto vals = getNormalizedValues(accessor, i); vertices[i].TCoords = core::vector2d<f32>(getNormalizedValues(accessor, i));
vertices[i].TCoords = core::vector2df(vals[0], vals[1]); }
} }
} }
@ -663,6 +907,7 @@ void SelfType::MeshExtractor::copyTCoords(
*/ */
std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file) std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
{ {
const bool isGlb = core::hasFileExtension(file->getFileName(), "glb");
auto size = file->getSize(); auto size = file->getSize();
if (size < 0) // this can happen if `ftell` fails if (size < 0) // this can happen if `ftell` fails
return std::nullopt; return std::nullopt;
@ -671,15 +916,11 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
return std::nullopt; return std::nullopt;
// We probably don't need this, but add it just to be sure. // We probably don't need this, but add it just to be sure.
buf[size] = '\0'; buf[size] = '\0';
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) {
return std::nullopt;
}
try { try {
return tiniergltf::GlTF(json); if (isGlb)
return tiniergltf::readGlb(buf.get(), size);
else
return tiniergltf::readGlTF(buf.get(), size);
} catch (const std::runtime_error &e) { } catch (const std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR); os::Printer::log("glTF loader", e.what(), ELL_ERROR);
return std::nullopt; return std::nullopt;
@ -692,4 +933,3 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
} // namespace scene } // namespace scene
} // namespace irr } // namespace irr

View file

@ -10,9 +10,11 @@
#include "path.h" #include "path.h"
#include "S3DVertex.h" #include "S3DVertex.h"
#include <tiniergltf.hpp> #include "tiniergltf.hpp"
#include <functional>
#include <cstddef> #include <cstddef>
#include <tuple>
#include <vector> #include <vector>
namespace irr namespace irr
@ -26,9 +28,9 @@ class CGLTFMeshFileLoader : public IMeshLoader
public: public:
CGLTFMeshFileLoader() noexcept {}; CGLTFMeshFileLoader() noexcept {};
bool isALoadableFileExtension(const io::path& filename) const override; bool isALoadableFileExtension(const io::path &filename) const override;
IAnimatedMesh* createMesh(io::IReadFile* file) override; IAnimatedMesh *createMesh(io::IReadFile *file) override;
private: private:
template <typename T> template <typename T>
@ -94,11 +96,12 @@ private:
const NormalizedValuesAccessor<N> &accessor, const NormalizedValuesAccessor<N> &accessor,
const std::size_t i); const std::size_t i);
class MeshExtractor { class MeshExtractor
{
public: public:
MeshExtractor(tiniergltf::GlTF &&model, MeshExtractor(tiniergltf::GlTF &&model,
CSkinnedMesh *mesh) noexcept CSkinnedMesh *mesh) noexcept
: m_gltf_model(model), m_irr_model(mesh) {}; : m_gltf_model(std::move(model)), m_irr_model(mesh) {};
/* Gets indices for the given mesh/primitive. /* Gets indices for the given mesh/primitive.
* *
@ -114,12 +117,15 @@ private:
std::size_t getPrimitiveCount(const std::size_t meshIdx) const; std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
void loadNodes() const; void load();
private: private:
const tiniergltf::GlTF m_gltf_model; const tiniergltf::GlTF m_gltf_model;
CSkinnedMesh *m_irr_model; CSkinnedMesh *m_irr_model;
std::vector<std::function<void()>> m_mesh_loaders;
std::vector<CSkinnedMesh::SJoint *> m_loaded_nodes;
void copyPositions(const std::size_t accessorIdx, void copyPositions(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const; std::vector<video::S3DVertex>& vertices) const;
@ -129,16 +135,24 @@ private:
void copyTCoords(const std::size_t accessorIdx, void copyTCoords(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const; std::vector<video::S3DVertex>& vertices) const;
void loadMesh( void addPrimitive(const tiniergltf::MeshPrimitive &primitive,
std::size_t meshIdx, const std::optional<std::size_t> skinIdx,
ISkinnedMesh::SJoint *parentJoint) const; CSkinnedMesh::SJoint *parent);
void loadNode( void deferAddMesh(const std::size_t meshIdx,
const std::size_t nodeIdx, const std::optional<std::size_t> skinIdx,
ISkinnedMesh::SJoint *parentJoint) const; CSkinnedMesh::SJoint *parentJoint);
void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint);
void loadNodes();
void loadSkins();
void loadAnimation(const std::size_t animIdx);
}; };
std::optional<tiniergltf::GlTF> tryParseGLTF(io::IReadFile* file); std::optional<tiniergltf::GlTF> tryParseGLTF(io::IReadFile *file);
}; };
} // namespace scene } // namespace scene

View file

@ -721,12 +721,19 @@ bool CIrrDeviceSDL::run()
irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT; irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED; irrevent.MouseInput.Event = irr::EMIE_MOUSE_MOVED;
MouseX = irrevent.MouseInput.X =
static_cast<s32>(SDL_event.motion.x * ScaleX);
MouseY = irrevent.MouseInput.Y =
static_cast<s32>(SDL_event.motion.y * ScaleY);
MouseXRel = static_cast<s32>(SDL_event.motion.xrel * ScaleX); MouseXRel = static_cast<s32>(SDL_event.motion.xrel * ScaleX);
MouseYRel = static_cast<s32>(SDL_event.motion.yrel * ScaleY); MouseYRel = static_cast<s32>(SDL_event.motion.yrel * ScaleY);
if (!SDL_GetRelativeMouseMode()) {
MouseX = static_cast<s32>(SDL_event.motion.x * ScaleX);
MouseY = static_cast<s32>(SDL_event.motion.y * ScaleY);
} else {
MouseX += MouseXRel;
MouseY += MouseYRel;
}
irrevent.MouseInput.X = MouseX;
irrevent.MouseInput.Y = MouseY;
irrevent.MouseInput.ButtonStates = MouseButtonStates; irrevent.MouseInput.ButtonStates = MouseButtonStates;
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0; irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0; irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;

View file

@ -158,9 +158,13 @@ public:
//! Sets the new position of the cursor. //! Sets the new position of the cursor.
void setPosition(s32 x, s32 y) override void setPosition(s32 x, s32 y) override
{ {
#ifndef __ANDROID__
// On Android, this somehow results in a camera jump when enabling
// relative mouse mode and it isn't supported anyway.
SDL_WarpMouseInWindow(Device->Window, SDL_WarpMouseInWindow(Device->Window,
static_cast<int>(x / Device->ScaleX), static_cast<int>(x / Device->ScaleX),
static_cast<int>(y / Device->ScaleY)); static_cast<int>(y / Device->ScaleY));
#endif
if (SDL_GetRelativeMouseMode()) { if (SDL_GetRelativeMouseMode()) {
// There won't be an event for this warp (details on libsdl-org/SDL/issues/6034) // There won't be an event for this warp (details on libsdl-org/SDL/issues/6034)
@ -298,6 +302,7 @@ private:
#endif #endif
s32 MouseX, MouseY; s32 MouseX, MouseY;
// these two only continue to exist for some Emscripten stuff idk about
s32 MouseXRel, MouseYRel; s32 MouseXRel, MouseYRel;
u32 MouseButtonStates; u32 MouseButtonStates;

View file

@ -480,8 +480,8 @@ add_library(IrrlichtMt::IrrlichtMt ALIAS IrrlichtMt)
target_include_directories(IrrlichtMt target_include_directories(IrrlichtMt
PUBLIC PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/>" "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
PRIVATE PRIVATE
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
${link_includes} ${link_includes}
) )

View file

@ -193,7 +193,7 @@ s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const
//! Returns amount of polygons in mesh. //! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const
{ {
if (mesh && mesh->getFrameCount() != 0) if (mesh && mesh->getMaxFrameNumber() != 0)
return getPolyCount(mesh->getMesh(0)); return getPolyCount(mesh->getMesh(0));
return 0; return 0;

View file

@ -111,11 +111,9 @@ CSkinnedMesh::~CSkinnedMesh()
} }
} }
//! returns the amount of frames in milliseconds. f32 CSkinnedMesh::getMaxFrameNumber() const
//! If the amount is 1, it is a static (=non animated) mesh.
u32 CSkinnedMesh::getFrameCount() const
{ {
return core::floor32(EndFrame + 1.f); return EndFrame;
} }
//! Gets the default animation speed of the animated mesh. //! Gets the default animation speed of the animated mesh.
@ -133,14 +131,14 @@ void CSkinnedMesh::setAnimationSpeed(f32 fps)
FramesPerSecond = fps; FramesPerSecond = fps;
} }
//! returns the animated mesh based on a detail level. 0 is the lowest, 255 the highest detail. Note, that some Meshes will ignore the detail level. //! returns the animated mesh based
IMesh *CSkinnedMesh::getMesh(s32 frame, s32 detailLevel, s32 startFrameLoop, s32 endFrameLoop) IMesh *CSkinnedMesh::getMesh(f32 frame)
{ {
// animate(frame,startFrameLoop, endFrameLoop); // animate(frame,startFrameLoop, endFrameLoop);
if (frame == -1) if (frame == -1)
return this; return this;
animateMesh((f32)frame, 1.0f); animateMesh(frame, 1.0f);
skinMesh(); skinMesh();
return this; return this;
} }
@ -222,6 +220,7 @@ void CSkinnedMesh::buildAllLocalAnimatedMatrices()
// IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility. // 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! // 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->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix);
// --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() --- // --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() ---
@ -496,8 +495,8 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
{ {
if (joint->Weights.size()) { if (joint->Weights.size()) {
// Find this joints pull on vertices... // Find this joints pull on vertices...
core::matrix4 jointVertexPull(core::matrix4::EM4CONST_NOTHING); // Note: It is assumed that the global inversed matrix has been calculated at this point.
jointVertexPull.setbyproduct(joint->GlobalAnimatedMatrix, joint->GlobalInversedMatrix); core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value();
core::vector3df thisVertexMove, thisNormalMove; core::vector3df thisVertexMove, thisNormalMove;
@ -510,8 +509,10 @@ void CSkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
// Pull this vertex... // Pull this vertex...
jointVertexPull.transformVect(thisVertexMove, weight.StaticPos); jointVertexPull.transformVect(thisVertexMove, weight.StaticPos);
if (AnimateNormals) if (AnimateNormals) {
jointVertexPull.rotateVect(thisNormalMove, weight.StaticNormal); thisNormalMove = jointVertexPull.rotateAndScaleVect(weight.StaticNormal);
thisNormalMove.normalize(); // must renormalize after potentially scaling
}
if (!(*(weight.Moved))) { if (!(*(weight.Moved))) {
*(weight.Moved) = true; *(weight.Moved) = true;
@ -764,9 +765,9 @@ void CSkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint)
joint->LocalAnimatedMatrix = joint->LocalMatrix; joint->LocalAnimatedMatrix = joint->LocalMatrix;
joint->GlobalAnimatedMatrix = joint->GlobalMatrix; joint->GlobalAnimatedMatrix = joint->GlobalMatrix;
if (joint->GlobalInversedMatrix.isIdentity()) { // might be pre calculated if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated
joint->GlobalInversedMatrix = joint->GlobalMatrix; joint->GlobalInversedMatrix = joint->GlobalMatrix;
joint->GlobalInversedMatrix.makeInverse(); // slow joint->GlobalInversedMatrix->makeInverse(); // slow
} }
for (u32 j = 0; j < joint->Children.size(); ++j) for (u32 j = 0; j < joint->Children.size(); ++j)

View file

@ -27,8 +27,8 @@ public:
//! destructor //! destructor
virtual ~CSkinnedMesh(); virtual ~CSkinnedMesh();
//! returns the amount of frames. If the amount is 1, it is a static (=non animated) mesh. //! If the duration is 0, it is a static (=non animated) mesh.
u32 getFrameCount() const override; f32 getMaxFrameNumber() const override;
//! Gets the default animation speed of the animated mesh. //! 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. */ /** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */
@ -39,8 +39,8 @@ public:
The actual speed is set in the scene node the mesh is instantiated in.*/ The actual speed is set in the scene node the mesh is instantiated in.*/
void setAnimationSpeed(f32 fps) override; void setAnimationSpeed(f32 fps) override;
//! returns the animated mesh based on a detail level (which is ignored) //! returns the animated mesh for the given frame
IMesh *getMesh(s32 frame, s32 detailLevel = 255, s32 startFrameLoop = -1, s32 endFrameLoop = -1) override; IMesh *getMesh(f32) override;
//! Animates this mesh's joints based on frame input //! Animates this mesh's joints based on frame input
//! blend: {0-old position, 1-New position} //! blend: {0-old position, 1-New position}

View file

@ -990,9 +990,9 @@ bool CXMeshFileLoader::parseDataObjectSkinWeights(SXMesh &mesh)
// transforms the mesh vertices to the space of the bone // transforms the mesh vertices to the space of the bone
// When concatenated to the bone's transform, this provides the // When concatenated to the bone's transform, this provides the
// world space coordinates of the mesh as affected by the bone // world space coordinates of the mesh as affected by the bone
core::matrix4 &MatrixOffset = joint->GlobalInversedMatrix; core::matrix4 MatrixOffset;
readMatrix(MatrixOffset); readMatrix(MatrixOffset);
joint->GlobalInversedMatrix = MatrixOffset;
if (!checkForOneFollowingSemicolons()) { if (!checkForOneFollowingSemicolons()) {
os::Printer::log("No finishing semicolon in Skin Weights found in x file", ELL_WARNING); os::Printer::log("No finishing semicolon in Skin Weights found in x file", ELL_WARNING);

View file

@ -191,8 +191,7 @@ bool CZipReader::scanGZipHeader()
} }
} else { } else {
// no file name? // no file name?
ZipFileName = Path; ZipFileName = core::deletePathFromFilename(Path);
core::deletePathFromFilename(ZipFileName);
// rename tgz to tar or remove gz extension // rename tgz to tar or remove gz extension
if (core::hasFileExtension(ZipFileName, "tgz")) { if (core::hasFileExtension(ZipFileName, "tgz")) {

View file

@ -1,6 +1,9 @@
#pragma once #pragma once
#include <json/json.h> #include <json/json.h>
#include "util/base64.h"
#include <cstdint>
#include <functional> #include <functional>
#include <stack> #include <stack>
#include <string> #include <string>
@ -13,7 +16,6 @@
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include "util/base64.h"
namespace tiniergltf { namespace tiniergltf {
@ -460,7 +462,8 @@ struct Buffer {
std::optional<std::string> name; std::optional<std::string> name;
std::string data; std::string data;
Buffer(const Json::Value &o, Buffer(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI) const std::function<std::string(const std::string &uri)> &resolveURI,
std::optional<std::string> &&glbData = std::nullopt)
: byteLength(as<std::size_t>(o["byteLength"])) : byteLength(as<std::size_t>(o["byteLength"]))
{ {
check(o.isObject()); check(o.isObject());
@ -468,6 +471,13 @@ struct Buffer {
if (o.isMember("name")) { if (o.isMember("name")) {
name = as<std::string>(o["name"]); name = as<std::string>(o["name"]);
} }
if (glbData.has_value()) {
check(!o.isMember("uri"));
data = *std::move(glbData);
// GLB allows padding, which need not be reflected in the JSON
check(byteLength + 3 >= data.size());
check(data.size() >= byteLength);
} else {
check(o.isMember("uri")); check(o.isMember("uri"));
bool dataURI = false; bool dataURI = false;
const std::string uri = as<std::string>(o["uri"]); const std::string uri = as<std::string>(o["uri"]);
@ -486,6 +496,7 @@ struct Buffer {
if (!dataURI) if (!dataURI)
data = resolveURI(uri); data = resolveURI(uri);
check(data.size() >= byteLength); check(data.size() >= byteLength);
}
data.resize(byteLength); data.resize(byteLength);
} }
}; };
@ -969,21 +980,16 @@ struct Sampler {
}; };
std::optional<MinFilter> minFilter; std::optional<MinFilter> minFilter;
std::optional<std::string> name; std::optional<std::string> name;
enum class WrapS { enum class Wrap {
REPEAT, REPEAT,
CLAMP_TO_EDGE, CLAMP_TO_EDGE,
MIRRORED_REPEAT, MIRRORED_REPEAT,
}; };
WrapS wrapS; Wrap wrapS;
enum class WrapT { Wrap wrapT;
REPEAT,
CLAMP_TO_EDGE,
MIRRORED_REPEAT,
};
WrapT wrapT;
Sampler(const Json::Value &o) Sampler(const Json::Value &o)
: wrapS(WrapS::REPEAT) : wrapS(Wrap::REPEAT)
, wrapT(WrapT::REPEAT) , wrapT(Wrap::REPEAT)
{ {
check(o.isObject()); check(o.isObject());
if (o.isMember("magFilter")) { if (o.isMember("magFilter")) {
@ -1009,21 +1015,16 @@ struct Sampler {
if (o.isMember("name")) { if (o.isMember("name")) {
name = as<std::string>(o["name"]); name = as<std::string>(o["name"]);
} }
if (o.isMember("wrapS")) { static std::unordered_map<Json::UInt64, Wrap> map = {
static std::unordered_map<Json::UInt64, WrapS> map = { {10497, Wrap::REPEAT},
{10497, WrapS::REPEAT}, {33071, Wrap::CLAMP_TO_EDGE},
{33071, WrapS::CLAMP_TO_EDGE}, {33648, Wrap::MIRRORED_REPEAT},
{33648, WrapS::MIRRORED_REPEAT},
}; };
if (o.isMember("wrapS")) {
const auto &v = o["wrapS"]; check(v.isUInt64()); const auto &v = o["wrapS"]; check(v.isUInt64());
wrapS = map.at(v.asUInt64()); wrapS = map.at(v.asUInt64());
} }
if (o.isMember("wrapT")) { if (o.isMember("wrapT")) {
static std::unordered_map<Json::UInt64, WrapT> map = {
{10497, WrapT::REPEAT},
{33071, WrapT::CLAMP_TO_EDGE},
{33648, WrapT::MIRRORED_REPEAT},
};
const auto &v = o["wrapT"]; check(v.isUInt64()); const auto &v = o["wrapT"]; check(v.isUInt64());
wrapT = map.at(v.asUInt64()); wrapT = map.at(v.asUInt64());
} }
@ -1093,6 +1094,12 @@ struct Texture {
}; };
template<> Texture as(const Json::Value &o) { return o; } template<> Texture as(const Json::Value &o) { return o; }
using UriResolver = std::function<std::string(const std::string &uri)>;
static inline std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
struct GlTF { struct GlTF {
std::optional<std::vector<Accessor>> accessors; std::optional<std::vector<Accessor>> accessors;
std::optional<std::vector<Animation>> animations; std::optional<std::vector<Animation>> animations;
@ -1111,12 +1118,10 @@ struct GlTF {
std::optional<std::vector<Scene>> scenes; std::optional<std::vector<Scene>> scenes;
std::optional<std::vector<Skin>> skins; std::optional<std::vector<Skin>> skins;
std::optional<std::vector<Texture>> textures; std::optional<std::vector<Texture>> textures;
static std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
GlTF(const Json::Value &o, GlTF(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI = uriError) const UriResolver &resolveUri = uriError,
std::optional<std::string> &&glbData = std::nullopt)
: asset(as<Asset>(o["asset"])) : asset(as<Asset>(o["asset"]))
{ {
check(o.isObject()); check(o.isObject());
@ -1138,7 +1143,8 @@ struct GlTF {
std::vector<Buffer> bufs; std::vector<Buffer> bufs;
bufs.reserve(b.size()); bufs.reserve(b.size());
for (Json::ArrayIndex i = 0; i < b.size(); ++i) { for (Json::ArrayIndex i = 0; i < b.size(); ++i) {
bufs.emplace_back(b[i], resolveURI); bufs.emplace_back(b[i], resolveUri,
i == 0 ? std::move(glbData) : std::nullopt);
} }
check(bufs.size() >= 1); check(bufs.size() >= 1);
buffers = std::move(bufs); buffers = std::move(bufs);
@ -1354,4 +1360,123 @@ struct GlTF {
} }
}; };
// std::span is C++ 20, so we roll our own little struct here.
template <typename T>
struct Span {
T *ptr;
uint32_t len;
bool empty() const {
return len == 0;
}
T *end() const {
return ptr + len;
}
template <typename U>
Span<U> cast() const {
return {(U *) ptr, len};
}
};
static Json::Value readJson(Span<const char> span) {
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(span.ptr, span.end(), &json, &err))
throw std::runtime_error(std::string("invalid JSON: ") + err);
return json;
}
inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
struct Chunk {
uint32_t type;
Span<const uint8_t> span;
};
struct Stream {
Span<const uint8_t> span;
bool eof() const {
return span.empty();
}
void advance(uint32_t n) {
span.len -= n;
span.ptr += n;
}
uint32_t readUint32() {
if (span.len < 4)
throw std::runtime_error("premature EOF");
uint32_t res = 0;
for (int i = 0; i < 4; ++i)
res += span.ptr[i] << (i * 8);
advance(4);
return res;
}
Chunk readChunk() {
const auto chunkLen = readUint32();
if (chunkLen % 4 != 0)
throw std::runtime_error("chunk length must be multiple of 4");
const auto chunkType = readUint32();
auto chunkPtr = span.ptr;
if (span.len < chunkLen)
throw std::runtime_error("premature EOF");
advance(chunkLen);
return {chunkType, {chunkPtr, chunkLen}};
}
};
constexpr uint32_t MAGIC_GLTF = 0x46546C67;
constexpr uint32_t MAGIC_JSON = 0x4E4F534A;
constexpr uint32_t MAGIC_BIN = 0x004E4942;
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
Stream is{{(const uint8_t *) data, static_cast<uint32_t>(len)}};
const auto magic = is.readUint32();
if (magic != MAGIC_GLTF)
throw std::runtime_error("wrong magic number");
const auto version = is.readUint32();
if (version != 2)
throw std::runtime_error("wrong version");
const auto length = is.readUint32();
if (length != len)
throw std::runtime_error("wrong length");
const auto json = is.readChunk();
if (json.type != MAGIC_JSON)
throw std::runtime_error("expected JSON chunk");
std::optional<std::string> buffer;
if (!is.eof()) {
const auto chunk = is.readChunk();
if (chunk.type == MAGIC_BIN)
buffer = std::string((const char *) chunk.span.ptr, chunk.span.len);
else if (chunk.type == MAGIC_JSON)
throw std::runtime_error("unexpected chunk");
// Ignore all other chunks. We still want to validate that
// 1. These chunks are valid;
// 2. These chunks are *not* JSON or BIN chunks
while (!is.eof()) {
const auto type = is.readChunk().type;
if (type == MAGIC_JSON || type == MAGIC_BIN)
throw std::runtime_error("unexpected chunk");
}
}
return GlTF(readJson(json.span.cast<const char>()), resolveUri, std::move(buffer));
}
inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
return GlTF(readJson({data, static_cast<uint32_t>(len)}), resolveUri);
}
} }

View file

@ -405,10 +405,11 @@ void Camera::update(LocalPlayer* player, f32 frametime, f32 tool_reload_ratio)
// Compute absolute camera position and target // Compute absolute camera position and target
m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos); m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos);
m_headnode->getAbsoluteTransformation().rotateVect(m_camera_direction, rel_cam_target - rel_cam_pos); m_camera_direction = m_headnode->getAbsoluteTransformation()
.rotateAndScaleVect(rel_cam_target - rel_cam_pos);
v3f abs_cam_up; v3f abs_cam_up = m_headnode->getAbsoluteTransformation()
m_headnode->getAbsoluteTransformation().rotateVect(abs_cam_up, rel_cam_up); .rotateAndScaleVect(rel_cam_up);
// Separate camera position for calculation // Separate camera position for calculation
v3f my_cp = m_camera_position; v3f my_cp = m_camera_position;

View file

@ -827,7 +827,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
} }
const char *model_ext[] = { const char *model_ext[] = {
".x", ".b3d", ".obj", ".gltf", ".x", ".b3d", ".obj", ".gltf", ".glb",
NULL NULL
}; };
name = removeStringEnd(filename, model_ext); name = removeStringEnd(filename, model_ext);
@ -1034,7 +1034,7 @@ void Client::Send(NetworkPacket* pkt)
m_con->Send(PEER_ID_SERVER, scf.channel, pkt, scf.reliable); m_con->Send(PEER_ID_SERVER, scf.channel, pkt, scf.reliable);
} }
// Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 bytes // Will fill up 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4 bytes
void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt, bool camera_inverted) void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt, bool camera_inverted)
{ {
v3f pf = myplayer->getPosition() * 100; v3f pf = myplayer->getPosition() * 100;
@ -1046,6 +1046,8 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *
u8 fov = std::fmin(255.0f, clientMap->getCameraFov() * 80.0f); u8 fov = std::fmin(255.0f, clientMap->getCameraFov() * 80.0f);
u8 wanted_range = std::fmin(255.0f, u8 wanted_range = std::fmin(255.0f,
std::ceil(clientMap->getWantedRange() * (1.0f / MAP_BLOCKSIZE))); std::ceil(clientMap->getWantedRange() * (1.0f / MAP_BLOCKSIZE)));
f32 movement_speed = myplayer->control.movement_speed;
f32 movement_dir = myplayer->control.movement_direction;
v3s32 position(pf.X, pf.Y, pf.Z); v3s32 position(pf.X, pf.Y, pf.Z);
v3s32 speed(sf.X, sf.Y, sf.Z); v3s32 speed(sf.X, sf.Y, sf.Z);
@ -1060,10 +1062,13 @@ void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *
[12+12+4+4+4] u8 fov*80 [12+12+4+4+4] u8 fov*80
[12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE) [12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE)
[12+12+4+4+4+1+1] u8 camera_inverted (bool) [12+12+4+4+4+1+1] u8 camera_inverted (bool)
[12+12+4+4+4+1+1+1] f32 movement_speed
[12+12+4+4+4+1+1+1+4] f32 movement_direction
*/ */
*pkt << position << speed << pitch << yaw << keyPressed; *pkt << position << speed << pitch << yaw << keyPressed;
*pkt << fov << wanted_range; *pkt << fov << wanted_range;
*pkt << camera_inverted; *pkt << camera_inverted;
*pkt << movement_speed << movement_dir;
} }
void Client::interact(InteractAction action, const PointedThing& pointed) void Client::interact(InteractAction action, const PointedThing& pointed)
@ -1142,7 +1147,7 @@ void Client::sendInit(const std::string &playerName)
NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size())); NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size()));
pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) 0; pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) 0;
pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX; pkt << CLIENT_PROTOCOL_VERSION_MIN << LATEST_PROTOCOL_VERSION;
pkt << playerName; pkt << playerName;
Send(&pkt); Send(&pkt);
@ -1397,6 +1402,8 @@ void Client::sendPlayerPos()
u32 keyPressed = player->control.getKeysPressed(); u32 keyPressed = player->control.getKeysPressed();
bool camera_inverted = m_camera->getCameraMode() == CAMERA_MODE_THIRD_FRONT; bool camera_inverted = m_camera->getCameraMode() == CAMERA_MODE_THIRD_FRONT;
f32 movement_speed = player->control.movement_speed;
f32 movement_dir = player->control.movement_direction;
if ( if (
player->last_position == player->getPosition() && player->last_position == player->getPosition() &&
@ -1406,7 +1413,9 @@ void Client::sendPlayerPos()
player->last_keyPressed == keyPressed && player->last_keyPressed == keyPressed &&
player->last_camera_fov == camera_fov && player->last_camera_fov == camera_fov &&
player->last_camera_inverted == camera_inverted && player->last_camera_inverted == camera_inverted &&
player->last_wanted_range == wanted_range) player->last_wanted_range == wanted_range &&
player->last_movement_speed == movement_speed &&
player->last_movement_dir == movement_dir)
return; return;
player->last_position = player->getPosition(); player->last_position = player->getPosition();
@ -1417,8 +1426,10 @@ void Client::sendPlayerPos()
player->last_camera_fov = camera_fov; player->last_camera_fov = camera_fov;
player->last_camera_inverted = camera_inverted; player->last_camera_inverted = camera_inverted;
player->last_wanted_range = wanted_range; player->last_wanted_range = wanted_range;
player->last_movement_speed = movement_speed;
player->last_movement_dir = movement_dir;
NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1); NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1 + 1 + 4 + 4);
writePlayerPos(player, &map, &pkt, camera_inverted); writePlayerPos(player, &map, &pkt, camera_inverted);

View file

@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gameparams.h" #include "gameparams.h"
#include "script/common/c_types.h" // LuaError #include "script/common/c_types.h" // LuaError
#include "util/numeric.h" #include "util/numeric.h"
#include "util/string.h" // StringMap
#ifdef SERVER #ifdef SERVER
#error Do not include in server builds #error Do not include in server builds

View file

@ -1015,8 +1015,7 @@ int ClientMap::getBackgroundBrightness(float max_d, u32 daylight_factor,
v3f z_dir = z_directions[i]; v3f z_dir = z_directions[i];
core::CMatrix4<f32> a; core::CMatrix4<f32> a;
a.buildRotateFromTo(v3f(0,1,0), z_dir); a.buildRotateFromTo(v3f(0,1,0), z_dir);
v3f dir = m_camera_direction; v3f dir = a.rotateAndScaleVect(m_camera_direction);
a.rotateVect(dir);
int br = 0; int br = 0;
float step = BS*1.5; float step = BS*1.5;
if(max_d > 35*BS) if(max_d > 35*BS)

View file

@ -1052,7 +1052,7 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
walking = true; walking = true;
} }
v2s32 new_anim = v2s32(0,0); v2f new_anim(0,0);
bool allow_update = false; bool allow_update = false;
// increase speed if using fast or flying fast // increase speed if using fast or flying fast
@ -1799,10 +1799,9 @@ void GenericCAO::processMessage(const std::string &data)
phys.speed_walk = override_speed_walk; phys.speed_walk = override_speed_walk;
} }
} else if (cmd == AO_CMD_SET_ANIMATION) { } else if (cmd == AO_CMD_SET_ANIMATION) {
// TODO: change frames send as v2s32 value
v2f range = readV2F32(is); v2f range = readV2F32(is);
if (!m_is_local_player) { if (!m_is_local_player) {
m_animation_range = v2s32((s32)range.X, (s32)range.Y); m_animation_range = range;
m_animation_speed = readF32(is); m_animation_speed = readF32(is);
m_animation_blend = readF32(is); m_animation_blend = readF32(is);
// these are sent inverted so we get true when the server sends nothing // these are sent inverted so we get true when the server sends nothing
@ -1812,7 +1811,7 @@ void GenericCAO::processMessage(const std::string &data)
LocalPlayer *player = m_env->getLocalPlayer(); LocalPlayer *player = m_env->getLocalPlayer();
if(player->last_animation == LocalPlayerAnimation::NO_ANIM) if(player->last_animation == LocalPlayerAnimation::NO_ANIM)
{ {
m_animation_range = v2s32((s32)range.X, (s32)range.Y); m_animation_range = range;
m_animation_speed = readF32(is); m_animation_speed = readF32(is);
m_animation_blend = readF32(is); m_animation_blend = readF32(is);
// these are sent inverted so we get true when the server sends nothing // these are sent inverted so we get true when the server sends nothing

View file

@ -99,7 +99,7 @@ private:
v2s16 m_tx_basepos; v2s16 m_tx_basepos;
bool m_initial_tx_basepos_set = false; bool m_initial_tx_basepos_set = false;
bool m_tx_select_horiz_by_yawpitch = false; bool m_tx_select_horiz_by_yawpitch = false;
v2s32 m_animation_range; v2f m_animation_range;
float m_animation_speed = 15.0f; float m_animation_speed = 15.0f;
float m_animation_blend = 0.0f; float m_animation_blend = 0.0f;
bool m_animation_loop = true; bool m_animation_loop = true;

View file

@ -1016,13 +1016,6 @@ void MapblockMeshGenerator::drawGlasslikeFramedNode()
} }
} }
void MapblockMeshGenerator::drawAllfacesNode()
{
static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2);
useTile(0, 0, 0);
drawAutoLightedCuboid(box);
}
void MapblockMeshGenerator::drawTorchlikeNode() void MapblockMeshGenerator::drawTorchlikeNode()
{ {
u8 wall = cur_node.n.getWallMounted(nodedef); u8 wall = cur_node.n.getWallMounted(nodedef);
@ -1545,6 +1538,17 @@ namespace {
}; };
} }
void MapblockMeshGenerator::drawAllfacesNode()
{
static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2);
TileSpec tiles[6];
for (int face = 0; face < 6; face++)
getTile(nodebox_tile_dirs[face], &tiles[face]);
if (data->m_smooth_lighting)
getSmoothLightFrame();
drawAutoLightedCuboid(box, nullptr, tiles, 6);
}
void MapblockMeshGenerator::drawNodeboxNode() void MapblockMeshGenerator::drawNodeboxNode()
{ {
TileSpec tiles[6]; TileSpec tiles[6];

View file

@ -43,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/touchcontrols.h" #include "gui/touchcontrols.h"
#include "itemdef.h" #include "itemdef.h"
#include "log.h" #include "log.h"
#include "log_internal.h"
#include "filesys.h" #include "filesys.h"
#include "gameparams.h" #include "gameparams.h"
#include "gettext.h" #include "gettext.h"
@ -413,16 +414,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
float m_user_exposure_compensation; float m_user_exposure_compensation;
bool m_bloom_enabled; bool m_bloom_enabled;
CachedPixelShaderSetting<float> m_bloom_intensity_pixel{"bloomIntensity"}; CachedPixelShaderSetting<float> m_bloom_intensity_pixel{"bloomIntensity"};
float m_bloom_intensity;
CachedPixelShaderSetting<float> m_bloom_strength_pixel{"bloomStrength"}; CachedPixelShaderSetting<float> m_bloom_strength_pixel{"bloomStrength"};
float m_bloom_strength;
CachedPixelShaderSetting<float> m_bloom_radius_pixel{"bloomRadius"}; CachedPixelShaderSetting<float> m_bloom_radius_pixel{"bloomRadius"};
float m_bloom_radius;
CachedPixelShaderSetting<float> m_cloud_height_pixel{"cloudHeight"};
CachedPixelShaderSetting<float> m_cloud_thickness_pixel{"cloudThickness"};
CachedPixelShaderSetting<float> m_cloud_density_pixel{"cloudDensity"};
CachedPixelShaderSetting<float, 2> m_cloud_offset_pixel{"cloudOffset"};
CachedPixelShaderSetting<float> m_cloud_radius_pixel{"cloudRadius"};
CachedPixelShaderSetting<float> m_saturation_pixel{"saturation"}; CachedPixelShaderSetting<float> m_saturation_pixel{"saturation"};
float m_gamma; float m_gamma;
CachedPixelShaderSetting<float> m_gamma_pixel{"gamma"}; CachedPixelShaderSetting<float> m_gamma_pixel{"gamma"};
@ -436,12 +429,8 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter
CachedPixelShaderSetting<float> CachedPixelShaderSetting<float>
m_volumetric_light_strength_pixel{"volumetricLightStrength"}; m_volumetric_light_strength_pixel{"volumetricLightStrength"};
static constexpr std::array<const char*, 5> SETTING_CALLBACKS = { static constexpr std::array<const char*, 1> SETTING_CALLBACKS = {
"exposure_compensation", "exposure_compensation",
"bloom_intensity",
"bloom_strength_factor",
"bloom_radius",
"gamma"
}; };
public: public:
@ -449,14 +438,6 @@ public:
{ {
if (name == "exposure_compensation") if (name == "exposure_compensation")
m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
if (name == "bloom_intensity")
m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
if (name == "bloom_strength_factor")
m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
if (name == "bloom_radius")
m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
if (name == "gamma")
m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f);
} }
static void settingsCallback(const std::string &name, void *userdata) static void settingsCallback(const std::string &name, void *userdata)
@ -475,10 +456,6 @@ public:
m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f); m_user_exposure_compensation = g_settings->getFloat("exposure_compensation", -1.0f, 1.0f);
m_bloom_enabled = g_settings->getBool("enable_bloom"); m_bloom_enabled = g_settings->getBool("enable_bloom");
m_bloom_intensity = g_settings->getFloat("bloom_intensity", 0.01f, 1.0f);
m_bloom_strength = RenderingEngine::BASE_BLOOM_STRENGTH * g_settings->getFloat("bloom_strength_factor", 0.1f, 10.0f);
m_bloom_radius = g_settings->getFloat("bloom_radius", 0.1f, 8.0f);
m_gamma = g_settings->getFloat("gamma", 1.0f, 5.0f);
m_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled; m_volumetric_light_enabled = g_settings->getBool("enable_volumetric_lighting") && m_bloom_enabled;
} }
@ -547,7 +524,9 @@ public:
m_texel_size0_vertex.set(m_texel_size0, services); m_texel_size0_vertex.set(m_texel_size0, services);
m_texel_size0_pixel.set(m_texel_size0, services); m_texel_size0_pixel.set(m_texel_size0, services);
const AutoExposure &exposure_params = m_client->getEnv().getLocalPlayer()->getLighting().exposure; const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting();
const AutoExposure &exposure_params = lighting.exposure;
std::array<float, 7> exposure_buffer = { std::array<float, 7> exposure_buffer = {
std::pow(2.0f, exposure_params.luminance_min), std::pow(2.0f, exposure_params.luminance_min),
std::pow(2.0f, exposure_params.luminance_max), std::pow(2.0f, exposure_params.luminance_max),
@ -560,14 +539,14 @@ public:
m_exposure_params_pixel.set(exposure_buffer.data(), services); m_exposure_params_pixel.set(exposure_buffer.data(), services);
if (m_bloom_enabled) { if (m_bloom_enabled) {
m_bloom_intensity_pixel.set(&m_bloom_intensity, services); float intensity = std::max(lighting.bloom_intensity, 0.0f);
m_bloom_radius_pixel.set(&m_bloom_radius, services); m_bloom_intensity_pixel.set(&intensity, services);
m_bloom_strength_pixel.set(&m_bloom_strength, services); float strength_factor = std::max(lighting.bloom_strength_factor, 0.0f);
m_bloom_strength_pixel.set(&strength_factor, services);
float radius = std::max(lighting.bloom_radius, 0.0f);
m_bloom_radius_pixel.set(&radius, services);
} }
m_gamma_pixel.set(&m_gamma, services);
const auto &lighting = m_client->getEnv().getLocalPlayer()->getLighting();
float saturation = lighting.saturation; float saturation = lighting.saturation;
m_saturation_pixel.set(&saturation, services); m_saturation_pixel.set(&saturation, services);
video::SColorf artificial_light = lighting.artificial_light_color; video::SColorf artificial_light = lighting.artificial_light_color;
@ -773,6 +752,7 @@ protected:
void processUserInput(f32 dtime); void processUserInput(f32 dtime);
void processKeyInput(); void processKeyInput();
void processItemSelection(u16 *new_playeritem); void processItemSelection(u16 *new_playeritem);
bool shouldShowTouchControls();
void dropSelectedItem(bool single_item = false); void dropSelectedItem(bool single_item = false);
void openInventory(); void openInventory();
@ -1615,6 +1595,14 @@ bool Game::createClient(const GameStartData &start_data)
return true; return true;
} }
bool Game::shouldShowTouchControls()
{
const std::string &touch_controls = g_settings->get("touch_controls");
if (touch_controls == "auto")
return RenderingEngine::getLastPointerType() == PointerType::Touch;
return is_yes(touch_controls);
}
bool Game::initGui() bool Game::initGui()
{ {
m_game_ui->init(); m_game_ui->init();
@ -1629,7 +1617,7 @@ bool Game::initGui()
gui_chat_console = make_irr<GUIChatConsole>(guienv, guienv->getRootGUIElement(), gui_chat_console = make_irr<GUIChatConsole>(guienv, guienv->getRootGUIElement(),
-1, chat_backend, client, &g_menumgr); -1, chat_backend, client, &g_menumgr);
if (g_settings->getBool("touch_controls")) { if (shouldShowTouchControls()) {
g_touchcontrols = new TouchControls(device, texture_src); g_touchcontrols = new TouchControls(device, texture_src);
g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled()); g_touchcontrols->setUseCrosshair(!isTouchCrosshairDisabled());
} }
@ -2081,6 +2069,15 @@ void Game::updateStats(RunStats *stats, const FpsControl &draw_times,
void Game::processUserInput(f32 dtime) void Game::processUserInput(f32 dtime)
{ {
bool desired = shouldShowTouchControls();
if (desired && !g_touchcontrols) {
g_touchcontrols = new TouchControls(device, texture_src);
} else if (!desired && g_touchcontrols) {
delete g_touchcontrols;
g_touchcontrols = nullptr;
}
// Reset input if window not active or some menu is active // Reset input if window not active or some menu is active
if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console.get())) { if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console.get())) {
if (m_game_focused) { if (m_game_focused) {
@ -2711,7 +2708,7 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
cur_control->setVisible(false); cur_control->setVisible(false);
} }
if (m_first_loop_after_window_activation) { if (m_first_loop_after_window_activation && !g_touchcontrols) {
m_first_loop_after_window_activation = false; m_first_loop_after_window_activation = false;
input->setMousePos(driver->getScreenSize().Width / 2, input->setMousePos(driver->getScreenSize().Width / 2,
@ -2727,6 +2724,8 @@ void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
m_first_loop_after_window_activation = true; m_first_loop_after_window_activation = true;
} }
if (g_touchcontrols)
m_first_loop_after_window_activation = true;
} }
// Get the factor to multiply with sensitivity to get the same mouse/joystick // Get the factor to multiply with sensitivity to get the same mouse/joystick
@ -2792,9 +2791,10 @@ void Game::updatePlayerControl(const CameraOrientation &cam)
isKeyDown(KeyType::PLACE), isKeyDown(KeyType::PLACE),
cam.camera_pitch, cam.camera_pitch,
cam.camera_yaw, cam.camera_yaw,
input->getMovementSpeed(), input->getJoystickSpeed(),
input->getMovementDirection() input->getJoystickDirection()
); );
control.setMovementFromKeys();
// autoforward if set: move at maximum speed // autoforward if set: move at maximum speed
if (player->getPlayerSettings().continuous_forward && if (player->getPlayerSettings().continuous_forward &&

View file

@ -536,9 +536,9 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
return; // Avoid zero divides return; // Avoid zero divides
// Angle according to camera view // Angle according to camera view
v3f fore(0.f, 0.f, 1.f);
scene::ICameraSceneNode *cam = client->getSceneManager()->getActiveCamera(); scene::ICameraSceneNode *cam = client->getSceneManager()->getActiveCamera();
cam->getAbsoluteTransformation().rotateVect(fore); v3f fore = cam->getAbsoluteTransformation()
.rotateAndScaleVect(v3f(0.f, 0.f, 1.f));
int angle = - fore.getHorizontalAngle().Y; int angle = - fore.getHorizontalAngle().Y;
// Limit angle and ajust with given offset // Limit angle and ajust with given offset

View file

@ -1447,6 +1447,8 @@ bool ImageSource::generateImagePart(std::string_view part_of_name,
video::IImage *img = generateImage(filename, source_image_names); video::IImage *img = generateImage(filename, source_image_names);
if (img) { if (img) {
upscaleImagesToMatchLargest(baseimg, img);
apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0), apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0),
img->getDimension()); img->getDimension());
img->drop(); img->drop();

View file

@ -24,6 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/mainmenumanager.h" #include "gui/mainmenumanager.h"
#include "gui/touchcontrols.h" #include "gui/touchcontrols.h"
#include "hud.h" #include "hud.h"
#include "log_internal.h"
#include "client/renderingengine.h"
void KeyCache::populate_nonchanging() void KeyCache::populate_nonchanging()
{ {
@ -141,6 +143,11 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
} }
} }
if (event.EventType == EET_MOUSE_INPUT_EVENT && !event.MouseInput.Simulated)
last_pointer_type = PointerType::Mouse;
else if (event.EventType == EET_TOUCH_INPUT_EVENT)
last_pointer_type = PointerType::Touch;
// Let the menu handle events, if one is active. // Let the menu handle events, if one is active.
if (isMenuActive()) { if (isMenuActive()) {
if (g_touchcontrols) if (g_touchcontrols)
@ -220,51 +227,42 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
/* /*
* RealInputHandler * RealInputHandler
*/ */
float RealInputHandler::getMovementSpeed() float RealInputHandler::getJoystickSpeed()
{ {
bool f = m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD]), if (g_touchcontrols && g_touchcontrols->getJoystickSpeed())
b = m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD]), return g_touchcontrols->getJoystickSpeed();
l = m_receiver->IsKeyDown(keycache.key[KeyType::LEFT]),
r = m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT]);
if (f || b || l || r)
{
// if contradictory keys pressed, stay still
if (f && b && l && r)
return 0.0f;
else if (f && b && !l && !r)
return 0.0f;
else if (!f && !b && l && r)
return 0.0f;
return 1.0f; // If there is a keyboard event, assume maximum speed
}
if (g_touchcontrols && g_touchcontrols->getMovementSpeed())
return g_touchcontrols->getMovementSpeed();
return joystick.getMovementSpeed(); return joystick.getMovementSpeed();
} }
float RealInputHandler::getMovementDirection() float RealInputHandler::getJoystickDirection()
{ {
float x = 0, z = 0; // `getJoystickDirection() == 0` means forward, so we cannot use
// `getJoystickDirection()` as a condition.
/* Check keyboard for input */ if (g_touchcontrols && g_touchcontrols->getJoystickSpeed())
if (m_receiver->IsKeyDown(keycache.key[KeyType::FORWARD])) return g_touchcontrols->getJoystickDirection();
z += 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::BACKWARD]))
z -= 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::RIGHT]))
x += 1;
if (m_receiver->IsKeyDown(keycache.key[KeyType::LEFT]))
x -= 1;
if (x != 0 || z != 0) /* If there is a keyboard event, it takes priority */
return std::atan2(x, z);
// `getMovementDirection() == 0` means forward, so we cannot use
// `getMovementDirection()` as a condition.
else if (g_touchcontrols && g_touchcontrols->getMovementSpeed())
return g_touchcontrols->getMovementDirection();
return joystick.getMovementDirection(); return joystick.getMovementDirection();
} }
v2s32 RealInputHandler::getMousePos()
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
return control->getPosition();
}
return m_mousepos;
}
void RealInputHandler::setMousePos(s32 x, s32 y)
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
control->setPosition(x, y);
} else {
m_mousepos = v2s32(x, y);
}
}
/* /*
* RandomInputHandler * RandomInputHandler
*/ */
@ -320,25 +318,11 @@ void RandomInputHandler::step(float dtime)
counterMovement -= dtime; counterMovement -= dtime;
if (counterMovement < 0.0) { if (counterMovement < 0.0) {
counterMovement = 0.1 * Rand(1, 40); counterMovement = 0.1 * Rand(1, 40);
movementSpeed = Rand(0,100)*0.01; joystickSpeed = Rand(0,100)*0.01;
movementDirection = Rand(-100, 100)*0.01 * M_PI; joystickDirection = Rand(-100, 100)*0.01 * M_PI;
} }
} else { } else {
bool f = keydown[keycache.key[KeyType::FORWARD]], joystickSpeed = 0.0f;
l = keydown[keycache.key[KeyType::LEFT]]; joystickDirection = 0.0f;
if (f || l) {
movementSpeed = 1.0f;
if (f && !l)
movementDirection = 0.0;
else if (!f && l)
movementDirection = -M_PI_2;
else if (f && l)
movementDirection = -M_PI_4;
else
movementDirection = 0.0;
} else {
movementSpeed = 0.0;
movementDirection = 0.0;
}
} }
} }

View file

@ -23,10 +23,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "joystick_controller.h" #include "joystick_controller.h"
#include <list> #include <list>
#include "keycode.h" #include "keycode.h"
#include "renderingengine.h"
class InputHandler; class InputHandler;
enum class PointerType {
Mouse,
Touch,
};
/**************************************************************************** /****************************************************************************
Fast key cache for main game loop Fast key cache for main game loop
****************************************************************************/ ****************************************************************************/
@ -199,6 +203,8 @@ public:
JoystickController *joystick = nullptr; JoystickController *joystick = nullptr;
PointerType getLastPointerType() { return last_pointer_type; }
private: private:
s32 mouse_wheel = 0; s32 mouse_wheel = 0;
@ -223,6 +229,8 @@ private:
// Intentionally not reset by clearInput/releaseAllKeys. // Intentionally not reset by clearInput/releaseAllKeys.
bool fullscreen_is_down = false; bool fullscreen_is_down = false;
PointerType last_pointer_type = PointerType::Mouse;
}; };
class InputHandler class InputHandler
@ -247,8 +255,8 @@ public:
virtual bool wasKeyReleased(GameKeyType k) = 0; virtual bool wasKeyReleased(GameKeyType k) = 0;
virtual bool cancelPressed() = 0; virtual bool cancelPressed() = 0;
virtual float getMovementSpeed() = 0; virtual float getJoystickSpeed() = 0;
virtual float getMovementDirection() = 0; virtual float getJoystickDirection() = 0;
virtual void clearWasKeyPressed() {} virtual void clearWasKeyPressed() {}
virtual void clearWasKeyReleased() {} virtual void clearWasKeyReleased() {}
@ -304,9 +312,9 @@ public:
return m_receiver->WasKeyReleased(keycache.key[k]) || joystick.wasKeyReleased(k); return m_receiver->WasKeyReleased(keycache.key[k]) || joystick.wasKeyReleased(k);
} }
virtual float getMovementSpeed(); virtual float getJoystickSpeed();
virtual float getMovementDirection(); virtual float getJoystickDirection();
virtual bool cancelPressed() virtual bool cancelPressed()
{ {
@ -331,25 +339,8 @@ public:
m_receiver->dontListenForKeys(); m_receiver->dontListenForKeys();
} }
virtual v2s32 getMousePos() virtual v2s32 getMousePos();
{ virtual void setMousePos(s32 x, s32 y);
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
return control->getPosition();
}
return m_mousepos;
}
virtual void setMousePos(s32 x, s32 y)
{
auto control = RenderingEngine::get_raw_device()->getCursorControl();
if (control) {
control->setPosition(x, y);
} else {
m_mousepos = v2s32(x, y);
}
}
virtual s32 getMouseWheel() virtual s32 getMouseWheel()
{ {
@ -388,8 +379,8 @@ public:
virtual bool wasKeyPressed(GameKeyType k) { return false; } virtual bool wasKeyPressed(GameKeyType k) { return false; }
virtual bool wasKeyReleased(GameKeyType k) { return false; } virtual bool wasKeyReleased(GameKeyType k) { return false; }
virtual bool cancelPressed() { return false; } virtual bool cancelPressed() { return false; }
virtual float getMovementSpeed() { return movementSpeed; } virtual float getJoystickSpeed() { return joystickSpeed; }
virtual float getMovementDirection() { return movementDirection; } virtual float getJoystickDirection() { return joystickDirection; }
virtual v2s32 getMousePos() { return mousepos; } virtual v2s32 getMousePos() { return mousepos; }
virtual void setMousePos(s32 x, s32 y) { mousepos = v2s32(x, y); } virtual void setMousePos(s32 x, s32 y) { mousepos = v2s32(x, y); }
@ -403,6 +394,6 @@ private:
KeyList keydown; KeyList keydown;
v2s32 mousepos; v2s32 mousepos;
v2s32 mousespeed; v2s32 mousespeed;
float movementSpeed; float joystickSpeed;
float movementDirection; float joystickDirection;
}; };

View file

@ -105,6 +105,8 @@ public:
u8 last_camera_fov = 0; u8 last_camera_fov = 0;
u8 last_wanted_range = 0; u8 last_wanted_range = 0;
bool last_camera_inverted = false; bool last_camera_inverted = false;
f32 last_movement_speed = 0.0f;
f32 last_movement_dir = 0.0f;
float camera_impact = 0.0f; float camera_impact = 0.0f;

View file

@ -357,16 +357,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
if (attached_absolute_pos_rot_matrix) { if (attached_absolute_pos_rot_matrix) {
// Apply attachment rotation // Apply attachment rotation
attached_absolute_pos_rot_matrix->rotateVect(pp.vel); pp.vel = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.vel);
attached_absolute_pos_rot_matrix->rotateVect(pp.acc); pp.acc = attached_absolute_pos_rot_matrix->rotateAndScaleVect(pp.acc);
} }
if (attractor_obj) if (attractor_obj)
attractor_origin += attractor_obj->getPosition() / BS; attractor_origin += attractor_obj->getPosition() / BS;
if (attractor_direction_obj) { if (attractor_direction_obj) {
auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix(); auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix();
if (attractor_absolute_pos_rot_matrix) if (attractor_absolute_pos_rot_matrix) {
attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction); attractor_direction = attractor_absolute_pos_rot_matrix
->rotateAndScaleVect(attractor_direction);
}
} }
pp.expirationtime = r_exp.pickWithin(); pp.expirationtime = r_exp.pickWithin();

View file

@ -41,7 +41,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
RenderingEngine *RenderingEngine::s_singleton = nullptr; RenderingEngine *RenderingEngine::s_singleton = nullptr;
const video::SColor RenderingEngine::MENU_SKY_COLOR = video::SColor(255, 140, 186, 250); const video::SColor RenderingEngine::MENU_SKY_COLOR = video::SColor(255, 140, 186, 250);
const float RenderingEngine::BASE_BLOOM_STRENGTH = 1.0f;
/* Helper classes */ /* Helper classes */
@ -173,7 +172,7 @@ static irr::IrrlichtDevice *createDevice(SIrrlichtCreationParameters params, std
/* RenderingEngine class */ /* RenderingEngine class */
RenderingEngine::RenderingEngine(IEventReceiver *receiver) RenderingEngine::RenderingEngine(MyEventReceiver *receiver)
{ {
sanity_check(!s_singleton); sanity_check(!s_singleton);
@ -226,6 +225,8 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver)
// This changes the minimum allowed number of vertices in a VBO. Default is 500. // This changes the minimum allowed number of vertices in a VBO. Default is 500.
driver->setMinHardwareBufferVertexCount(4); driver->setMinHardwareBufferVertexCount(4);
m_receiver = receiver;
s_singleton = this; s_singleton = this;
g_settings->registerChangedCallback("fullscreen", settingChangedCallback, this); g_settings->registerChangedCallback("fullscreen", settingChangedCallback, this);

View file

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <string> #include <string>
#include "client/inputhandler.h"
#include "irrlichttypes_extrabloated.h" #include "irrlichttypes_extrabloated.h"
#include "debug.h" #include "debug.h"
#include "client/shader.h" #include "client/shader.h"
@ -81,9 +82,8 @@ class RenderingEngine
{ {
public: public:
static const video::SColor MENU_SKY_COLOR; static const video::SColor MENU_SKY_COLOR;
static const float BASE_BLOOM_STRENGTH;
RenderingEngine(IEventReceiver *eventReceiver); RenderingEngine(MyEventReceiver *eventReceiver);
~RenderingEngine(); ~RenderingEngine();
void setResizable(bool resize); void setResizable(bool resize);
@ -168,6 +168,12 @@ public:
const irr::core::dimension2d<u32> initial_screen_size, const irr::core::dimension2d<u32> initial_screen_size,
const bool initial_window_maximized); const bool initial_window_maximized);
static PointerType getLastPointerType()
{
sanity_check(s_singleton && s_singleton->m_receiver);
return s_singleton->m_receiver->getLastPointerType();
}
private: private:
static void settingChangedCallback(const std::string &name, void *data); static void settingChangedCallback(const std::string &name, void *data);
v2u32 _getWindowSize() const; v2u32 _getWindowSize() const;
@ -175,5 +181,6 @@ private:
std::unique_ptr<RenderingCore> core; std::unique_ptr<RenderingCore> core;
irr::IrrlichtDevice *m_device = nullptr; irr::IrrlichtDevice *m_device = nullptr;
irr::video::IVideoDriver *driver; irr::video::IVideoDriver *driver;
MyEventReceiver *m_receiver = nullptr;
static RenderingEngine *s_singleton; static RenderingEngine *s_singleton;
}; };

View file

@ -322,6 +322,9 @@ public:
private: private:
// Are shaders even enabled?
bool m_enabled;
// The id of the thread that is allowed to use irrlicht directly // The id of the thread that is allowed to use irrlicht directly
std::thread::id m_main_thread; std::thread::id m_main_thread;
@ -360,6 +363,12 @@ ShaderSource::ShaderSource()
// Add a dummy ShaderInfo as the first index, named "" // Add a dummy ShaderInfo as the first index, named ""
m_shaderinfo_cache.emplace_back(); m_shaderinfo_cache.emplace_back();
m_enabled = g_settings->getBool("enable_shaders");
if (!m_enabled) {
warningstream << "You are running " PROJECT_NAME_C " with shaders disabled, "
"this is not a recommended configuration." << std::endl;
}
// Add main global constant setter // Add main global constant setter
addShaderConstantSetterFactory(new MainShaderConstantSetterFactory()); addShaderConstantSetterFactory(new MainShaderConstantSetterFactory());
} }
@ -368,9 +377,11 @@ ShaderSource::~ShaderSource()
{ {
MutexAutoLock lock(m_shaderinfo_cache_mutex); MutexAutoLock lock(m_shaderinfo_cache_mutex);
if (!m_enabled)
return;
// Delete materials // Delete materials
video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()-> auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices();
getGPUProgrammingServices();
for (ShaderInfo &i : m_shaderinfo_cache) { for (ShaderInfo &i : m_shaderinfo_cache) {
if (!i.name.empty()) if (!i.name.empty())
gpu->deleteShaderMaterial(i.material); gpu->deleteShaderMaterial(i.material);
@ -499,9 +510,11 @@ void ShaderSource::rebuildShaders()
{ {
MutexAutoLock lock(m_shaderinfo_cache_mutex); MutexAutoLock lock(m_shaderinfo_cache_mutex);
if (!m_enabled)
return;
// Delete materials // Delete materials
video::IGPUProgrammingServices *gpu = RenderingEngine::get_video_driver()-> auto *gpu = RenderingEngine::get_video_driver()->getGPUProgrammingServices();
getGPUProgrammingServices();
for (ShaderInfo &i : m_shaderinfo_cache) { for (ShaderInfo &i : m_shaderinfo_cache) {
if (!i.name.empty()) { if (!i.name.empty()) {
gpu->deleteShaderMaterial(i.material); gpu->deleteShaderMaterial(i.material);
@ -548,12 +561,11 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
} }
shaderinfo.material = shaderinfo.base_material; shaderinfo.material = shaderinfo.base_material;
bool enable_shaders = g_settings->getBool("enable_shaders"); if (!m_enabled)
if (!enable_shaders)
return shaderinfo; return shaderinfo;
video::IVideoDriver *driver = RenderingEngine::get_video_driver(); video::IVideoDriver *driver = RenderingEngine::get_video_driver();
video::IGPUProgrammingServices *gpu = driver->getGPUProgrammingServices(); auto *gpu = driver->getGPUProgrammingServices();
if (!driver->queryFeature(video::EVDF_ARB_GLSL) || !gpu) { if (!driver->queryFeature(video::EVDF_ARB_GLSL) || !gpu) {
throw ShaderException(gettext("Shaders are enabled but GLSL is not " throw ShaderException(gettext("Shaders are enabled but GLSL is not "
"supported by the driver.")); "supported by the driver."));
@ -561,7 +573,7 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
// Create shaders header // Create shaders header
bool fully_programmable = driver->getDriverType() == video::EDT_OGLES2 || driver->getDriverType() == video::EDT_OPENGL3; bool fully_programmable = driver->getDriverType() == video::EDT_OGLES2 || driver->getDriverType() == video::EDT_OPENGL3;
std::stringstream shaders_header; std::ostringstream shaders_header;
shaders_header shaders_header
<< std::noboolalpha << std::noboolalpha
<< std::showpoint // for GLSL ES << std::showpoint // for GLSL ES
@ -588,10 +600,14 @@ ShaderInfo ShaderSource::generateShader(const std::string &name,
attribute mediump vec4 inVertexTangent; attribute mediump vec4 inVertexTangent;
attribute mediump vec4 inVertexBinormal; attribute mediump vec4 inVertexBinormal;
)"; )";
// Our vertex color has components reversed compared to what OpenGL
// normally expects, so we need to take that into account.
vertex_header += "#define inVertexColor (inVertexColor.bgra)\n";
fragment_header = R"( fragment_header = R"(
precision mediump float; precision mediump float;
)"; )";
} else { } else {
/* legacy OpenGL driver */
shaders_header << R"( shaders_header << R"(
#version 120 #version 120
#define lowp #define lowp

View file

@ -137,8 +137,8 @@ void DirectionalLight::update_frustum(const Camera *cam, Client *client, bool fo
// when camera offset changes, adjust the current frustum view matrix to avoid flicker // when camera offset changes, adjust the current frustum view matrix to avoid flicker
v3s16 cam_offset = cam->getOffset(); v3s16 cam_offset = cam->getOffset();
if (cam_offset != shadow_frustum.camera_offset) { if (cam_offset != shadow_frustum.camera_offset) {
v3f rotated_offset; v3f rotated_offset = shadow_frustum.ViewMat.rotateAndScaleVect(
shadow_frustum.ViewMat.rotateVect(rotated_offset, intToFloat(cam_offset - shadow_frustum.camera_offset, BS)); intToFloat(cam_offset - shadow_frustum.camera_offset, BS));
shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset); shadow_frustum.ViewMat.setTranslation(shadow_frustum.ViewMat.getTranslation() + rotated_offset);
shadow_frustum.player += intToFloat(shadow_frustum.camera_offset - cam->getOffset(), BS); shadow_frustum.player += intToFloat(shadow_frustum.camera_offset - cam->getOffset(), BS);
shadow_frustum.camera_offset = cam_offset; shadow_frustum.camera_offset = cam_offset;

View file

@ -838,14 +838,10 @@ void Sky::updateStars()
); );
core::CMatrix4<f32> a; core::CMatrix4<f32> a;
a.buildRotateFromTo(v3f(0, 1, 0), r); a.buildRotateFromTo(v3f(0, 1, 0), r);
v3f p = v3f(-d, 1, -d); v3f p = a.rotateAndScaleVect(v3f(-d, 1, -d));
v3f p1 = v3f(d, 1, -d); v3f p1 = a.rotateAndScaleVect(v3f(d, 1, -d));
v3f p2 = v3f(d, 1, d); v3f p2 = a.rotateAndScaleVect(v3f(d, 1, d));
v3f p3 = v3f(-d, 1, d); v3f p3 = a.rotateAndScaleVect(v3f(-d, 1, d));
a.rotateVect(p);
a.rotateVect(p1);
a.rotateVect(p2);
a.rotateVect(p3);
vertices.push_back(video::S3DVertex(p, {}, {}, {})); vertices.push_back(video::S3DVertex(p, {}, {}, {}));
vertices.push_back(video::S3DVertex(p1, {}, {}, {})); vertices.push_back(video::S3DVertex(p1, {}, {}, {}));
vertices.push_back(video::S3DVertex(p2, {}, {}, {})); vertices.push_back(video::S3DVertex(p2, {}, {}, {}));

View file

@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <cassert> #include <cassert>
#include <cstring> // memcpy #include <cstring> // memcpy
#include <memory>
namespace sound { namespace sound {

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once #pragma once
#include <memory>
#include "al_helpers.h" #include "al_helpers.h"
namespace sound { namespace sound {

View file

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <sstream> #include <sstream>
#include <unordered_set> #include <unordered_set>
#include <algorithm> #include <algorithm>
#include <queue>
#include "gamedef.h" #include "gamedef.h"
#include "inventory.h" #include "inventory.h"
#include "util/serialize.h" #include "util/serialize.h"

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