From b155e999212cd91e304208e3d8744a8a31a345d6 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Fri, 29 Nov 2024 23:53:27 -0600 Subject: [PATCH 01/25] Moved script. Changed name. --- scripts/defaultScripts.js | 2 +- .../armored-chat => system/domainChat}/README.md | 12 ++++++------ .../domainChat/domainChat.js} | 6 +++--- .../domainChat/domainChat.qml} | 0 .../domainChat/domainChatQuick.qml} | 0 .../domainChat}/img/icon_black.png | Bin .../domainChat}/img/icon_white.png | Bin .../domainChat}/img/ui/send.svg | 0 .../domainChat}/img/ui/send_black.png | Bin .../domainChat}/img/ui/send_white.png | Bin .../domainChat}/img/ui/settings_black.png | Bin .../domainChat}/img/ui/settings_white.png | Bin .../domainChat}/img/ui/social_black.png | Bin .../domainChat}/img/ui/social_white.png | Bin .../domainChat}/img/ui/world_black.png | Bin .../domainChat}/img/ui/world_white.png | Bin 16 files changed, 10 insertions(+), 10 deletions(-) rename scripts/{communityScripts/armored-chat => system/domainChat}/README.md (93%) rename scripts/{communityScripts/armored-chat/armored_chat.js => system/domainChat/domainChat.js} (98%) rename scripts/{communityScripts/armored-chat/armored_chat.qml => system/domainChat/domainChat.qml} (100%) rename scripts/{communityScripts/armored-chat/armored_chat_quick_message.qml => system/domainChat/domainChatQuick.qml} (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/icon_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send.svg (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/send_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/settings_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/social_white.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_black.png (100%) rename scripts/{communityScripts/armored-chat => system/domainChat}/img/ui/world_white.png (100%) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 31afd6e2db..d185daadb5 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -46,7 +46,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [ "communityScripts/notificationCore/notificationCore.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, - "communityScripts/armored-chat/armored_chat.js", + "system/domainChat/domainChat.js", //"system/chat.js" ]; diff --git a/scripts/communityScripts/armored-chat/README.md b/scripts/system/domainChat/README.md similarity index 93% rename from scripts/communityScripts/armored-chat/README.md rename to scripts/system/domainChat/README.md index 2385494676..8ed2e8d911 100644 --- a/scripts/communityScripts/armored-chat/README.md +++ b/scripts/system/domainChat/README.md @@ -1,15 +1,15 @@ -# Armored Chat +# Domain Chat -1. What is Armored Chat +1. What is Domain Chat 2. User manual - Installation - Settings - Usability tips 3. Development -## What is Armored Chat +## What is Domain Chat -Armored Chat is a chat application strictly made to communicate between players in the same domain. It is made using QML and to be as light weight as reasonably possible. +Domain Chat is a chat application strictly made to communicate between players in the same domain. It is made using QML and to be as light weight as reasonably possible. ### Dependencies @@ -21,7 +21,7 @@ For notifications, AC uses [notificationCore.js](https://github.com/overte-org/o ### Installation -Armored Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js). +Domain Chat is preinstalled courtesy of [defaultScripts.js](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js). If AC is not preinstalled, or for some other reason it can not be automatically installed, you can install it manually by following [these instructions](https://github.com/overte-org/overte/blob/8661e8a858663b48e8485c2cd7120dc3e2d7b87e/scripts/defaultScripts.js) to open your script management application, and loading the script url: @@ -33,7 +33,7 @@ https://raw.githubusercontent.com/overte-org/overte/master/scripts/communityScri ### Settings -Armored Chat comes with basic settings for managing itself. +Domain Chat comes with basic settings for managing itself. #### External window diff --git a/scripts/communityScripts/armored-chat/armored_chat.js b/scripts/system/domainChat/domainChat.js similarity index 98% rename from scripts/communityScripts/armored-chat/armored_chat.js rename to scripts/system/domainChat/domainChat.js index 779dc3ff54..a7836859ce 100644 --- a/scripts/communityScripts/armored-chat/armored_chat.js +++ b/scripts/system/domainChat/domainChat.js @@ -1,5 +1,5 @@ // -// armored_chat.js +// domainChat.js // // Created by Armored Dragon, 2024. // Copyright 2024 Overte e.V. @@ -61,7 +61,7 @@ appButton.clicked.connect(toggleMainChatWindow); quickMessage = new OverlayWindow({ - source: Script.resolvePath("./armored_chat_quick_message.qml"), + source: Script.resolvePath("./domainChatQuick.qml"), }); _openWindow(); @@ -78,7 +78,7 @@ } function _openWindow() { chatOverlayWindow = new Desktop.createWindow( - Script.resolvePath("./armored_chat.qml"), + Script.resolvePath("./domainChat.qml"), { title: "Chat", size: { x: 550, y: 400 }, diff --git a/scripts/communityScripts/armored-chat/armored_chat.qml b/scripts/system/domainChat/domainChat.qml similarity index 100% rename from scripts/communityScripts/armored-chat/armored_chat.qml rename to scripts/system/domainChat/domainChat.qml diff --git a/scripts/communityScripts/armored-chat/armored_chat_quick_message.qml b/scripts/system/domainChat/domainChatQuick.qml similarity index 100% rename from scripts/communityScripts/armored-chat/armored_chat_quick_message.qml rename to scripts/system/domainChat/domainChatQuick.qml diff --git a/scripts/communityScripts/armored-chat/img/icon_black.png b/scripts/system/domainChat/img/icon_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/icon_black.png rename to scripts/system/domainChat/img/icon_black.png diff --git a/scripts/communityScripts/armored-chat/img/icon_white.png b/scripts/system/domainChat/img/icon_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/icon_white.png rename to scripts/system/domainChat/img/icon_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/send.svg b/scripts/system/domainChat/img/ui/send.svg similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send.svg rename to scripts/system/domainChat/img/ui/send.svg diff --git a/scripts/communityScripts/armored-chat/img/ui/send_black.png b/scripts/system/domainChat/img/ui/send_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send_black.png rename to scripts/system/domainChat/img/ui/send_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/send_white.png b/scripts/system/domainChat/img/ui/send_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/send_white.png rename to scripts/system/domainChat/img/ui/send_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_black.png b/scripts/system/domainChat/img/ui/settings_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/settings_black.png rename to scripts/system/domainChat/img/ui/settings_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/settings_white.png b/scripts/system/domainChat/img/ui/settings_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/settings_white.png rename to scripts/system/domainChat/img/ui/settings_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/social_black.png b/scripts/system/domainChat/img/ui/social_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/social_black.png rename to scripts/system/domainChat/img/ui/social_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/social_white.png b/scripts/system/domainChat/img/ui/social_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/social_white.png rename to scripts/system/domainChat/img/ui/social_white.png diff --git a/scripts/communityScripts/armored-chat/img/ui/world_black.png b/scripts/system/domainChat/img/ui/world_black.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/world_black.png rename to scripts/system/domainChat/img/ui/world_black.png diff --git a/scripts/communityScripts/armored-chat/img/ui/world_white.png b/scripts/system/domainChat/img/ui/world_white.png similarity index 100% rename from scripts/communityScripts/armored-chat/img/ui/world_white.png rename to scripts/system/domainChat/img/ui/world_white.png From 1d47275b410e73fadf4e6489891a3d6a6938f7e2 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 00:32:20 -0600 Subject: [PATCH 02/25] Removed floofchat compatibility. --- scripts/system/domainChat/domainChat.js | 36 ------------------------- 1 file changed, 36 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index a7836859ce..b00a424499 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -28,7 +28,6 @@ var palData = AvatarManager.getPalData().data; Controller.keyPressEvent.connect(keyPressEvent); - Messages.subscribe("Chat"); // Floofchat Messages.subscribe("chat"); Messages.messageReceived.connect(receivedMessage); AvatarManager.avatarAddedEvent.connect((sessionId) => { @@ -103,10 +102,7 @@ const timeArray = _formatTimestamp(currentTimestamp); if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. - if (message.forApp) return; // Floofchat - // Floofchat compatibility hook - message = floofChatCompatibilityConversion(message); message.channel = message.channel.toLowerCase(); // Check the channel. If the channel is not one we have, do nothing. @@ -215,8 +211,6 @@ action: "send_chat_message", }) ); - - floofChatCompatibilitySendMessage(message, channel); } function _avatarAction(type, sessionId) { Script.setTimeout(() => { @@ -303,34 +297,4 @@ function _emitEvent(packet = { type: "" }) { chatOverlayWindow.sendToQml(packet); } - - // - // Floofchat compatibility functions - // Added to ease the transition between Floofchat to ArmoredChat - // These functions can be safely removed at a much later date. - function floofChatCompatibilityConversion(message) { - if (message.type === "TransmitChatMessage" && !message.forApp) { - return { - position: message.position, - message: message.message, - displayName: message.displayName, - channel: message.channel.toLowerCase(), - }; - } - return message; - } - - function floofChatCompatibilitySendMessage(message, channel) { - Messages.sendMessage( - "Chat", - JSON.stringify({ - position: MyAvatar.position, - message: message, - displayName: MyAvatar.sessionDisplayName, - channel: channel.charAt(0).toUpperCase() + channel.slice(1), - type: "TransmitChatMessage", - forApp: "Floof", - }) - ); - } })(); From 777912999eba1b903dd465212f4a0553747bd119 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 01:23:49 -0600 Subject: [PATCH 03/25] Notification organization. --- scripts/system/domainChat/domainChat.js | 45 +++++++++---------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index b00a424499..c70a8f925f 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -101,33 +101,20 @@ const currentTimestamp = _getTimestamp(); const timeArray = _formatTimestamp(currentTimestamp); - if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. - - message.channel = message.channel.toLowerCase(); - - // Check the channel. If the channel is not one we have, do nothing. - if (!channels.includes(message.channel)) return; - - // If message is local, and if player is too far away from location, do nothing. - if (message.channel == "local" && isTooFar(message.position)) return; + if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. + message.channel = message.channel.toLowerCase(); // Only recognize channel names as lower case. + + if (!channels.includes(message.channel)) return; // Check the channel. If the channel is not one we have, do nothing. + if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. // Format the timestamp message.timeString = timeArray[0]; message.dateString = timeArray[1]; + + _emitEvent({ type: "show_message", ...message }); // Update qml view of to new message. + _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. - // Update qml view of to new message - _emitEvent({ type: "show_message", ...message }); - - // Show new message on screen - Messages.sendLocalMessage( - "Floof-Notif", - JSON.stringify({ - sender: message.displayName, - text: message.message, - }) - ); - - // Save message to history + // Create a new variable based on the message that will be saved. let savedMessage = message; // Remove unnecessary data. @@ -238,13 +225,7 @@ // Show new message on screen if (settings.join_notification){ - Messages.sendLocalMessage( - "Floof-Notif", - JSON.stringify({ - sender: displayName, - text: type, - }) - ); + _notificationCoreMessage(displayName, type) } _emitEvent({ type: "notification", ...message }); @@ -288,6 +269,12 @@ return timeArray; } + function _notificationCoreMessage(displayName, message){ + Messages.sendLocalMessage( + "Floof-Notif", + JSON.stringify({ sender: displayName, text: message }) + ); + } /** * Emit a packet to the HTML front end. Easy communication! From 54b4aa70b8b3dbdc6abc54a2772fa987d779f0b8 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 07:22:28 -0600 Subject: [PATCH 04/25] Message formatting. --- scripts/system/domainChat/domainChat.js | 86 ++++++++++++- scripts/system/domainChat/domainChat.qml | 157 ++++++++++++++--------- 2 files changed, 174 insertions(+), 69 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index c70a8f925f..4e62afd061 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -7,6 +7,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// TODO: Message trimming + (() => { ("use strict"); @@ -110,8 +112,12 @@ // Format the timestamp message.timeString = timeArray[0]; message.dateString = timeArray[1]; - - _emitEvent({ type: "show_message", ...message }); // Update qml view of to new message. + + let formattedMessage = _parseMessage(message.message); // Format the message for viewing + let formattedMessagePacket = { ...message }; + formattedMessagePacket.message = formattedMessage + + _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. @@ -238,9 +244,15 @@ // Load message history messageHistory.forEach((message) => { const timeArray = _formatTimestamp(_getTimestamp()); - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; - _emitEvent({ type: "show_message", ...message }); + messagePacket = { ...message }; + messagePacket.timeString = timeArray[0]; + messagePacket.dateString = timeArray[1]; + + let formattedMessage = _parseMessage(messagePacket.message); + let formattedMessagePacket = messagePacket; + formattedMessagePacket.message = formattedMessage; + + _emitEvent({ type: "show_message", ...formattedMessagePacket }); }); } @@ -275,6 +287,70 @@ JSON.stringify({ sender: displayName, text: message }) ); } + function _parseMessage(message){ + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode + const overteLocationRegex = null; + + let runningMessage = message; + let messageArray = []; + + const regexPatterns = [ + { type: "url", regex: urlRegex }, + { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode + { type: "overteLocation", regex: overteLocationRegex } + ] + + // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com + + while (true) { + let firstMatch = _findFirstMatch(); + + if (firstMatch == null) { + // If there was not any matches found in the entire message, format the whole message as a single text entry. + messageArray.push({type: 'text', value: runningMessage}); + + // Append a final 'fill width' to the message text. + messageArray.push({type: 'messageEnd'}); + break; + } + + _formatMessage(firstMatch); + } + + return messageArray; + + function _formatMessage(firstMatch){ + let indexOfFirstMatch = firstMatch[0]; + let regex = regexPatterns[firstMatch[1]].regex; + + let foundMatch = runningMessage.match(regex)[0]; + + messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); + + runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with + } + + function _findFirstMatch(){ + let indexOfFirstMatch = Infinity; + let indexOfRegexPattern = Infinity; + + for (let i = 0; regexPatterns.length > i; i++){ + let indexOfMatch = runningMessage.search(regexPatterns[i].regex); + + if (indexOfMatch == -1) continue; // No match found + + if (indexOfMatch < indexOfFirstMatch) { + indexOfFirstMatch = indexOfMatch; + indexOfRegexPattern = i; + } + } + + if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match + return null; // No found match + } + } /** * Emit a packet to the HTML front end. Easy communication! diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 07eb75c626..405e5d5ea9 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -162,7 +162,7 @@ Rectangle { model: getChannel(pageVal) delegate: Loader { property int delegateIndex: model.index - property string delegateText: model.text + property var delegateText: model.text property string delegateUsername: model.username property string delegateDate: model.date @@ -384,7 +384,7 @@ Rectangle { Rectangle { property int index: delegateIndex - property string texttest: delegateText + property var texttest: delegateText property string username: delegateUsername property string date: delegateDate @@ -410,22 +410,83 @@ Rectangle { } } - TextEdit { - anchors.top: parent.children[0].bottom - x: 5 - text: texttest - color:"white" - font.pointSize: 12 - readOnly: true - selectByMouse: true - selectByKeyboard: true + Flow { + anchors.top: parent.children[0].bottom; width: parent.width * 0.8 - height: contentHeight - wrapMode: Text.Wrap - textFormat: TextEdit.RichText + x: 5 + + Repeater { + model: texttest; + + RowLayout { + width: { + switch (model.type) { + case "text": + return children[0].width; + case "url": + return children[1].width; + } + } + + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + width: Math.min(parent.parent.parent.width, contentWidth); + + visible: model.type === 'text' || model.type === 'mention'; + + color: { + switch (model.type) { + case "mention": + return "purple"; + default: + return "white"; + } + } + } + + RowLayout { + width: Math.min(parent.parent.parent.width, children[0].contentWidth); + visible: model.type === 'url'; + + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + color: "#4EBAFD"; + font.underline: true + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openWebBrowser(model.value) + } + } + } - onLinkActivated: { - Window.openWebBrowser(link) + Text { + text: "πŸ——" + font.pointSize: 10 + wrapMode: Text.Wrap + color: "white" + + MouseArea { + anchors.fill: parent; + + onClicked: { + Qt.openUrlExternally(model.value) + } + } + } + } + + Item { + Layout.fillWidth: true + visible: model.type === 'messageEnd' + } + } } } } @@ -436,7 +497,7 @@ Rectangle { Rectangle{ property int index: delegateIndex - property string texttest: delegateText + property var texttest: delegateText property string username: delegateUsername property string date: delegateDate color: "#171717" @@ -517,8 +578,6 @@ Rectangle { channel = getChannel(channel) // Format content - message = formatContent(message); - message = embedImages(message); if (type === "notification"){ channel.append({ text: message, date: date, type: "notification" }); @@ -529,23 +588,24 @@ Rectangle { return; } - var current_time = new Date(); - var elapsed_time = current_time - last_message_time; - var elapsed_minutes = elapsed_time / (1000 * 60); + // TODO: Replace new time generation with time pregenerated from message + // var current_time = new Date(); + // var elapsed_time = current_time - last_message_time; + // var elapsed_minutes = elapsed_time / (1000 * 60); - var last_item_index = channel.count - 1; - var last_item = channel.get(last_item_index); + // var last_item_index = channel.count - 1; + // var last_item = channel.get(last_item_index); - if (last_message_user === username && elapsed_minutes < 1 && last_item){ - message = "
" + message - last_item.text = last_item.text += "\n" + message; - load_scroll_timer.running = true; - last_message_time = new Date(); - return; - } + // if (last_message_user === username && elapsed_minutes < 1 && last_item){ + // message = "
" + message + // last_item.text = last_item.text += "\n" + message; + // load_scroll_timer.running = true; + // last_message_time = new Date(); + // return; + // } - last_message_user = username; - last_message_time = new Date(); + // last_message_user = username; + // last_message_time = new Date(); channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } @@ -554,37 +614,6 @@ Rectangle { return channels[id]; } - function formatContent(mess) { - var arrow = /\ {return `` + match + ` πŸ——`}); - - var newline = /\n/gi; - mess = mess.replace(newline, "
"); - return mess - } - - function embedImages(mess){ - var image_link = /(https?:(\/){2})[\w.-]+(?:\.[\w\.-]+)+(?:\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)(?:png|jpe?g|gif|bmp|svg|webp)/g; - var matches = mess.match(image_link); - var new_message = "" - var listed = [] - var total_emeds = 0 - - new_message += mess - - for (var i = 0; matches && matches.length > i && total_emeds < 3; i++){ - if (!listed.includes(matches[i])) { - new_message += "
" - listed.push(matches[i]); - total_emeds++ - } - } - return new_message; - } - // Messages from script function fromScript(message) { From 9174ffa4d195bf59da5ad70408966764f2db5d8f Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 30 Nov 2024 09:29:08 -0600 Subject: [PATCH 05/25] World links. --- scripts/system/domainChat/domainChat.js | 4 +- scripts/system/domainChat/domainChat.qml | 71 ++++++++++++++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 4e62afd061..6d2809f782 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -117,7 +117,7 @@ let formattedMessagePacket = { ...message }; formattedMessagePacket.message = formattedMessage - _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. + _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. @@ -290,7 +290,7 @@ function _parseMessage(message){ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode - const overteLocationRegex = null; + const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; let runningMessage = message; let messageArray = []; diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 405e5d5ea9..46340d233a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -414,6 +414,7 @@ Rectangle { anchors.top: parent.children[0].bottom; width: parent.width * 0.8 x: 5 + id: messageBoxFlow Repeater { model: texttest; @@ -432,7 +433,7 @@ Rectangle { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: Math.min(parent.parent.parent.width, contentWidth); + width: Math.min(messageBoxFlow.width, contentWidth); visible: model.type === 'text' || model.type === 'mention'; @@ -447,41 +448,87 @@ Rectangle { } RowLayout { - width: Math.min(parent.parent.parent.width, children[0].contentWidth); + width: Math.min(messageBoxFlow.width, children[0].contentWidth); visible: model.type === 'url'; Text { - text: model.value || "" - font.pointSize: 12 - wrapMode: Text.Wrap + text: model.value || ""; + font.pointSize: 12; + wrapMode: Text.Wrap; color: "#4EBAFD"; - font.underline: true + font.underline: true; + width: parent.width; MouseArea { anchors.fill: parent; onClicked: { - Window.openWebBrowser(model.value) + Window.openWebBrowser(model.value); } } } Text { - text: "πŸ——" - font.pointSize: 10 - wrapMode: Text.Wrap - color: "white" + text: "πŸ——"; + font.pointSize: 10; + wrapMode: Text.Wrap; + color: "white"; MouseArea { anchors.fill: parent; onClicked: { - Qt.openUrlExternally(model.value) + Qt.openUrlExternally(model.value); } } } } + RowLayout { + visible: model.type === 'overteLocation'; + width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); + height: 20; + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Rectangle { + width: parent.width; + height: 20; + color: "lightgray" + radius: 2; + + Image { + source: "./img/ui/world_black.png" + width: 18; + height: 18; + sourceSize.width: 18 + sourceSize.height: 18 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 2 + anchors.rightMargin: 10 + } + + Text { + text: model.value.split('hifi://')[1].split('/')[0]; + color: "black" + font.pointSize: 12 + x: parent.children[0].width + 5; + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openUrl(model.value); + } + } + + } + } + + Item { Layout.fillWidth: true visible: model.type === 'messageEnd' From e470cb23b44ff45d9c713b7c0303b6d4c64b36f9 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 17 Dec 2024 18:13:49 -0600 Subject: [PATCH 06/25] Message media embedding. --- scripts/system/domainChat/domainChat.js | 60 +++++++++++++++++++----- scripts/system/domainChat/domainChat.qml | 15 +++++- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 6d2809f782..34cd128bce 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -93,7 +93,7 @@ chatOverlayWindow.fromQml.connect(fromQML); quickMessage.fromQml.connect(fromQML); } - function receivedMessage(channel, message) { + async function receivedMessage(channel, message) { // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; @@ -113,9 +113,8 @@ message.timeString = timeArray[0]; message.dateString = timeArray[1]; - let formattedMessage = _parseMessage(message.message); // Format the message for viewing let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = formattedMessage + formattedMessagePacket.message = await _parseMessage(message.message) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -237,23 +236,22 @@ _emitEvent({ type: "notification", ...message }); }, 1500); } - function _loadSettings() { + async function _loadSettings() { settings = Settings.getValue("ArmoredChat-Config", settings); if (messageHistory) { // Load message history - messageHistory.forEach((message) => { + for (message of messageHistory) { const timeArray = _formatTimestamp(_getTimestamp()); messagePacket = { ...message }; messagePacket.timeString = timeArray[0]; messagePacket.dateString = timeArray[1]; - let formattedMessage = _parseMessage(messagePacket.message); - let formattedMessagePacket = messagePacket; - formattedMessagePacket.message = formattedMessage; + let formattedMessagePacket = {...messagePacket}; + formattedMessagePacket.message = await _parseMessage(messagePacket.message); _emitEvent({ type: "show_message", ...formattedMessagePacket }); - }); + } } // Send current settings to the app @@ -287,7 +285,7 @@ JSON.stringify({ sender: displayName, text: message }) ); } - function _parseMessage(message){ + async function _parseMessage(message){ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; @@ -308,7 +306,9 @@ if (firstMatch == null) { // If there was not any matches found in the entire message, format the whole message as a single text entry. - messageArray.push({type: 'text', value: runningMessage}); + if (messageArray.length == 0) { + messageArray.push({type: 'text', value: runningMessage}); + } // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); @@ -318,6 +318,20 @@ _formatMessage(firstMatch); } + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; + + const res = await fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); + + // TODO: Add support for other media types + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + } + } + } + return messageArray; function _formatMessage(firstMatch){ @@ -360,4 +374,28 @@ function _emitEvent(packet = { type: "" }) { chatOverlayWindow.sendToQml(packet); } + + function fetch(url, options = {method: "GET"}) { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + + if (req.readyState === req.DONE) { + if (req.status === 200) { + console.log("Content type:", req.getResponseHeader("content-type")); + resolve(req); + + } else { + console.log("Error", req.status, req.statusText); + reject(); + } + } + }; + + req.open(options.method, url); + req.send(); + }); + } + })(); diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 46340d233a..f0350e972a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -510,7 +510,7 @@ Rectangle { } Text { - text: model.value.split('hifi://')[1].split('/')[0]; + text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; color: "black" font.pointSize: 12 x: parent.children[0].width + 5; @@ -533,6 +533,18 @@ Rectangle { Layout.fillWidth: true visible: model.type === 'messageEnd' } + + Item { + visible: model.type === 'imageEmbed'; + width: messageBoxFlow.width; + height: 200 + + Image { + source: model.type === 'imageEmbed' ? model.value : '' + sourceSize.width: 400 + sourceSize.height: 200 + } + } } } } @@ -653,6 +665,7 @@ Rectangle { // last_message_user = username; // last_message_time = new Date(); + channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } From 353f7ec30d8de5e03582ff2d0a2ac3dafeeaa34d Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 18 Dec 2024 05:04:23 -0600 Subject: [PATCH 07/25] Fix domain notifications. --- scripts/system/domainChat/domainChat.qml | 76 ++++++------------------ 1 file changed, 18 insertions(+), 58 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index f0350e972a..2220127f98 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -167,11 +167,8 @@ Rectangle { property string delegateDate: model.date sourceComponent: { - if (model.type === "chat") { - return template_chat_message; - } else if (model.type === "notification") { - return template_notification; - } + if (model.type === "chat") return template_chat_message; + if (model.type === "notification") return template_notification; } } @@ -384,9 +381,7 @@ Rectangle { Rectangle { property int index: delegateIndex - property var texttest: delegateText property string username: delegateUsername - property string date: delegateDate height: Math.max(65, children[1].height + 30) color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) @@ -405,7 +400,7 @@ Rectangle { Text{ anchors.right: parent.right - text: date + text: delegateDate color: "lightgray" } } @@ -417,24 +412,14 @@ Rectangle { id: messageBoxFlow Repeater { - model: texttest; + model: delegateText; RowLayout { - width: { - switch (model.type) { - case "text": - return children[0].width; - case "url": - return children[1].width; - } - } - Text { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: Math.min(messageBoxFlow.width, contentWidth); - + width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; visible: model.type === 'text' || model.type === 'mention'; color: { @@ -448,7 +433,7 @@ Rectangle { } RowLayout { - width: Math.min(messageBoxFlow.width, children[0].contentWidth); + width: children[0].contentWidth; visible: model.type === 'url'; Text { @@ -524,11 +509,9 @@ Rectangle { Window.openUrl(model.value); } } - } } - Item { Layout.fillWidth: true visible: model.type === 'messageEnd' @@ -554,11 +537,10 @@ Rectangle { Component { id: template_notification - Rectangle{ + Rectangle { property int index: delegateIndex - property var texttest: delegateText property string username: delegateUsername - property string date: delegateDate + color: "#171717" width: parent.width height: 40 @@ -574,15 +556,14 @@ Rectangle { } } - Item { width: parent.width - parent.children[0].width - 5 height: parent.height anchors.left: parent.children[0].right - TextEdit{ - text: texttest - color:"white" + TextEdit { + text: delegateText + color: "white" font.pointSize: 12 readOnly: true width: parent.width * 0.8 @@ -595,8 +576,8 @@ Rectangle { } Text { - text: date - color:"white" + text: delegateDate + color: "white" font.pointSize: 12 anchors.right: parent.right height: parent.height @@ -617,16 +598,16 @@ Rectangle { } function scrollToBottom(bypassDistanceCheck = false, extraMoveDistance = 0) { - const totalHeight = listview.height; // Total height of the content - const currentPosition = messageViewFlickable.contentY; // Current position of the view - const windowHeight = listview.parent.parent.height; // Total height of the window + const totalHeight = listview.height; // Total height of the content + const currentPosition = messageViewFlickable.contentY; // Current position of the view + const windowHeight = listview.parent.parent.height; // Total height of the window const bottomPosition = currentPosition + windowHeight; // Check if the view is within 300 units from the bottom const closeEnoughToBottom = totalHeight - bottomPosition <= 300; if (!bypassDistanceCheck && !closeEnoughToBottom) return; - if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow. - if (bottomPosition == totalHeight) return; // At the bottom, do nothing. + if (totalHeight < windowHeight) return; // No reason to scroll, we don't have an overflow. + if (bottomPosition == totalHeight) return; // At the bottom, do nothing. messageViewFlickable.contentY = listview.height - listview.parent.parent.height; messageViewFlickable.returnToBounds(); @@ -640,32 +621,11 @@ Rectangle { if (type === "notification"){ channel.append({ text: message, date: date, type: "notification" }); - last_message_user = ""; scrollToBottom(null, 30); - last_message_time = new Date(); return; } - // TODO: Replace new time generation with time pregenerated from message - // var current_time = new Date(); - // var elapsed_time = current_time - last_message_time; - // var elapsed_minutes = elapsed_time / (1000 * 60); - - // var last_item_index = channel.count - 1; - // var last_item = channel.get(last_item_index); - - // if (last_message_user === username && elapsed_minutes < 1 && last_item){ - // message = "
" + message - // last_item.text = last_item.text += "\n" + message; - // load_scroll_timer.running = true; - // last_message_time = new Date(); - // return; - // } - - // last_message_user = username; - // last_message_time = new Date(); - channel.append({ text: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } From 392a1446a9faa26042445c6ab54262eb2ff13c02 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 18 Dec 2024 17:38:52 -0600 Subject: [PATCH 08/25] Broke out templates into their own files. --- scripts/system/domainChat/domainChat.qml | 218 +----------------- .../qml_widgets/TemplateChatMessage.qml | 161 +++++++++++++ .../qml_widgets/TemplateNotification.qml | 59 +++++ 3 files changed, 223 insertions(+), 215 deletions(-) create mode 100644 scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml create mode 100644 scripts/system/domainChat/qml_widgets/TemplateNotification.qml diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 2220127f98..f40121ee71 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 import controlsUit 1.0 as HifiControlsUit +import "./qml_widgets" Rectangle { color: Qt.rgba(0.1,0.1,0.1,1) @@ -376,221 +377,8 @@ Rectangle { } // Templates - Component { - id: template_chat_message - - Rectangle { - property int index: delegateIndex - property string username: delegateUsername - - height: Math.max(65, children[1].height + 30) - color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) - width: listview.parent.parent.width - Layout.fillWidth: true - - Item { - width: parent.width - 10 - anchors.horizontalCenter: parent.horizontalCenter - height: 22 - - Text{ - text: username - color: "lightgray" - } - - Text{ - anchors.right: parent.right - text: delegateDate - color: "lightgray" - } - } - - Flow { - anchors.top: parent.children[0].bottom; - width: parent.width * 0.8 - x: 5 - id: messageBoxFlow - - Repeater { - model: delegateText; - - RowLayout { - Text { - text: model.value || "" - font.pointSize: 12 - wrapMode: Text.Wrap - width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; - visible: model.type === 'text' || model.type === 'mention'; - - color: { - switch (model.type) { - case "mention": - return "purple"; - default: - return "white"; - } - } - } - - RowLayout { - width: children[0].contentWidth; - visible: model.type === 'url'; - - Text { - text: model.value || ""; - font.pointSize: 12; - wrapMode: Text.Wrap; - color: "#4EBAFD"; - font.underline: true; - width: parent.width; - - MouseArea { - anchors.fill: parent; - - onClicked: { - Window.openWebBrowser(model.value); - } - } - } - - Text { - text: "πŸ——"; - font.pointSize: 10; - wrapMode: Text.Wrap; - color: "white"; - - MouseArea { - anchors.fill: parent; - - onClicked: { - Qt.openUrlExternally(model.value); - } - } - } - } - - RowLayout { - visible: model.type === 'overteLocation'; - width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); - height: 20; - Layout.leftMargin: 5 - Layout.rightMargin: 5 - - Rectangle { - width: parent.width; - height: 20; - color: "lightgray" - radius: 2; - - Image { - source: "./img/ui/world_black.png" - width: 18; - height: 18; - sourceSize.width: 18 - sourceSize.height: 18 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 2 - anchors.rightMargin: 10 - } - - Text { - text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; - color: "black" - font.pointSize: 12 - x: parent.children[0].width + 5; - anchors.verticalCenter: parent.verticalCenter - } - - MouseArea { - anchors.fill: parent; - - onClicked: { - Window.openUrl(model.value); - } - } - } - } - - Item { - Layout.fillWidth: true - visible: model.type === 'messageEnd' - } - - Item { - visible: model.type === 'imageEmbed'; - width: messageBoxFlow.width; - height: 200 - - Image { - source: model.type === 'imageEmbed' ? model.value : '' - sourceSize.width: 400 - sourceSize.height: 200 - } - } - } - } - } - } - } - - Component { - id: template_notification - - Rectangle { - property int index: delegateIndex - property string username: delegateUsername - - color: "#171717" - width: parent.width - height: 40 - - Item { - width: 10 - height: parent.height - - Rectangle { - height: parent.height - width: 5 - color: "#505186" - } - } - - Item { - width: parent.width - parent.children[0].width - 5 - height: parent.height - anchors.left: parent.children[0].right - - TextEdit { - text: delegateText - color: "white" - font.pointSize: 12 - readOnly: true - width: parent.width * 0.8 - selectByMouse: true - selectByKeyboard: true - height: parent.height - wrapMode: Text.Wrap - verticalAlignment: Text.AlignVCenter - font.italic: true - } - - Text { - text: delegateDate - color: "white" - font.pointSize: 12 - anchors.right: parent.right - height: parent.height - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - font.italic: true - } - } - - } - - } + TemplateChatMessage { id: template_chat_message } + TemplateNotification { id: template_notification } property var channels: { "local": local, diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml new file mode 100644 index 0000000000..4165e8d37c --- /dev/null +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -0,0 +1,161 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 + +Component { + id: template_chat_message + + Rectangle { + property int index: delegateIndex + property string username: delegateUsername + + height: Math.max(65, children[1].height + 30) + color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) + width: listview.parent.parent.width + Layout.fillWidth: true + + Item { + width: parent.width - 10 + anchors.horizontalCenter: parent.horizontalCenter + height: 22 + + Text{ + text: username + color: "lightgray" + } + + Text{ + anchors.right: parent.right + text: delegateDate + color: "lightgray" + } + } + + Flow { + anchors.top: parent.children[0].bottom; + width: parent.width * 0.8 + x: 5 + id: messageBoxFlow + + Repeater { + model: delegateText; + + RowLayout { + Text { + text: model.value || "" + font.pointSize: 12 + wrapMode: Text.Wrap + width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; + visible: model.type === 'text' || model.type === 'mention'; + + color: { + switch (model.type) { + case "mention": + return "purple"; + default: + return "white"; + } + } + } + + RowLayout { + width: children[0].contentWidth; + visible: model.type === 'url'; + + Text { + text: model.value || ""; + font.pointSize: 12; + wrapMode: Text.Wrap; + color: "#4EBAFD"; + font.underline: true; + width: parent.width; + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openWebBrowser(model.value); + } + } + } + + Text { + text: "πŸ——"; + font.pointSize: 10; + wrapMode: Text.Wrap; + color: "white"; + + MouseArea { + anchors.fill: parent; + + onClicked: { + Qt.openUrlExternally(model.value); + } + } + } + } + + RowLayout { + visible: model.type === 'overteLocation'; + width: Math.min(messageBoxFlow.width, children[0].children[1].contentWidth + 35); + height: 20; + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Rectangle { + width: parent.width; + height: 20; + color: "lightgray" + radius: 2; + + Image { + source: "./img/ui/world_black.png" + width: 18; + height: 18; + sourceSize.width: 18 + sourceSize.height: 18 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 2 + anchors.rightMargin: 10 + } + + Text { + text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; + color: "black" + font.pointSize: 12 + x: parent.children[0].width + 5; + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent; + + onClicked: { + Window.openUrl(model.value); + } + } + } + } + + Item { + Layout.fillWidth: true + visible: model.type === 'messageEnd' + } + + Item { + visible: model.type === 'imageEmbed'; + width: messageBoxFlow.width; + height: 200 + + Image { + source: model.type === 'imageEmbed' ? model.value : '' + sourceSize.width: 400 + sourceSize.height: 200 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml new file mode 100644 index 0000000000..e522d58c54 --- /dev/null +++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml @@ -0,0 +1,59 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 + +Component { + id: template_notification + + Rectangle { + property int index: delegateIndex + property string username: delegateUsername + + color: "#171717" + width: parent.width + height: 40 + + Item { + width: 10 + height: parent.height + + Rectangle { + height: parent.height + width: 5 + color: "#505186" + } + } + + Item { + width: parent.width - parent.children[0].width - 5 + height: parent.height + anchors.left: parent.children[0].right + + TextEdit { + text: delegateText + color: "white" + font.pointSize: 12 + readOnly: true + width: parent.width * 0.8 + selectByMouse: true + selectByKeyboard: true + height: parent.height + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + font.italic: true + } + + Text { + text: delegateDate + color: "white" + font.pointSize: 12 + anchors.right: parent.right + height: parent.height + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.italic: true + } + } + } +} \ No newline at end of file From 60284015c26f6dc58da27f79fbbd46c3ed489286 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 04:41:49 -0600 Subject: [PATCH 09/25] Actually fix notifications. --- scripts/system/domainChat/domainChat.js | 16 +++--- scripts/system/domainChat/domainChat.qml | 10 ++-- .../qml_widgets/TemplateChatMessage.qml | 9 ++-- .../qml_widgets/TemplateNotification.qml | 54 +++++++------------ 4 files changed, 33 insertions(+), 56 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 34cd128bce..2d80688d22 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -32,12 +32,8 @@ Controller.keyPressEvent.connect(keyPressEvent); Messages.subscribe("chat"); Messages.messageReceived.connect(receivedMessage); - AvatarManager.avatarAddedEvent.connect((sessionId) => { - _avatarAction("connected", sessionId); - }); - AvatarManager.avatarRemovedEvent.connect((sessionId) => { - _avatarAction("left", sessionId); - }); + AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); }); + AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); }); startup(); @@ -205,7 +201,7 @@ ); } function _avatarAction(type, sessionId) { - Script.setTimeout(() => { + Script.setTimeout(async () => { if (type == "connected") { palData = AvatarManager.getPalData().data; } @@ -233,7 +229,11 @@ _notificationCoreMessage(displayName, type) } - _emitEvent({ type: "notification", ...message }); + // Format notification message + let formattedMessagePacket = {...message}; + formattedMessagePacket.message = await _parseMessage(message.message); + + _emitEvent({ type: "notification", ...formattedMessagePacket }); }, 1500); } async function _loadSettings() { diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index f40121ee71..6a79cb7e2e 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -9,7 +9,6 @@ Rectangle { signal sendToScript(var message); property string pageVal: "local" - property string last_message_user: "" property date last_message_time: new Date() // When the window is created on the script side, the window starts open. @@ -163,7 +162,7 @@ Rectangle { model: getChannel(pageVal) delegate: Loader { property int delegateIndex: model.index - property var delegateText: model.text + property var delegateText: model.message property string delegateUsername: model.username property string delegateDate: model.date @@ -171,7 +170,6 @@ Rectangle { if (model.type === "chat") return template_chat_message; if (model.type === "notification") return template_notification; } - } } } @@ -406,15 +404,13 @@ Rectangle { channel = getChannel(channel) // Format content - if (type === "notification"){ - channel.append({ text: message, date: date, type: "notification" }); + channel.append({ message: message, date: date, type: "notification" }); scrollToBottom(null, 30); - return; } - channel.append({ text: message, username: username, date: date, type: type }); + channel.append({ message: message, username: username, date: date, type: type }); load_scroll_timer.running = true; } diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 4165e8d37c..a9c366ce76 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -7,7 +7,6 @@ Component { Rectangle { property int index: delegateIndex - property string username: delegateUsername height: Math.max(65, children[1].height + 30) color: index % 2 === 0 ? "transparent" : Qt.rgba(0.15,0.15,0.15,1) @@ -19,12 +18,12 @@ Component { anchors.horizontalCenter: parent.horizontalCenter height: 22 - Text{ - text: username + Text { + text: delegateUsername color: "lightgray" } - Text{ + Text { anchors.right: parent.right text: delegateDate color: "lightgray" @@ -109,7 +108,7 @@ Component { radius: 2; Image { - source: "./img/ui/world_black.png" + source: "../img/ui/world_black.png" width: 18; height: 18; sourceSize.width: 18 diff --git a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml index e522d58c54..4b9797d7f4 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateNotification.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateNotification.qml @@ -6,15 +6,12 @@ Component { id: template_notification Rectangle { - property int index: delegateIndex - property string username: delegateUsername - color: "#171717" width: parent.width height: 40 - Item { - width: 10 + RowLayout { + width: parent.width height: parent.height Rectangle { @@ -22,38 +19,23 @@ Component { width: 5 color: "#505186" } - } - - Item { - width: parent.width - parent.children[0].width - 5 - height: parent.height - anchors.left: parent.children[0].right - - TextEdit { - text: delegateText - color: "white" - font.pointSize: 12 - readOnly: true - width: parent.width * 0.8 - selectByMouse: true - selectByKeyboard: true - height: parent.height - wrapMode: Text.Wrap - verticalAlignment: Text.AlignVCenter - font.italic: true - } - Text { - text: delegateDate - color: "white" - font.pointSize: 12 - anchors.right: parent.right - height: parent.height - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - font.italic: true + Repeater { + model: delegateText + + TextEdit { + visible: model.value != undefined; + text: model.value || "" + color: "white" + font.pointSize: 12 + readOnly: true + selectByMouse: true + selectByKeyboard: true + height: root.height + wrapMode: Text.Wrap + font.italic: true + } } } } -} \ No newline at end of file +} From 6dbd63ebe9c5444e5e3e3bce39b0c5cb5ab1a407 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 04:48:56 -0600 Subject: [PATCH 10/25] Fix formatting sometimes not formatting the last part of a message. --- scripts/system/domainChat/domainChat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 2d80688d22..08be9822d9 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -305,10 +305,8 @@ let firstMatch = _findFirstMatch(); if (firstMatch == null) { - // If there was not any matches found in the entire message, format the whole message as a single text entry. - if (messageArray.length == 0) { - messageArray.push({type: 'text', value: runningMessage}); - } + // Format any remaining text as a basic 'text' type. + messageArray.push({type: 'text', value: runningMessage}); // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); From 27fa5da04883b0560141c57a4a72b4ed3f358c2e Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 19 Dec 2024 21:48:05 -0600 Subject: [PATCH 11/25] Housekeeping: data formatting --- scripts/system/domainChat/domainChat.js | 72 +++++++------------------ scripts/system/domainChat/formatting.js | 64 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 scripts/system/domainChat/formatting.js diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 08be9822d9..03aeadd789 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -12,6 +12,10 @@ (() => { ("use strict"); + Script.include([ + "./formatting.js" + ]) + var appIsVisible = false; var settings = { external_window: false, @@ -93,22 +97,16 @@ // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; - message = JSON.parse(message); - - // Get the message data - const currentTimestamp = _getTimestamp(); - const timeArray = _formatTimestamp(currentTimestamp); - + + if ((message = formatting.toJSON(message)) == null) return; // Make sure we are working with a JSON object we expect, otherwise kill + message = formatting.addTimeAndDateStringToPacket(message); + if (!message.channel) message.channel = "domain"; // We don't know where to put this message. Assume it is a domain wide message. message.channel = message.channel.toLowerCase(); // Only recognize channel names as lower case. if (!channels.includes(message.channel)) return; // Check the channel. If the channel is not one we have, do nothing. if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. - - // Format the timestamp - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; - + let formattedMessagePacket = { ...message }; formattedMessagePacket.message = await _parseMessage(message.message) @@ -116,24 +114,16 @@ _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. // Create a new variable based on the message that will be saved. - let savedMessage = message; + let trimmedPacket = formatting.trimPacketToSave(message); + messageHistory.push(trimmedPacket); - // Remove unnecessary data. - delete savedMessage.position; - delete savedMessage.timeString; - delete savedMessage.dateString; - delete savedMessage.action; - - savedMessage.timestamp = currentTimestamp; - - messageHistory.push(savedMessage); while (messageHistory.length > settings.maximum_messages) { messageHistory.shift(); } Settings.setValue("ArmoredChat-Messages", messageHistory); - // Check to see if the message is close enough to the user function isTooFar(messagePosition) { + // Check to see if the message is close enough to the user return Vec3.distance(MyAvatar.position, messagePosition) > maxLocalDistance; } } @@ -218,10 +208,7 @@ } // Format the packet - let message = {}; - const timeArray = _formatTimestamp(_getTimestamp()); - message.timeString = timeArray[0]; - message.dateString = timeArray[1]; + let message = addTimeAndDateStringToPacket({}); message.message = `${displayName} ${type}`; // Show new message on screen @@ -242,43 +229,20 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - const timeArray = _formatTimestamp(_getTimestamp()); - messagePacket = { ...message }; - messagePacket.timeString = timeArray[0]; - messagePacket.dateString = timeArray[1]; + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await _parseMessage(messagePacket.message); // Parse the message for the UI - let formattedMessagePacket = {...messagePacket}; - formattedMessagePacket.message = await _parseMessage(messagePacket.message); - - _emitEvent({ type: "show_message", ...formattedMessagePacket }); + _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } } - // Send current settings to the app - _emitEvent({ type: "initial_settings", settings: settings }); + _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { console.log("Saving config"); Settings.setValue("ArmoredChat-Config", settings); } - function _getTimestamp(){ - return Date.now(); - } - function _formatTimestamp(timestamp){ - let timeArray = []; - - timeArray.push(new Date().toLocaleTimeString(undefined, { - hour12: false, - })); - - timeArray.push(new Date(timestamp).toLocaleDateString(undefined, { - year: "numeric", - month: "long", - day: "numeric", - })); - - return timeArray; - } function _notificationCoreMessage(displayName, message){ Messages.sendLocalMessage( "Floof-Notif", diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js new file mode 100644 index 0000000000..b523f30d2d --- /dev/null +++ b/scripts/system/domainChat/formatting.js @@ -0,0 +1,64 @@ +// +// formatting.js +// +// Created by Armored Dragon, 2024. +// Copyright 2024 Overte e.V. +// +// This just does some basic formatting and minor housekeeping for the domainChat.js application +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +const formatting = { + toJSON: function(data) { + if (typeof data == "object") return data; // Already JSON + + try { + const parsedData = JSON.parse(data); + return parsedData; + } catch (e) { + console.log('Failed to convert data to JSON.') + return null; // Could not convert to json, some error; + } + }, + addTimeAndDateStringToPacket: function(packet) { + // Gets the current time and adds it to a given packet + const timeArray = formatting.helpers._timestampArray(packet.timestamp); + packet.timeString = timeArray[0]; + packet.dateString = timeArray[1]; + return packet; + }, + trimPacketToSave: function(packet) { + // Takes a packet, and returns a packet containing only what is needed to save. + let newPacket = { + channel: packet.channel || "", + displayName: packet.displayName || "", + message: packet.message || "", + timestamp: packet.timestamp || formatting.helpers.getTimestamp(), + }; + return newPacket; + }, + + helpers: { + // Small functions that are used often in the other functions. + _timestampArray: function(timestamp) { + const currentDate = timestamp || formatting.helpers.getTimestamp(); + let timeArray = []; + + timeArray.push(new Date(currentDate).toLocaleTimeString(undefined, { + hour12: false, + })); + + timeArray.push(new Date(currentDate).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + })); + + return timeArray; + }, + getTimestamp: function(){ + return Date.now(); + } + } +} From 891d533bd8b8323921e6d74127f2b1172981d026 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Fri, 20 Dec 2024 05:32:17 -0600 Subject: [PATCH 12/25] Move message parsing to formatting file. --- scripts/system/domainChat/domainChat.js | 114 ++---------------------- scripts/system/domainChat/formatting.js | 98 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 107 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 03aeadd789..6d75556081 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -108,7 +108,7 @@ if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = await _parseMessage(message.message) + formattedMessagePacket.message = await formatting.parseMessage(message.message) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -218,7 +218,7 @@ // Format notification message let formattedMessagePacket = {...message}; - formattedMessagePacket.message = await _parseMessage(message.message); + formattedMessagePacket.message = await formatting.parseMessage(message.message); _emitEvent({ type: "notification", ...formattedMessagePacket }); }, 1500); @@ -229,15 +229,15 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - messagePacket = { ...message }; // Create new variable - messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp - messagePacket.message = await _parseMessage(messagePacket.message); // Parse the message for the UI + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await formatting.parseMessage(messagePacket.message); // Parse the message for the UI - _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI + _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } } - _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app + _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { console.log("Saving config"); @@ -249,85 +249,6 @@ JSON.stringify({ sender: displayName, text: message }) ); } - async function _parseMessage(message){ - const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; - const mentionRegex = /@(\w+)/; // FIXME: Remove - devcode - const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; - - let runningMessage = message; - let messageArray = []; - - const regexPatterns = [ - { type: "url", regex: urlRegex }, - { type: "mention", regex: mentionRegex }, // FIXME: Remove - devcode - { type: "overteLocation", regex: overteLocationRegex } - ] - - // Here is a link https://www.example.com, #hashtag, and @mention. Just for some spice here is another https://exampletwo.com - - while (true) { - let firstMatch = _findFirstMatch(); - - if (firstMatch == null) { - // Format any remaining text as a basic 'text' type. - messageArray.push({type: 'text', value: runningMessage}); - - // Append a final 'fill width' to the message text. - messageArray.push({type: 'messageEnd'}); - break; - } - - _formatMessage(firstMatch); - } - - for (dataChunk of messageArray){ - if (dataChunk.type == 'url'){ - let url = dataChunk.value; - - const res = await fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 - const contentType = res.getResponseHeader("content-type"); - - // TODO: Add support for other media types - if (contentType.startsWith('image/')) { - messageArray.push({type: 'imageEmbed', value: url}); - } - } - } - - return messageArray; - - function _formatMessage(firstMatch){ - let indexOfFirstMatch = firstMatch[0]; - let regex = regexPatterns[firstMatch[1]].regex; - - let foundMatch = runningMessage.match(regex)[0]; - - messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); - messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); - - runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with - } - - function _findFirstMatch(){ - let indexOfFirstMatch = Infinity; - let indexOfRegexPattern = Infinity; - - for (let i = 0; regexPatterns.length > i; i++){ - let indexOfMatch = runningMessage.search(regexPatterns[i].regex); - - if (indexOfMatch == -1) continue; // No match found - - if (indexOfMatch < indexOfFirstMatch) { - indexOfFirstMatch = indexOfMatch; - indexOfRegexPattern = i; - } - } - - if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match - return null; // No found match - } - } - /** * Emit a packet to the HTML front end. Easy communication! * @param {Object} packet - The Object packet to emit to the HTML @@ -337,27 +258,6 @@ chatOverlayWindow.sendToQml(packet); } - function fetch(url, options = {method: "GET"}) { - return new Promise((resolve, reject) => { - let req = new XMLHttpRequest(); - req.onreadystatechange = function () { - - if (req.readyState === req.DONE) { - if (req.status === 200) { - console.log("Content type:", req.getResponseHeader("content-type")); - resolve(req); - - } else { - console.log("Error", req.status, req.statusText); - reject(); - } - } - }; - - req.open(options.method, url); - req.send(); - }); - } })(); diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index b523f30d2d..05d3bf4cfa 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -38,6 +38,82 @@ const formatting = { }; return newPacket; }, + parseMessage: async function(message) { + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; + + let runningMessage = message; // The remaining message that will be parsed + let messageArray = []; // An array of messages that are split up by the formatting functions + + const regexPatterns = [ + { type: "url", regex: urlRegex }, + { type: "overteLocation", regex: overteLocationRegex } + ] + + while (true) { + let firstMatch = _findFirstMatch(); + + if (firstMatch == null) { + // If there is no more text to parse, break out of the loop and return the message array. + // Format any remaining text as a basic 'text' type. + messageArray.push({type: 'text', value: runningMessage}); + + // Append a final 'fill width' to the message text. + messageArray.push({type: 'messageEnd'}); + break; + } + + _formatMessage(firstMatch); + } + + // Embed images in the message array. + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; + + const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); + + // TODO: Add support for other media types + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + } + } + } + + return messageArray; + + function _formatMessage(firstMatch){ + let indexOfFirstMatch = firstMatch[0]; + let regex = regexPatterns[firstMatch[1]].regex; + + let foundMatch = runningMessage.match(regex)[0]; + + messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); + + runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with + } + + function _findFirstMatch(){ + let indexOfFirstMatch = Infinity; + let indexOfRegexPattern = Infinity; + + for (let i = 0; regexPatterns.length > i; i++){ + let indexOfMatch = runningMessage.search(regexPatterns[i].regex); + + if (indexOfMatch == -1) continue; // No match found + + if (indexOfMatch < indexOfFirstMatch) { + indexOfFirstMatch = indexOfMatch; + indexOfRegexPattern = i; + } + } + + if (indexOfFirstMatch !== Infinity) return [indexOfFirstMatch, indexOfRegexPattern]; // If there was a found match + return null; // No found match + } + }, helpers: { // Small functions that are used often in the other functions. @@ -59,6 +135,28 @@ const formatting = { }, getTimestamp: function(){ return Date.now(); + }, + fetch: function (url, options = {method: "GET"}) { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + + if (req.readyState === req.DONE) { + if (req.status === 200) { + console.log("Content type:", req.getResponseHeader("content-type")); + resolve(req); + + } else { + console.log("Error", req.status, req.statusText); + reject(); + } + } + }; + + req.open(options.method, url); + req.send(); + }); } } } From 6976432574a0a266d6a6ebcfa13fb948c4253ded Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 21 Dec 2024 05:45:59 -0600 Subject: [PATCH 13/25] Support animated images. --- .../domainChat/qml_widgets/TemplateChatMessage.qml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index a9c366ce76..7829ee791d 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.7 +import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 @@ -147,10 +147,11 @@ Component { width: messageBoxFlow.width; height: 200 - Image { + AnimatedImage { source: model.type === 'imageEmbed' ? model.value : '' - sourceSize.width: 400 - sourceSize.height: 200 + height: Math.min(sourceSize.height, 200); + onStatusChanged: playing = (status == AnimatedImage.Ready) + fillMode: Image.PreserveAspectFit } } } From eb6530136705887471fe1f2474dad19ca70dd751 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Sat, 21 Dec 2024 06:44:23 -0600 Subject: [PATCH 14/25] Basic video support. --- scripts/system/domainChat/formatting.js | 7 +++- .../qml_widgets/TemplateChatMessage.qml | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 05d3bf4cfa..5e1458db8d 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -74,10 +74,14 @@ const formatting = { const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 const contentType = res.getResponseHeader("content-type"); - // TODO: Add support for other media types if (contentType.startsWith('image/')) { messageArray.push({type: 'imageEmbed', value: url}); + continue; } + if (contentType.startsWith('video/')){ + messageArray.push({type: 'videoEmbed', value: url}); + continue; + } } } @@ -144,7 +148,6 @@ const formatting = { if (req.readyState === req.DONE) { if (req.status === 200) { - console.log("Content type:", req.getResponseHeader("content-type")); resolve(req); } else { diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 7829ee791d..9e33a0503e 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 +import QtMultimedia 5.15 Component { id: template_chat_message @@ -150,10 +151,49 @@ Component { AnimatedImage { source: model.type === 'imageEmbed' ? model.value : '' height: Math.min(sourceSize.height, 200); - onStatusChanged: playing = (status == AnimatedImage.Ready) fillMode: Image.PreserveAspectFit } } + + Item { + visible: model.type === 'videoEmbed'; + width: messageBoxFlow.width; + height: 200; + + Video { + id: videoPlayer + source: model.type === 'videoEmbed' ? model.value : '' + height: 200; + width: 400; + fillMode: Image.PreserveAspectFit + autoLoad: false; + + onStatusChanged: { + if (status === 7) { + // Weird hack to make the video restart when it's over + // Ideally you'd want to use the seek function to restart the video but it doesn't work? + // Will need to make a more refined solution for this later. in the form of a more advanced media player. + // For now, this is sufficient. -AD + let originalURL = videoPlayer.source; + videoPlayer.source = ""; + videoPlayer.source = originalURL; + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + const videoIsOver = videoPlayer.position == videoPlayer.duration + if (videoPlayer.playbackState == MediaPlayer.PlayingState) { + videoPlayer.pause(); + } + else { + parent.play(); + } + } + } + } + } } } } From cf4ec3fb572b06b6c6364e486adbcc36291052e1 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 31 Dec 2024 16:22:55 -0600 Subject: [PATCH 15/25] Add setting to force virtual window in VR. --- scripts/system/domainChat/domainChat.js | 34 ++++++++++++++++++------ scripts/system/domainChat/domainChat.qml | 25 +++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 6d75556081..e1e73bd6cb 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -7,8 +7,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// TODO: Message trimming - (() => { ("use strict"); @@ -20,8 +18,10 @@ var settings = { external_window: false, maximum_messages: 200, - join_notification: true + join_notification: true, + switchToInternalOnHeadsetUsed: true }; + let temporaryChangeModeToVirtual = false; // Global vars var tablet; @@ -38,6 +38,7 @@ Messages.messageReceived.connect(receivedMessage); AvatarManager.avatarAddedEvent.connect((sessionId) => { _avatarAction("connected", sessionId); }); AvatarManager.avatarRemovedEvent.connect((sessionId) => { _avatarAction("left", sessionId); }); + HMD.displayModeChanged.connect(_onHMDDisplayModeChanged); startup(); @@ -134,15 +135,16 @@ break; case "setting_change": // Set the setting value, and save the config - settings[event.setting] = event.value; // Update local settings - _saveSettings(); // Save local settings + settings[event.setting] = event.value; // Update local settings + _saveSettings(); // Save local settings // Extra actions to preform. switch (event.setting) { case "external_window": - chatOverlayWindow.presentationMode = event.value - ? Desktop.PresentationMode.NATIVE - : Desktop.PresentationMode.VIRTUAL; + _changePresentationMode(event.value); + break; + case "switchToInternalOnHeadsetUsed": + _onHMDDisplayModeChanged(HMD.active); break; } @@ -176,6 +178,22 @@ }); } } + function _onHMDDisplayModeChanged(isHMDActive){ + // If the user enabled automatic switching to internal when they put on a headset... + if (!settings.switchToInternalOnHeadsetUsed) return; + + if (isHMDActive) temporaryChangeModeToVirtual = true; + else temporaryChangeModeToVirtual = false; + + _changePresentationMode(settings.external_window); + } + function _changePresentationMode(changeToExternal){ + if (temporaryChangeModeToVirtual) changeToExternal = false; + + chatOverlayWindow.presentationMode = changeToExternal + ? Desktop.PresentationMode.NATIVE + : Desktop.PresentationMode.VIRTUAL; + } function _sendMessage(message, channel) { if (message.length == 0) return; diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 6a79cb7e2e..287bb1a825 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -369,6 +369,29 @@ Rectangle { } } } + // Switch to internal on VR Mode + Rectangle { + width: parent.width + height: 40 + color: "transparent" + + Text { + text: "Force Virtual window in VR" + color: "white" + font.pointSize: 12 + anchors.verticalCenter: parent.verticalCenter + } + + CheckBox { + id: s_force_vw_in_vr + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + onCheckedChanged: { + toScript({type: 'setting_change', setting: 'switchToInternalOnHeadsetUsed', value: checked}) + } + } + } } } @@ -433,9 +456,11 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; + if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; break; } } From 37cdbacf684665eebe0a47cbe25d3aa534a00a39 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 1 Jan 2025 07:35:04 -0600 Subject: [PATCH 16/25] Message embedding toggle. --- scripts/system/domainChat/domainChat.js | 11 ++++---- scripts/system/domainChat/domainChat.qml | 25 ++++++++++++++++++ scripts/system/domainChat/formatting.js | 32 +++++++++++++----------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index e1e73bd6cb..5a97c4dee9 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -19,7 +19,8 @@ external_window: false, maximum_messages: 200, join_notification: true, - switchToInternalOnHeadsetUsed: true + switchToInternalOnHeadsetUsed: true, + enableEmbedding: false // Prevents information leakage, default false }; let temporaryChangeModeToVirtual = false; @@ -109,7 +110,7 @@ if (message.channel == "local" && isTooFar(message.position)) return; // If message is local, and if player is too far away from location, do nothing. let formattedMessagePacket = { ...message }; - formattedMessagePacket.message = await formatting.parseMessage(message.message) + formattedMessagePacket.message = await formatting.parseMessage(message.message, settings.enableEmbedding) _emitEvent({ type: "show_message", ...formattedMessagePacket }); // Update qml view of to new message. _notificationCoreMessage(message.displayName, message.message) // Show a new message on screen. @@ -247,9 +248,9 @@ if (messageHistory) { // Load message history for (message of messageHistory) { - messagePacket = { ...message }; // Create new variable - messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp - messagePacket.message = await formatting.parseMessage(messagePacket.message); // Parse the message for the UI + messagePacket = { ...message }; // Create new variable + messagePacket = formatting.addTimeAndDateStringToPacket(messagePacket); // Add timestamp + messagePacket.message = await formatting.parseMessage(messagePacket.message, settings.enableEmbedding); // Parse the message for the UI _emitEvent({ type: "show_message", ...messagePacket }); // Send message to UI } diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 287bb1a825..ef5860587a 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -392,6 +392,29 @@ Rectangle { } } } + // Toggle media embedding + Rectangle { + width: parent.width + height: 40 + color: "transparent" + + Text { + text: "Enable media embedding" + color: "white" + font.pointSize: 12 + anchors.verticalCenter: parent.verticalCenter + } + + CheckBox { + id: s_enable_embedding + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + onCheckedChanged: { + toScript({type: 'setting_change', setting: 'enableEmbedding', value: checked}) + } + } + } } } @@ -456,11 +479,13 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; + if (message.settings.enableEmbedding) s_enable_embedding.checked = true; break; } } diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 5e1458db8d..24662e4ff1 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -38,7 +38,7 @@ const formatting = { }; return newPacket; }, - parseMessage: async function(message) { + parseMessage: async function(message, enableEmbedding) { const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const overteLocationRegex = /hifi:\/\/[a-zA-Z0-9_-]+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+\/[-+]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+,[+-]?\d*\.?\d+/; @@ -67,23 +67,25 @@ const formatting = { } // Embed images in the message array. - for (dataChunk of messageArray){ - if (dataChunk.type == 'url'){ - let url = dataChunk.value; + if (enableEmbedding) { + for (dataChunk of messageArray){ + if (dataChunk.type == 'url'){ + let url = dataChunk.value; - const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 - const contentType = res.getResponseHeader("content-type"); + const res = await formatting.helpers.fetch(url, {method: 'GET'}); // TODO: Replace with 'HEAD' method. https://github.com/overte-org/overte/issues/1273 + const contentType = res.getResponseHeader("content-type"); - if (contentType.startsWith('image/')) { - messageArray.push({type: 'imageEmbed', value: url}); - continue; - } - if (contentType.startsWith('video/')){ - messageArray.push({type: 'videoEmbed', value: url}); - continue; + if (contentType.startsWith('image/')) { + messageArray.push({type: 'imageEmbed', value: url}); + continue; + } + if (contentType.startsWith('video/')){ + messageArray.push({type: 'videoEmbed', value: url}); + continue; + } } - } - } + } + } return messageArray; From fae1fdd7e045e0ded4174c0d9da9a8747470e5e2 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 1 Jan 2025 11:41:17 -0600 Subject: [PATCH 17/25] Remove lingering print function. --- scripts/system/domainChat/domainChat.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index ef5860587a..9ac69f71ac 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -479,8 +479,6 @@ Rectangle { domain.clear(); break; case "initial_settings": - - print(JSON.stringify(message.settings)); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; From 1c9174ff8f06a8fa290dfd3b22a5f8b46cc2ff23 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 15 Jan 2025 11:46:50 -0600 Subject: [PATCH 18/25] Fix text highlighting. --- .../qml_widgets/TemplateChatMessage.qml | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 9e33a0503e..8a8606c0ff 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -19,15 +19,21 @@ Component { anchors.horizontalCenter: parent.horizontalCenter height: 22 - Text { + TextEdit { text: delegateUsername color: "lightgray" + readOnly: true + selectByMouse: true + selectByKeyboard: true } - Text { + TextEdit { anchors.right: parent.right text: delegateDate color: "lightgray" + readOnly: true + selectByMouse: true + selectByKeyboard: true } } @@ -41,12 +47,15 @@ Component { model: delegateText; RowLayout { - Text { + TextEdit { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; visible: model.type === 'text' || model.type === 'mention'; + readOnly: true + selectByMouse: true + selectByKeyboard: true color: { switch (model.type) { @@ -62,13 +71,16 @@ Component { width: children[0].contentWidth; visible: model.type === 'url'; - Text { + TextEdit { text: model.value || ""; font.pointSize: 12; wrapMode: Text.Wrap; color: "#4EBAFD"; font.underline: true; width: parent.width; + readOnly: true + selectByMouse: true + selectByKeyboard: true MouseArea { anchors.fill: parent; @@ -120,12 +132,15 @@ Component { anchors.rightMargin: 10 } - Text { + TextEdit { text: model.type === 'overteLocation' ? model.value.split('hifi://')[1].split('/')[0] : ''; color: "black" font.pointSize: 12 x: parent.children[0].width + 5; anchors.verticalCenter: parent.verticalCenter + readOnly: true + selectByMouse: true + selectByKeyboard: true } MouseArea { From e67526cdf5ddfbff7931794c34dcfd3eaeb6617c Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 15 Jan 2025 13:53:16 -0600 Subject: [PATCH 19/25] Fix binding loops. --- .../qml_widgets/TemplateChatMessage.qml | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 8a8606c0ff..b5b82a7d79 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -20,26 +20,23 @@ Component { height: 22 TextEdit { - text: delegateUsername - color: "lightgray" - readOnly: true - selectByMouse: true - selectByKeyboard: true + text: delegateUsername; + color: "lightgray"; + readOnly: true; + selectByMouse: true; + selectByKeyboard: true; } - TextEdit { - anchors.right: parent.right - text: delegateDate - color: "lightgray" - readOnly: true - selectByMouse: true - selectByKeyboard: true + Text { + anchors.right: parent.right; + text: delegateDate; + color: "lightgray"; } } Flow { anchors.top: parent.children[0].bottom; - width: parent.width * 0.8 + width: parent.width; x: 5 id: messageBoxFlow @@ -47,11 +44,13 @@ Component { model: delegateText; RowLayout { + width: parent.width; + TextEdit { text: model.value || "" font.pointSize: 12 wrapMode: Text.Wrap - width: model.type === 'text' || model.type === 'mention' ? Math.min(messageBoxFlow.width, contentWidth) : 0; + width: messageBoxFlow.width; visible: model.type === 'text' || model.type === 'mention'; readOnly: true selectByMouse: true @@ -68,10 +67,11 @@ Component { } RowLayout { - width: children[0].contentWidth; + width: urlTypeTextDisplay.width; visible: model.type === 'url'; TextEdit { + id: urlTypeTextDisplay; text: model.value || ""; font.pointSize: 12; wrapMode: Text.Wrap; @@ -154,8 +154,8 @@ Component { } Item { - Layout.fillWidth: true - visible: model.type === 'messageEnd' + Layout.fillWidth: true; + visible: model.type === 'messageEnd'; } Item { From dbf5af052b59a7467d7d1ac2a73a4ded88bd7ae8 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Thu, 16 Jan 2025 14:53:54 -0600 Subject: [PATCH 20/25] Remove video embeds. Added debug logs. Don't save settings when initializing the application. --- scripts/system/domainChat/domainChat.js | 21 ++++- scripts/system/domainChat/domainChat.qml | 13 ++- .../qml_widgets/TemplateChatMessage.qml | 79 +++++++++---------- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 5a97c4dee9..0034f73093 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -99,7 +99,6 @@ // Is the message a chat message? channel = channel.toLowerCase(); if (channel !== "chat") return; - if ((message = formatting.toJSON(message)) == null) return; // Make sure we are working with a JSON object we expect, otherwise kill message = formatting.addTimeAndDateStringToPacket(message); @@ -194,6 +193,8 @@ chatOverlayWindow.presentationMode = changeToExternal ? Desktop.PresentationMode.NATIVE : Desktop.PresentationMode.VIRTUAL; + + console.log(`Presentation mode was changed to ${chatOverlayWindow.presentationMode}`); } function _sendMessage(message, channel) { if (message.length == 0) return; @@ -244,6 +245,7 @@ } async function _loadSettings() { settings = Settings.getValue("ArmoredChat-Config", settings); + console.log("Loading settings: ", jstr(settings)); if (messageHistory) { // Load message history @@ -259,10 +261,11 @@ _emitEvent({ type: "initial_settings", settings: settings }); // Send current settings to the app } function _saveSettings() { - console.log("Saving config"); + console.log("Saving settings: ", jstr(settings)); Settings.setValue("ArmoredChat-Config", settings); } function _notificationCoreMessage(displayName, message){ + console.log("Sending notification to notificationCore:", `Display name: ${displayName}\n Message: ${message}`); Messages.sendLocalMessage( "Floof-Notif", JSON.stringify({ sender: displayName, text: message }) @@ -274,9 +277,19 @@ * @param {("show_message"|"clear_messages"|"notification"|"initial_settings")} packet.type - The type of packet it is */ function _emitEvent(packet = { type: "" }) { + if (packet.type == `show_message`) { + // Don't show the message contents, this is a courtesy to prevent message leakage in the logs. + let strippedPacket = {...packet}; + delete strippedPacket.message + console.log("Sending packet to QML interface", jstr(strippedPacket)); + } + else { + console.log("Sending packet to QML interface", jstr(packet)); + } + chatOverlayWindow.sendToQml(packet); } - - + // Debug and developer functions and data + const jstr = (object) => JSON.stringify(object, null, 4); // JSON Stringify function with formatting })(); diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index 9ac69f71ac..be4b6d4f79 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -8,8 +8,10 @@ Rectangle { color: Qt.rgba(0.1,0.1,0.1,1) signal sendToScript(var message); - property string pageVal: "local" - property date last_message_time: new Date() + property string pageVal: "local"; + property date last_message_time: new Date(); + property bool initialized: false; + // When the window is created on the script side, the window starts open. // Once the QML window is created wait, then send the initialized signal. @@ -445,7 +447,6 @@ Rectangle { messageViewFlickable.returnToBounds(); } - function addMessage(username, message, date, channel, type){ channel = getChannel(channel) @@ -479,17 +480,23 @@ Rectangle { domain.clear(); break; case "initial_settings": + print(`Got settings:\n ${JSON.stringify(message.settings, null, 4)}`); if (message.settings.external_window) s_external_window.checked = true; if (message.settings.maximum_messages) s_maximum_messages.value = message.settings.maximum_messages; if (message.settings.join_notification) s_join_notification.checked = true; if (message.settings.switchToInternalOnHeadsetUsed) s_force_vw_in_vr.checked = true; if (message.settings.enableEmbedding) s_enable_embedding.checked = true; + + initialized = true; // Application is ready + break; } } // Send message to script function toScript(packet){ + if (packet.type === "setting_change" && !initialized) return; // Don't announce a change in settings if not ready + sendToScript(packet) } } diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index b5b82a7d79..641a0ca0b3 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,7 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -import QtMultimedia 5.15 +// import QtMultimedia 5.15 Component { id: template_chat_message @@ -170,45 +170,44 @@ Component { } } - Item { - visible: model.type === 'videoEmbed'; - width: messageBoxFlow.width; - height: 200; - - Video { - id: videoPlayer - source: model.type === 'videoEmbed' ? model.value : '' - height: 200; - width: 400; - fillMode: Image.PreserveAspectFit - autoLoad: false; - - onStatusChanged: { - if (status === 7) { - // Weird hack to make the video restart when it's over - // Ideally you'd want to use the seek function to restart the video but it doesn't work? - // Will need to make a more refined solution for this later. in the form of a more advanced media player. - // For now, this is sufficient. -AD - let originalURL = videoPlayer.source; - videoPlayer.source = ""; - videoPlayer.source = originalURL; - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - const videoIsOver = videoPlayer.position == videoPlayer.duration - if (videoPlayer.playbackState == MediaPlayer.PlayingState) { - videoPlayer.pause(); - } - else { - parent.play(); - } - } - } - } - } + // Item { + // visible: model.type === 'videoEmbed'; + // width: messageBoxFlow.width; + // height: 200; + + // Video { + // id: videoPlayer + // source: model.type === 'videoEmbed' ? model.value : '' + // height: 200; + // width: 400; + // fillMode: Image.PreserveAspectFit + // autoLoad: false; + + // onStatusChanged: { + // if (status === 7) { + // // Weird hack to make the video restart when it's over + // // Ideally you'd want to use the seek function to restart the video but it doesn't work? + // // Will need to make a more refined solution for this later. in the form of a more advanced media player. + // // For now, this is sufficient. -AD + // let originalURL = videoPlayer.source; + // videoPlayer.source = ""; + // videoPlayer.source = originalURL; + // } + // } + + // MouseArea { + // anchors.fill: parent + // onClicked: { + // const videoIsOver = videoPlayer.position == videoPlayer.duration + // if (videoPlayer.playbackState == MediaPlayer.PlayingState) { + // videoPlayer.pause(); + // } + // else { + // parent.play(); + // } + // } + // } + // } } } } From cf8a21c794a9c230e335a635c0967cd9b234c290 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Wed, 22 Jan 2025 23:12:03 -0600 Subject: [PATCH 21/25] Fix spacing on messages with links. --- scripts/system/domainChat/domainChat.qml | 1 - scripts/system/domainChat/formatting.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/system/domainChat/domainChat.qml b/scripts/system/domainChat/domainChat.qml index be4b6d4f79..28ec98d390 100644 --- a/scripts/system/domainChat/domainChat.qml +++ b/scripts/system/domainChat/domainChat.qml @@ -189,7 +189,6 @@ Rectangle { } } - ListModel { id: local } diff --git a/scripts/system/domainChat/formatting.js b/scripts/system/domainChat/formatting.js index 24662e4ff1..c534f3a720 100644 --- a/scripts/system/domainChat/formatting.js +++ b/scripts/system/domainChat/formatting.js @@ -56,7 +56,7 @@ const formatting = { if (firstMatch == null) { // If there is no more text to parse, break out of the loop and return the message array. // Format any remaining text as a basic 'text' type. - messageArray.push({type: 'text', value: runningMessage}); + if (runningMessage.trim() != "") messageArray.push({type: 'text', value: runningMessage}); // Append a final 'fill width' to the message text. messageArray.push({type: 'messageEnd'}); @@ -95,7 +95,7 @@ const formatting = { let foundMatch = runningMessage.match(regex)[0]; - messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); + if (runningMessage.substring(0, indexOfFirstMatch) != "") messageArray.push({type: 'text', value: runningMessage.substring(0, indexOfFirstMatch)}); messageArray.push({type: regexPatterns[firstMatch[1]].type, value: runningMessage.substring(indexOfFirstMatch, indexOfFirstMatch + foundMatch.length)}); runningMessage = runningMessage.substring(indexOfFirstMatch + foundMatch.length); // Remove the part of the message we have worked with From aacdf64bf6809a15b36a6ed079d7802535ff3f5d Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Mon, 27 Jan 2025 23:53:58 -0600 Subject: [PATCH 22/25] Fix wrapping. Remove unused video embed code. --- .../qml_widgets/TemplateChatMessage.qml | 47 ++----------------- 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml index 641a0ca0b3..0f97a614ae 100644 --- a/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml +++ b/scripts/system/domainChat/qml_widgets/TemplateChatMessage.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 -// import QtMultimedia 5.15 Component { id: template_chat_message @@ -43,14 +42,15 @@ Component { Repeater { model: delegateText; - RowLayout { + Item { width: parent.width; + height: children[0].contentHeight; TextEdit { text: model.value || "" font.pointSize: 12 - wrapMode: Text.Wrap - width: messageBoxFlow.width; + wrapMode: TextEdit.WordWrap + width: parent.width * 0.8 visible: model.type === 'text' || model.type === 'mention'; readOnly: true selectByMouse: true @@ -170,44 +170,7 @@ Component { } } - // Item { - // visible: model.type === 'videoEmbed'; - // width: messageBoxFlow.width; - // height: 200; - - // Video { - // id: videoPlayer - // source: model.type === 'videoEmbed' ? model.value : '' - // height: 200; - // width: 400; - // fillMode: Image.PreserveAspectFit - // autoLoad: false; - - // onStatusChanged: { - // if (status === 7) { - // // Weird hack to make the video restart when it's over - // // Ideally you'd want to use the seek function to restart the video but it doesn't work? - // // Will need to make a more refined solution for this later. in the form of a more advanced media player. - // // For now, this is sufficient. -AD - // let originalURL = videoPlayer.source; - // videoPlayer.source = ""; - // videoPlayer.source = originalURL; - // } - // } - - // MouseArea { - // anchors.fill: parent - // onClicked: { - // const videoIsOver = videoPlayer.position == videoPlayer.duration - // if (videoPlayer.playbackState == MediaPlayer.PlayingState) { - // videoPlayer.pause(); - // } - // else { - // parent.play(); - // } - // } - // } - // } + } } } From 25fe4850b99c655b7fa3bc3943e6c40925572c95 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 28 Jan 2025 00:00:11 -0600 Subject: [PATCH 23/25] Added a sortOrder. https://github.com/overte-org/overte/pull/1276 --- scripts/system/domainChat/domainChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 0034f73093..74de8d1b9b 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -49,6 +49,7 @@ appButton = tablet.addButton({ icon: Script.resolvePath("./img/icon_white.png"), activeIcon: Script.resolvePath("./img/icon_black.png"), + sortOrder: 8, text: "CHAT", isActive: appIsVisible, }); From 6cc11fdd402055044c4ed6de9b3196320d5f6382 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Wed, 12 Feb 2025 10:40:06 -0600 Subject: [PATCH 24/25] Rebase --- .github/workflows/linux_server_build.yml | 16 +- .github/workflows/master_build.yml | 4 +- .github/workflows/master_deploy_apidocs.yml | 2 +- .github/workflows/master_deploy_doxygen.yml | 2 +- .github/workflows/pr_build.yml | 8 +- .github/workflows/release_build.yml | 1 - .gitignore | 1 + CHANGELOG.md | 84 +- hifi_qt.py | 2 +- .../dialogs/graphics/GraphicsSettings.qml | 30 + interface/src/Application.cpp | 56 +- interface/src/Application.h | 11 + interface/src/CameraRootTransformNode.cpp | 48 + interface/src/CameraRootTransformNode.h | 20 + interface/src/avatar/MyAvatar.cpp | 13 + interface/src/avatar/MyAvatar.h | 4 + .../src/raypick/PickScriptingInterface.cpp | 4 + .../scripting/RenderScriptingInterface.cpp | 7 + .../src/scripting/RenderScriptingInterface.h | 18 +- interface/src/ui/PreferencesDialog.cpp | 13 +- .../src/avatars-renderer/Avatar.cpp | 31 +- .../src/RenderableZoneEntityItem.cpp | 47 +- .../entities/src/EntityItemPropertiesDocs.cpp | 4 +- libraries/entities/src/EntityTypes.h | 6 +- libraries/gpu/src/gpu/Batch.h | 4 + libraries/gpu/src/gpu/Texture.h | 73 +- libraries/gpu/src/gpu/Texture_ktx.cpp | 124 ++- .../src/material-networking/TextureCache.cpp | 12 + .../model-serializers/src/GLTFSerializer.cpp | 2 +- libraries/octree/src/OctreePacketData.cpp | 9 +- .../procedural/src/procedural/Procedural.cpp | 220 +++- .../procedural/src/procedural/Procedural.h | 1 + .../src/AmbientOcclusionEffect.cpp | 5 +- .../src/AmbientOcclusionStage.cpp | 48 +- .../render-utils/src/AmbientOcclusionStage.h | 65 +- .../src/AssembleLightingStageTask.cpp | 1 + .../render-utils/src/BackgroundStage.cpp | 65 +- libraries/render-utils/src/BackgroundStage.h | 64 +- libraries/render-utils/src/BloomEffect.cpp | 5 +- libraries/render-utils/src/BloomStage.cpp | 51 +- libraries/render-utils/src/BloomStage.h | 66 +- .../src/DeferredLightingEffect.cpp | 17 +- libraries/render-utils/src/DrawHaze.cpp | 4 +- libraries/render-utils/src/FadeEffect.cpp | 3 +- libraries/render-utils/src/FadeEffectJobs.cpp | 3 +- libraries/render-utils/src/HazeStage.cpp | 54 +- libraries/render-utils/src/HazeStage.h | 66 +- .../render-utils/src/HighlightEffect.cpp | 10 +- libraries/render-utils/src/LightClusters.cpp | 8 +- libraries/render-utils/src/LightClusters.h | 2 +- libraries/render-utils/src/LightPayload.cpp | 8 +- libraries/render-utils/src/LightStage.cpp | 74 +- libraries/render-utils/src/LightStage.h | 124 +-- .../render-utils/src/RenderCommonTask.cpp | 7 +- libraries/render-utils/src/RenderCommonTask.h | 3 + .../render-utils/src/RenderDeferredTask.cpp | 5 +- .../render-utils/src/RenderForwardTask.cpp | 5 +- .../render-utils/src/SubsurfaceScattering.cpp | 4 +- .../src/ToneMapAndResampleTask.cpp | 5 +- .../render-utils/src/TonemappingStage.cpp | 48 +- libraries/render-utils/src/TonemappingStage.h | 65 +- libraries/render-utils/src/ZoneRenderer.cpp | 21 +- libraries/render-utils/src/sdf_text3D.slh | 4 +- libraries/render-utils/src/text/Font.cpp | 2 +- libraries/render-utils/src/text/Font.h | 4 +- libraries/render/src/render/DrawStatus.cpp | 3 +- .../render/src/render/HighlightStage.cpp | 49 +- libraries/render/src/render/HighlightStage.h | 52 +- libraries/render/src/render/HighlightStyle.h | 2 + libraries/render/src/render/Scene.cpp | 12 +- libraries/render/src/render/Stage.cpp | 14 +- libraries/render/src/render/Stage.h | 127 ++- libraries/render/src/render/StageSetup.h | 35 + .../render/src/render/TransitionStage.cpp | 46 +- libraries/render/src/render/TransitionStage.h | 46 +- libraries/shared/CMakeLists.txt | 3 +- libraries/shared/src/AACube.h | 16 + libraries/shared/src/BlendshapeConstants.h | 19 + libraries/shared/src/PickFilter.h | 5 +- libraries/shared/src/SerDes.cpp | 85 ++ libraries/shared/src/SerDes.h | 953 ++++++++++++++++++ scripts/system/create/edit.js | 5 + scripts/system/domainChat/domainChat.js | 3 +- scripts/system/graphicsSettings.js | 54 +- scripts/system/places/icons/portalFX.png | Bin 0 -> 79835 bytes scripts/system/places/places.css | 67 +- scripts/system/places/places.html | 41 +- scripts/system/places/places.js | 87 +- scripts/system/places/portal.js | 201 ++++ scripts/system/places/sounds/portalSound.mp3 | Bin 0 -> 83172 bytes .../system/places/sounds/teleportSound.mp3 | Bin 0 -> 53079 bytes server-console/src/main.js | 2 +- tests/ktx/src/KtxTests.cpp | 22 + tests/ktx/src/KtxTests.h | 1 + tests/shared/src/SerializerTests.cpp | 232 +++++ tests/shared/src/SerializerTests.h | 33 + .../linux-ci/Dockerfile_build_ubuntu-20.04 | 6 +- .../linux-ci/Dockerfile_build_ubuntu-22.04 | 6 +- .../rpm_package/Dockerfile_build_fedora-39 | 20 - tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 | 22 +- 100 files changed, 2906 insertions(+), 1156 deletions(-) create mode 100644 interface/src/CameraRootTransformNode.cpp create mode 100644 interface/src/CameraRootTransformNode.h create mode 100644 libraries/render/src/render/StageSetup.h create mode 100644 libraries/shared/src/SerDes.cpp create mode 100644 libraries/shared/src/SerDes.h create mode 100644 scripts/system/places/icons/portalFX.png create mode 100644 scripts/system/places/portal.js create mode 100644 scripts/system/places/sounds/portalSound.mp3 create mode 100644 scripts/system/places/sounds/teleportSound.mp3 create mode 100644 tests/shared/src/SerializerTests.cpp create mode 100644 tests/shared/src/SerializerTests.h delete mode 100644 tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 diff --git a/.github/workflows/linux_server_build.yml b/.github/workflows/linux_server_build.yml index 64f1529221..084287c25b 100644 --- a/.github/workflows/linux_server_build.yml +++ b/.github/workflows/linux_server_build.yml @@ -82,16 +82,6 @@ jobs: arch: aarch64 runner: [self_hosted, type-cax41, image-arm-app-docker-ce] - - os: fedora-39 - image: docker.io/overte/overte-server-build:0.1.4-fedora-39-amd64 - arch: amd64 - runner: [self_hosted, type-cx52, image-x86-app-docker-ce] - - - os: fedora-39 - image: docker.io/overte/overte-server-build:0.1.4-fedora-39-aarch64 - arch: aarch64 - runner: [self_hosted, type-cax41, image-arm-app-docker-ce] - - os: fedora-40 image: docker.io/overte/overte-server-build:0.1.4-fedora-40-amd64 arch: amd64 @@ -224,10 +214,6 @@ jobs: else # RPM if [ "${{ matrix.os }}" == "rockylinux-9" ]; then echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.el9.$INSTALLER_EXT" >> $GITHUB_ENV - elif [ "${{ matrix.os }}" == "fedora-38" ]; then - echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc38.$INSTALLER_EXT" >> $GITHUB_ENV - elif [ "${{ matrix.os }}" == "fedora-39" ]; then - echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc39.$INSTALLER_EXT" >> $GITHUB_ENV elif [ "${{ matrix.os }}" == "fedora-40" ]; then echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc40.$INSTALLER_EXT" >> $GITHUB_ENV else @@ -266,7 +252,7 @@ jobs: - name: Archive cmake logs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cmake-logs-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.number }}.tar.xz path: cmake-logs-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.number }}.tar.xz diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml index 8754c08696..2704ea16d4 100644 --- a/.github/workflows/master_build.yml +++ b/.github/workflows/master_build.yml @@ -65,7 +65,7 @@ jobs: run: | echo "UPLOAD_PREFIX=build/overte/master" >> $GITHUB_ENV - echo ::set-output name=github_sha_short::`echo $GIT_COMMIT | cut -c1-7` + echo "{github_sha_short}={`echo $GIT_COMMIT | cut -c1-7`}" >> $GITHUB_OUTPUT echo "JOB_NAME=build (${{matrix.os}}, ${{matrix.build_type}})" >> $GITHUB_ENV echo "APP_TARGET_NAME=$APP_NAME" >> $GITHUB_ENV # Linux build variables @@ -82,7 +82,7 @@ jobs: echo "ZIP_ARGS=-r" >> $GITHUB_ENV echo "INSTALLER_EXT=dmg" >> $GITHUB_ENV echo "CMAKE_EXTRA=-DOVERTE_CPU_ARCHITECTURE= -DCMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=OFF -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -DOPENSSL_LIBRARIES=/usr/local/opt/openssl/lib -G Xcode" >> $GITHUB_ENV - echo "::set-output name=symbols_archive::${BUILD_NUMBER}-${{ matrix.build_type }}-mac-symbols.zip" + echo "symbols_archive=${BUILD_NUMBER}-${{ matrix.build_type }}-mac-symbols.zip" >> $GITHUB_ENV echo "APP_TARGET_NAME=Overte" >> $GITHUB_ENV fi # Windows build variables diff --git a/.github/workflows/master_deploy_apidocs.yml b/.github/workflows/master_deploy_apidocs.yml index 79d4533b40..a4c501ac5a 100644 --- a/.github/workflows/master_deploy_apidocs.yml +++ b/.github/workflows/master_deploy_apidocs.yml @@ -31,7 +31,7 @@ jobs: jsdoc root.js -r api-mainpage.md -c config.json -d output - name: Deploy API-docs - uses: SamKirkland/FTP-Deploy-Action@v4.3.4 + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 with: server: www531.your-server.de protocol: ftps diff --git a/.github/workflows/master_deploy_doxygen.yml b/.github/workflows/master_deploy_doxygen.yml index 9a12a165a8..d2caa15d18 100644 --- a/.github/workflows/master_deploy_doxygen.yml +++ b/.github/workflows/master_deploy_doxygen.yml @@ -29,7 +29,7 @@ jobs: doxygen Doxyfile - name: Deploy Doxygen - uses: SamKirkland/FTP-Deploy-Action@v4.3.4 + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 with: server: www531.your-server.de protocol: ftps diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index b1b8470bf0..c9d06d5663 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -64,8 +64,8 @@ jobs: runner: [self_hosted, type-cx52, image-x86-app-docker-ce] arch: amd64 build_type: full - apt-dependencies: pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 # add missing dependencies to docker image when convenient - image: docker.io/overte/overte-full-build:0.1.1-ubuntu-20.04-amd64 + # apt-dependencies: # add missing dependencies to docker image when convenient + image: docker.io/overte/overte-full-build:0.1.2-ubuntu-20.04-amd64 # Android builds are currently failing #- os: ubuntu-18.04 # build_type: android @@ -75,7 +75,7 @@ jobs: runner: [self_hosted, type-cax41, image-arm-app-docker-ce] arch: aarch64 build_type: full - image: docker.io/overte/overte-full-build:0.1.1-ubuntu-22.04-aarch64 + image: docker.io/overte/overte-full-build:0.1.2-ubuntu-22.04-aarch64 fail-fast: false runs-on: ${{matrix.runner}} container: ${{matrix.image}} @@ -244,7 +244,7 @@ jobs: - name: Archive cmake logs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cmake-logs-${{ matrix.os }}-${{ github.event.number }}.tar.xz path: ./cmake-logs-${{ matrix.os }}-${{ github.event.number }}.tar.xz diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index a678053632..b72075017c 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -56,7 +56,6 @@ jobs: echo "UPLOAD_PREFIX=build/overte/release/" >> $GITHUB_ENV fi - echo ::set-output name=github_sha_short::`echo $GIT_COMMIT | cut -c1-7` echo "JOB_NAME=${{matrix.os}}, ${{matrix.build_type}}" >> $GITHUB_ENV echo "APP_TARGET_NAME=$APP_NAME" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index 90ff187027..6c35659bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ local.properties !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.cache # Workspace *.code-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index de11f33c5e..c95c32c268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,84 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), This project does **not** adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - +## [2024.11.1] 2024.11.23 -## [2024.07.1] 2023.07.12 +### Fixes +- Hard code link colors in Armored Chat (PR1083) +- ArmoredChat: Alleviate scrolling issue (PR1106) +- Armored Chat: Change the 'open in new window' character (PR1084) +- Fix mouselook ignoring setting. (PR1081) +- Fix controllerScripts uncaught exception. (PR1086) +- Fix wireshark dissector (PR1088) +- Create App: Material Assistant: Add Mtoon, Shader_simple, missing PBR properties and bug fixes. (PR1091) +- Fix login failure handling and improve logging. (PR1093) +- add a setting to workaround the GLES colorspace conversion issue (PR1105) +- Improve model load priority (PR1085) +- fix accidentally clearing url fields when you don't have view permission (PR1138) +- Avatar App: Fixed lingering references to now deleted QML element (PR1155) +- Fix selfie mode movement (PR1127) +- Fix Create App not honoring menu bar actions (PR1123) +- Fix Uuid.NULL behavior (PR1168) +- Rebuild fonts with full charset (NOT -allglyphs) (PR1172) +- fix web entities not accepting keyboard focus (PR1187) +- Fix stutter when an object is fading (PR1185) +- fix density max typo (PR1195) +- Fix ArmoredChat quick_message qml dialog colors on light theme systems (PR1196) +- Fix missing properties in Script API (PR1215) +- Fix ArmoredChat scrolling (PR1210) +- Force enable JSDoc to get scripting console autocomplete working on Windows (PR1219) +- Fix lack of entityHostType property (PR1224) +- Fix access-after-delete on leaving domain with entity scripts (PR1230) +- fix fade out not working in forward rendering (PR1234) +- Fix access-after-delete during entity script engine cleanup (PR1236) +- Fix script-related crashes on exiting a domain (PR1251) +- Update privacy policy link (PR1237) + +### Changes +- Replace Floofchat with ArmoredChat (PR961) +- MouseLook.js refactor (PR1004) +- Custom shader fallbacks (PR1058) +- Update outdated language (PR1102) +- Automated entity property serialization (PR1098) +- Create app: highlight avatar entities (PR1152) +- Update Avatar App icons (PR1141) +- Place App: Weekly promoted place (PR1153) +- Change minimum angular velocity to a lower one (PR1171) +- Places App: Persisted Maturity Filter and Default value for Newbies. (PR1164) + +### Additions +- Mirrors + Portals (PR721) +- Entity tags (PR748) +- Web Entity wantsKeyboardFocus (PR814) +- Audio Zone Properties (PR847) +- Ability to smooth model animations (PR889) +- GPU Particles (PR884) +- Unlit Shapes (PR1041) +- Ambient Light Color (PR1043) +- Dump protocol data (PR1087) +- Sound Entities (PR894) +- Zone properties for tonemapping and ambient occlusion (PR1050) +- Add bloom, haze, AO, and procedural shaders to Graphics settings (PR1053) +- Create App: Revolutionary "Paste" Url buttons for the "Create Model", "Create Material" and "Create Voxels" UI (PR1094) +- Text verticalAlignment, send entity property enums as uint8_t, fix text recalculating too often, fix textSize (PR1111) +- Create app: Grab and Equip (PR1160) +- Create App: Add "Paste" button for NewSoundDialog QML (PR1202) +- Added sounds to all incoming chat messages (PR1250) + +### Removals +- Remove (deprecated) attachments (PR1069) +- Remove unused onFirstRun.js (PR1089) +- Remove Google Poly (PR1137) +- Remove hifi screenshare (PR1165) + +### Build System +- Add CLion-style build directories to .gitignore (PR1135) + +### Security +- Sanitize notificationCore text to prevent XSS (PR1078) + + +## [2024.07.1] 2024.07.12 ### Fixes - Fix more warnings (PR1007) @@ -57,7 +132,7 @@ This project does **not** adhere to [Semantic Versioning](https://semver.org/spe - Remove remnants of RELEASE_NAME. (PR1077) -## [2024.06.1] 2023.06.24 +## [2024.06.1] 2024.06.24 ### Fixes - Fix QNetworkRequest::FollowRedirectsAttribute deprecated warning (PR711) @@ -306,9 +381,6 @@ This project does **not** adhere to [Semantic Versioning](https://semver.org/spe - Added a setting to disable snapshot notifications (PR189) - Added a setting to switch between screenshot formats (PR134) -### Removals -- - ### Build system - Fixed "may be used uninitialized" warning for blendtime (PR269) - Updated SPIRV-Cross to sdk-1.3.231.1 (PR271) diff --git a/hifi_qt.py b/hifi_qt.py index b4ab5e9b3c..053ff997b7 100644 --- a/hifi_qt.py +++ b/hifi_qt.py @@ -157,7 +157,7 @@ def __init__(self, args): u_major = int( distro.major_version() or '0' ) if distro.id() == 'ubuntu' or distro.id() == 'linuxmint': if (distro.id() == 'ubuntu' and u_major == 20) or distro.id() == 'linuxmint' and u_major == 20: - self.qtUrl = self.assets_url + '/dependencies/qt5/qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz' + self.qtUrl = self.assets_url + '/dependencies/qt5/qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz' elif (distro.id() == 'ubuntu' and u_major > 20) or (distro.id() == 'linuxmint' and u_major > 20): self.__no_qt_package_error() else: diff --git a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml index a928b1379f..a0804957be 100644 --- a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml +++ b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml @@ -658,6 +658,36 @@ Flickable { } } } + Item { + Layout.preferredWidth: parent.width + Layout.preferredHeight: 35 + Layout.topMargin: 16 + + HifiStylesUit.RalewayRegular { + id: enableCameraClippingHeader + text: "3rd Person Camera Clipping" + anchors.left: parent.left + anchors.top: parent.top + width: 200 + height: parent.height + size: 16 + color: "#FFFFFF" + } + + HifiControlsUit.CheckBox { + id: enableCameraClipping + checked: Render.cameraClippingEnabled + boxSize: 16 + spacing: -1 + colorScheme: hifi.colorSchemes.dark + anchors.left: enableCameraClippingHeader.right + anchors.leftMargin: 20 + anchors.top: parent.top + onCheckedChanged: { + Render.cameraClippingEnabled = enableCameraClipping.checked; + } + } + } } ColumnLayout { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 64aba5ce7e..c5951a2aff 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -241,6 +241,7 @@ #include #include #include +#include #include "ResourceRequestObserver.h" @@ -969,6 +970,7 @@ const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const bool DEFAULT_SHOW_GRAPHICS_ICON = true; const QString DEFAULT_CURSOR_NAME = "SYSTEM"; const bool DEFAULT_MINI_TABLET_ENABLED = false; const bool DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED = true; @@ -1003,6 +1005,7 @@ Application::Application( _previousSessionCrashed(false), //setupEssentials(parser, false)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), + _cameraClippingEnabled("cameraClippingEnabled", false), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _firstRun(Settings::firstRun, true), @@ -1010,6 +1013,7 @@ Application::Application( _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), + _showGraphicsIconSetting("showGraphicsIcon", DEFAULT_SHOW_GRAPHICS_ICON), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _awayStateWhenFocusLostInVREnabled("awayStateWhenFocusLostInVREnabled", DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED), _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), @@ -2497,6 +2501,17 @@ void Application::initialize(const QCommandLineParser &parser) { DependencyManager::get()->setPrecisionPicking(rayPickID, value); }); + // Setup the camera clipping ray pick + { + _prevCameraClippingEnabled = _cameraClippingEnabled.get(); + auto cameraRayPick = std::make_shared(Vectors::ZERO, -Vectors::UP, + PickFilter(PickScriptingInterface::getPickEntities() | + PickScriptingInterface::getPickLocalEntities()), + MyAvatar::ZOOM_MAX, 0.0f, _prevCameraClippingEnabled); + cameraRayPick->parentTransform = std::make_shared(); + _cameraClippingRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, cameraRayPick); + } + BillboardModeHelpers::setBillboardRotationOperator([](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos, bool rotate90x) { const glm::quat ROTATE_90X = glm::angleAxis(-(float)M_PI_2, Vectors::RIGHT); @@ -3682,9 +3697,7 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("updateCamera"); - glm::vec3 boomOffset; auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; // The render mode is default or mirror if the camera is in mirror mode, assigned further below renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; @@ -3723,6 +3736,16 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); _myCamera.setPosition(extractTranslation(worldCameraMat)); } else { + float boomLength = myAvatar->getBoomLength(); + if (getCameraClippingEnabled()) { + auto result = + DependencyManager::get()->getPrevPickResultTyped(_cameraClippingRayPickID); + if (result && result->doesIntersect()) { + const float CAMERA_CLIPPING_EPSILON = 0.1f; + boomLength = std::min(boomLength, result->distance - CAMERA_CLIPPING_EPSILON); + } + } + glm::vec3 boomOffset = myAvatar->getModelScale() * boomLength * -IDENTITY_FORWARD; _thirdPersonHMDCameraBoomValid = false; if (mode == CAMERA_MODE_THIRD_PERSON) { _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); @@ -3800,7 +3823,19 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { _myCamera.update(); } - renderArgs._cameraMode = (int8_t)_myCamera.getMode(); + renderArgs._cameraMode = (int8_t)mode; + + const bool shouldEnableCameraClipping = + (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_LOOK_AT || mode == CAMERA_MODE_SELFIE) && !isHMDMode() && + getCameraClippingEnabled(); + if (_prevCameraClippingEnabled != shouldEnableCameraClipping) { + if (shouldEnableCameraClipping) { + DependencyManager::get()->enablePick(_cameraClippingRayPickID); + } else { + DependencyManager::get()->disablePick(_cameraClippingRayPickID); + } + _prevCameraClippingEnabled = shouldEnableCameraClipping; + } } void Application::runTests() { @@ -3815,6 +3850,16 @@ void Application::setFieldOfView(float fov) { } } +void Application::setCameraClippingEnabled(bool enabled) { + _cameraClippingEnabled.set(enabled); + _prevCameraClippingEnabled = enabled; + if (enabled) { + DependencyManager::get()->enablePick(_cameraClippingRayPickID); + } else { + DependencyManager::get()->disablePick(_cameraClippingRayPickID); + } +} + void Application::setHMDTabletScale(float hmdTabletScale) { _hmdTabletScale.set(hmdTabletScale); } @@ -3841,6 +3886,11 @@ void Application::setPreferAvatarFingerOverStylus(bool value) { _preferAvatarFingerOverStylusSetting.set(value); } +void Application::setShowGraphicsIcon(bool value) { + _showGraphicsIconSetting.set(value); + DependencyManager::get< MessagesClient >()->sendLocalMessage("Overte-ShowGraphicsIconChanged", ""); +} + void Application::setPreferredCursor(const QString& cursorName) { qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; diff --git a/interface/src/Application.h b/interface/src/Application.h index f3cbd351eb..2c7a97a5eb 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -246,6 +246,9 @@ class Application : public QApplication, float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); + bool getCameraClippingEnabled() { return _cameraClippingEnabled.get(); } + void setCameraClippingEnabled(bool enabled); + float getHMDTabletScale() { return _hmdTabletScale.get(); } void setHMDTabletScale(float hmdTabletScale); float getDesktopTabletScale() { return _desktopTabletScale.get(); } @@ -263,6 +266,9 @@ class Application : public QApplication, bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } void setPreferAvatarFingerOverStylus(bool value); + bool getShowGraphicsIcon() { return _showGraphicsIconSetting.get(); } + void setShowGraphicsIcon(bool value); + bool getMiniTabletEnabled() { return _miniTabletEnabledSetting.get(); } void setMiniTabletEnabled(bool enabled); @@ -716,6 +722,7 @@ private slots: Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; + Setting::Handle _cameraClippingEnabled; Setting::Handle _hmdTabletScale; Setting::Handle _desktopTabletScale; Setting::Handle _firstRun; @@ -723,6 +730,7 @@ private slots: Setting::Handle _hmdTabletBecomesToolbarSetting; Setting::Handle _preferStylusOverLaserSetting; Setting::Handle _preferAvatarFingerOverStylusSetting; + Setting::Handle _showGraphicsIconSetting; Setting::Handle _constrainToolbarPosition; Setting::Handle _awayStateWhenFocusLostInVREnabled; Setting::Handle _preferredCursor; @@ -889,5 +897,8 @@ private slots: DiscordPresence* _discordPresence{ nullptr }; bool _profilingInitialized { false }; + + bool _prevCameraClippingEnabled { false }; + unsigned int _cameraClippingRayPickID; }; #endif // hifi_Application_h diff --git a/interface/src/CameraRootTransformNode.cpp b/interface/src/CameraRootTransformNode.cpp new file mode 100644 index 0000000000..596bdab3d3 --- /dev/null +++ b/interface/src/CameraRootTransformNode.cpp @@ -0,0 +1,48 @@ +// +// Created by HifiExperiments on 10/30/2024 +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "CameraRootTransformNode.h" + +#include "Application.h" +#include "DependencyManager.h" +#include "avatar/AvatarManager.h" +#include "avatar/MyAvatar.h" + +Transform CameraRootTransformNode::getTransform() { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + glm::vec3 pos; + glm::quat ori; + + CameraMode mode = qApp->getCamera().getMode(); + if (mode == CAMERA_MODE_FIRST_PERSON || mode == CAMERA_MODE_THIRD_PERSON) { + pos = myAvatar->getDefaultEyePosition(); + ori = myAvatar->getHeadOrientation(); + } else if (mode == CAMERA_MODE_FIRST_PERSON_LOOK_AT) { + pos = myAvatar->getCameraEyesPosition(0.0f); + ori = myAvatar->getLookAtRotation(); + } else { + ori = myAvatar->getLookAtRotation(); + pos = myAvatar->getLookAtPivotPoint(); + + if (mode == CAMERA_MODE_SELFIE) { + ori = ori * glm::angleAxis(PI, ori * Vectors::UP); + } + } + + ori = ori * glm::angleAxis(-PI / 2.0f, Vectors::RIGHT); + + glm::vec3 scale = glm::vec3(myAvatar->scaleForChildren()); + return Transform(ori, scale, pos); +} + +QVariantMap CameraRootTransformNode::toVariantMap() const { + QVariantMap map; + map["joint"] = "CameraRoot"; + return map; +} diff --git a/interface/src/CameraRootTransformNode.h b/interface/src/CameraRootTransformNode.h new file mode 100644 index 0000000000..6a0f58f42e --- /dev/null +++ b/interface/src/CameraRootTransformNode.h @@ -0,0 +1,20 @@ +// +// Created by HifiExperiments on 10/30/2024 +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_CameraRootTransformNode_h +#define hifi_CameraRootTransformNode_h + +#include "TransformNode.h" + +class CameraRootTransformNode : public TransformNode { +public: + CameraRootTransformNode() {} + Transform getTransform() override; + QVariantMap toVariantMap() const override; +}; + +#endif // hifi_CameraRootTransformNode_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1779d64dc6..cbe2b8e77f 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -75,6 +75,8 @@ #include "WarningsSuppression.h" #include "ScriptPermissions.h" +#include "Application.h" + using namespace std; const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f; @@ -226,6 +228,7 @@ MyAvatar::MyAvatar(QThread* thread) : _scaleSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "scale", _targetScale), _yawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "yawSpeed", _yawSpeed), _hmdYawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hmdYawSpeed", _hmdYawSpeed), + _cameraSensitivitySetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "cameraSensitivity", qApp->getCamera().getSensitivity()), _pitchSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "pitchSpeed", _pitchSpeed), _fullAvatarURLSetting(QStringList() << SETTINGS_FULL_PRIVATE_GROUP_NAME << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()), @@ -1324,6 +1327,7 @@ void MyAvatar::saveData() { _scaleSetting.set(_targetScale); _yawSpeedSetting.set(_yawSpeed); _hmdYawSpeedSetting.set(_hmdYawSpeed); + _cameraSensitivitySetting.set(getCameraSensitivity()); _pitchSpeedSetting.set(_pitchSpeed); // only save the fullAvatarURL if it has not been overwritten on command line @@ -2084,6 +2088,7 @@ void MyAvatar::loadData() { _yawSpeed = _yawSpeedSetting.get(_yawSpeed); _hmdYawSpeed = _hmdYawSpeedSetting.get(_hmdYawSpeed); + setCameraSensitivity(_cameraSensitivitySetting.get(getCameraSensitivity())); _pitchSpeed = _pitchSpeedSetting.get(_pitchSpeed); _prefOverrideAnimGraphUrl.set(_animGraphURLSetting.get().toString()); @@ -7002,3 +7007,11 @@ void MyAvatar::resetPointAt() { POINT_BLEND_LINEAR_ALPHA_NAME, POINT_ALPHA_BLENDING); } } + +float MyAvatar::getCameraSensitivity() const { + return qApp->getCamera().getSensitivity(); +} + +void MyAvatar::setCameraSensitivity(float cameraSensitivity) { + qApp->getCamera().setSensitivity(cameraSensitivity); +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 2777d2c82f..a3586db6db 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1407,6 +1407,9 @@ class MyAvatar : public Avatar { float getHMDYawSpeed() const { return _hmdYawSpeed; } void setHMDYawSpeed(float speed) { _hmdYawSpeed = speed; } + float getCameraSensitivity() const; + void setCameraSensitivity(float cameraSensitivity); + static const float ZOOM_MIN; static const float ZOOM_MAX; static const float ZOOM_DEFAULT; @@ -3007,6 +3010,7 @@ private slots: Setting::Handle _scaleSetting; Setting::Handle _yawSpeedSetting; Setting::Handle _hmdYawSpeedSetting; + Setting::Handle _cameraSensitivitySetting; Setting::Handle _pitchSpeedSetting; Setting::Handle _fullAvatarURLSetting; Setting::Handle _fullAvatarModelNameSetting; diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index 5323c52faf..221b0fcb63 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -21,6 +21,7 @@ #include "ParabolaPick.h" #include "CollisionPick.h" +#include "CameraRootTransformNode.h" #include "SpatialParentFinder.h" #include "PickTransformNode.h" #include "MouseTransformNode.h" @@ -537,6 +538,9 @@ void PickScriptingInterface::setParentTransform(std::shared_ptr pick, } else if (joint == "Avatar") { pick->parentTransform = std::make_shared(); return; + } else if (joint == "CameraRoot") { + pick->parentTransform = std::make_shared(); + return; } else { parentUuid = myAvatar->getSessionUUID(); parentJointIndex = myAvatar->getJointIndex(joint); diff --git a/interface/src/scripting/RenderScriptingInterface.cpp b/interface/src/scripting/RenderScriptingInterface.cpp index e126a5734e..db3e398547 100644 --- a/interface/src/scripting/RenderScriptingInterface.cpp +++ b/interface/src/scripting/RenderScriptingInterface.cpp @@ -342,6 +342,13 @@ void RenderScriptingInterface::setVerticalFieldOfView(float fieldOfView) { } } +void RenderScriptingInterface::setCameraClippingEnabled(bool enabled) { + if (qApp->getCameraClippingEnabled() != enabled) { + qApp->setCameraClippingEnabled(enabled); + emit settingsChanged(); + } +} + QStringList RenderScriptingInterface::getScreens() const { QStringList screens; diff --git a/interface/src/scripting/RenderScriptingInterface.h b/interface/src/scripting/RenderScriptingInterface.h index 56b474cf31..97d7868a96 100644 --- a/interface/src/scripting/RenderScriptingInterface.h +++ b/interface/src/scripting/RenderScriptingInterface.h @@ -37,6 +37,7 @@ * they're disabled. * @property {integer} antialiasingMode - The active anti-aliasing mode. * @property {number} viewportResolutionScale - The view port resolution scale, > 0.0. + * @property {boolean} cameraClippingEnabled - true if third person camera clipping is enabled, false if it's disabled. */ class RenderScriptingInterface : public QObject { Q_OBJECT @@ -49,6 +50,7 @@ class RenderScriptingInterface : public QObject { Q_PROPERTY(AntialiasingConfig::Mode antialiasingMode READ getAntialiasingMode WRITE setAntialiasingMode NOTIFY settingsChanged) Q_PROPERTY(float viewportResolutionScale READ getViewportResolutionScale WRITE setViewportResolutionScale NOTIFY settingsChanged) Q_PROPERTY(float verticalFieldOfView READ getVerticalFieldOfView WRITE setVerticalFieldOfView NOTIFY settingsChanged) + Q_PROPERTY(bool cameraClippingEnabled READ getCameraClippingEnabled WRITE setCameraClippingEnabled NOTIFY settingsChanged) public: RenderScriptingInterface(); @@ -261,7 +263,21 @@ public slots: * @function Render.setVerticalFieldOfView * @param {number} fieldOfView - The vertical field of view in degrees to set. */ - void setVerticalFieldOfView( float fieldOfView ); + void setVerticalFieldOfView(float fieldOfView); + + /*@jsdoc + * Gets whether or not third person camera clipping is enabled. + * @function Render.getCameraClippingEnabled + * @returns {boolean} true if camera clipping is enabled, false if it's disabled. + */ + bool getCameraClippingEnabled() { return qApp->getCameraClippingEnabled(); } + + /*@jsdoc + * Sets whether or not third person camera clipping is enabled. + * @function Render.setCameraClippingEnabled + * @param {boolean} enabled - true to enable third person camera clipping, false to disable. + */ + void setCameraClippingEnabled(bool enabled); signals: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 65e4daa5e0..623aede2fa 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -225,7 +225,13 @@ void setupPreferences() { preferences->addPreference(delaySlider); } - static const QString VIEW_CATEGORY{ "View" }; + { + auto getter = []() -> bool { return qApp->getShowGraphicsIcon(); }; + auto setter = [](bool value) { qApp->setShowGraphicsIcon(value); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "Show Graphics icon on tablet and toolbar", getter, setter)); + } + + static const QString VIEW_CATEGORY { "View" }; { auto getter = [myAvatar]()->float { return myAvatar->getRealWorldFieldOfView(); }; auto setter = [myAvatar](float value) { myAvatar->setRealWorldFieldOfView(value); }; @@ -243,6 +249,11 @@ void setupPreferences() { preference->setStep(1); preferences->addPreference(preference); } + { + auto getter = []()->bool { return qApp->getCameraClippingEnabled(); }; + auto setter = [](bool value) { qApp->setCameraClippingEnabled(value); }; + preferences->addPreference(new CheckPreference(VIEW_CATEGORY, "Enable 3rd Person Camera Clipping?", getter, setter)); + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 4b63dcb932..fb22db3ce0 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -994,9 +994,9 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const QString statsFormat = QString("(%1 Kbps, %2 Hz)"); if (!renderedDisplayName.isEmpty()) { - statsFormat.prepend(" - "); + statsFormat.append("\n"); } - renderedDisplayName += statsFormat.arg(QString::number(kilobitsPerSecond, 'f', 2)).arg(getReceiveRate()); + renderedDisplayName = statsFormat.arg(QString::number(kilobitsPerSecond, 'f', 2)).arg(getReceiveRate()) + renderedDisplayName; } // Compute display name extent/position offset @@ -1010,18 +1010,18 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const // Compute background position/size static const float SLIGHTLY_IN_FRONT = 0.1f; static const float BORDER_RELATIVE_SIZE = 0.1f; - static const float BEVEL_FACTOR = 0.1f; + // static const float BEVEL_FACTOR = 0.1f; const int border = BORDER_RELATIVE_SIZE * nameDynamicRect.height(); - const int left = text_x - border; - const int bottom = text_y - border; - const int width = nameDynamicRect.width() + 2.0f * border; + // FIXME: Beveled box is broken + // const int left = text_x - border; + // const int bottom = text_y - border; + // const int width = nameDynamicRect.width() + 2.0f * border; const int height = nameDynamicRect.height() + 2.0f * border; - const int bevelDistance = BEVEL_FACTOR * height; + // const int bevelDistance = BEVEL_FACTOR * height; // Display name and background colors glm::vec4 textColor(0.93f, 0.93f, 0.93f, _displayNameAlpha); - glm::vec4 backgroundColor(0.2f, 0.2f, 0.2f, - (_displayNameAlpha / DISPLAYNAME_ALPHA) * DISPLAYNAME_BACKGROUND_ALPHA); + glm::vec4 backgroundColor(0.2f, 0.2f, 0.2f,(_displayNameAlpha / DISPLAYNAME_ALPHA) * DISPLAYNAME_BACKGROUND_ALPHA); // Compute display name transform auto textTransform = calculateDisplayNameTransform(view, textPosition); @@ -1029,12 +1029,11 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const textTransform.postScale(1.0f / height); batch.setModelTransform(textTransform); - { - PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); - DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true, forward); - DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, - bevelDistance, backgroundColor, _nameRectGeometryID); - } + // { + // PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); + // DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true, forward); + // DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, bevelDistance, backgroundColor, _nameRectGeometryID); + // } // Render actual name QByteArray nameUTF8 = renderedDisplayName.toLocal8Bit(); @@ -1044,7 +1043,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const batch.setModelTransform(textTransform); { PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderText"); - displayNameRenderer->draw(batch, { nameUTF8.data(), textColor, { text_x, -text_y }, glm::vec2(-1.0f), forward }); + displayNameRenderer->draw(batch, { nameUTF8.data(), textColor, { text_x, -text_y }, glm::vec2(-1.0f), TextAlignment::LEFT, forward }); } } } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 178e122c32..593a3c020b 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -4,6 +4,7 @@ // // Created by Clement on 4/22/15. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -39,46 +40,46 @@ ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { if (_stage) { if (!LightStage::isIndexInvalid(_sunIndex)) { - _stage->removeLight(_sunIndex); + _stage->removeElement(_sunIndex); _sunIndex = INVALID_INDEX; } if (!LightStage::isIndexInvalid(_ambientIndex)) { - _stage->removeLight(_ambientIndex); + _stage->removeElement(_ambientIndex); _ambientIndex = INVALID_INDEX; } } if (_backgroundStage) { if (!BackgroundStage::isIndexInvalid(_backgroundIndex)) { - _backgroundStage->removeBackground(_backgroundIndex); + _backgroundStage->removeElement(_backgroundIndex); _backgroundIndex = INVALID_INDEX; } } if (_hazeStage) { if (!HazeStage::isIndexInvalid(_hazeIndex)) { - _hazeStage->removeHaze(_hazeIndex); + _hazeStage->removeElement(_hazeIndex); _hazeIndex = INVALID_INDEX; } } if (_bloomStage) { if (!BloomStage::isIndexInvalid(_bloomIndex)) { - _bloomStage->removeBloom(_bloomIndex); + _bloomStage->removeElement(_bloomIndex); _bloomIndex = INVALID_INDEX; } } if (_tonemappingStage) { if (!TonemappingStage::isIndexInvalid(_tonemappingIndex)) { - _tonemappingStage->removeTonemapping(_tonemappingIndex); + _tonemappingStage->removeElement(_tonemappingIndex); _tonemappingIndex = INVALID_INDEX; } } if (_ambientOcclusionStage) { if (!AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) { - _ambientOcclusionStage->removeAmbientOcclusion(_ambientOcclusionIndex); + _ambientOcclusionStage->removeElement(_ambientOcclusionIndex); _ambientOcclusionIndex = INVALID_INDEX; } } @@ -123,7 +124,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { // Sun if (_needSunUpdate) { if (LightStage::isIndexInvalid(_sunIndex)) { - _sunIndex = _stage->addLight(_sunLight); + _sunIndex = _stage->addElement(_sunLight); } else { _stage->updateLightArrayBuffer(_sunIndex); } @@ -136,7 +137,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { if (_needAmbientUpdate) { if (LightStage::isIndexInvalid(_ambientIndex)) { - _ambientIndex = _stage->addLight(_ambientLight); + _ambientIndex = _stage->addElement(_ambientLight); } else { _stage->updateLightArrayBuffer(_ambientIndex); } @@ -149,7 +150,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { if (_needBackgroundUpdate) { if (BackgroundStage::isIndexInvalid(_backgroundIndex)) { - _backgroundIndex = _backgroundStage->addBackground(_background); + _backgroundIndex = _backgroundStage->addElement(_background); } _needBackgroundUpdate = false; } @@ -158,7 +159,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needHazeUpdate) { if (HazeStage::isIndexInvalid(_hazeIndex)) { - _hazeIndex = _hazeStage->addHaze(_haze); + _hazeIndex = _hazeStage->addElement(_haze); } _needHazeUpdate = false; } @@ -167,7 +168,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needBloomUpdate) { if (BloomStage::isIndexInvalid(_bloomIndex)) { - _bloomIndex = _bloomStage->addBloom(_bloom); + _bloomIndex = _bloomStage->addElement(_bloom); } _needBloomUpdate = false; } @@ -176,7 +177,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needTonemappingUpdate) { if (TonemappingStage::isIndexInvalid(_tonemappingIndex)) { - _tonemappingIndex = _tonemappingStage->addTonemapping(_tonemapping); + _tonemappingIndex = _tonemappingStage->addElement(_tonemapping); } _needTonemappingUpdate = false; } @@ -185,7 +186,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needAmbientOcclusionUpdate) { if (AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) { - _ambientOcclusionIndex = _ambientOcclusionStage->addAmbientOcclusion(_ambientOcclusion); + _ambientOcclusionIndex = _ambientOcclusionStage->addElement(_ambientOcclusion); } _needAmbientOcclusionUpdate = false; } @@ -205,9 +206,9 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { } if (_skyboxMode == COMPONENT_MODE_DISABLED) { - _backgroundStage->_currentFrame.pushBackground(INVALID_INDEX); + _backgroundStage->_currentFrame.pushElement(INVALID_INDEX); } else if (_skyboxMode == COMPONENT_MODE_ENABLED) { - _backgroundStage->_currentFrame.pushBackground(_backgroundIndex); + _backgroundStage->_currentFrame.pushElement(_backgroundIndex); } if (_ambientLightMode == COMPONENT_MODE_DISABLED) { @@ -218,25 +219,25 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { // Haze only if the mode is not inherit, as the model deals with on/off if (_hazeMode != COMPONENT_MODE_INHERIT) { - _hazeStage->_currentFrame.pushHaze(_hazeIndex); + _hazeStage->_currentFrame.pushElement(_hazeIndex); } if (_bloomMode == COMPONENT_MODE_DISABLED) { - _bloomStage->_currentFrame.pushBloom(INVALID_INDEX); + _bloomStage->_currentFrame.pushElement(INVALID_INDEX); } else if (_bloomMode == COMPONENT_MODE_ENABLED) { - _bloomStage->_currentFrame.pushBloom(_bloomIndex); + _bloomStage->_currentFrame.pushElement(_bloomIndex); } if (_tonemappingMode == COMPONENT_MODE_DISABLED) { - _tonemappingStage->_currentFrame.pushTonemapping(0); // Use the fallback tonemapping for "off" + _tonemappingStage->_currentFrame.pushElement(0); // Use the fallback tonemapping for "off" } else if (_tonemappingMode == COMPONENT_MODE_ENABLED) { - _tonemappingStage->_currentFrame.pushTonemapping(_tonemappingIndex); + _tonemappingStage->_currentFrame.pushElement(_tonemappingIndex); } if (_ambientOcclusionMode == COMPONENT_MODE_DISABLED) { - _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX); + _ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX); } else if (_ambientOcclusionMode == COMPONENT_MODE_ENABLED) { - _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(_ambientOcclusionIndex); + _ambientOcclusionStage->_currentFrame.pushElement(_ambientOcclusionIndex); } } diff --git a/libraries/entities/src/EntityItemPropertiesDocs.cpp b/libraries/entities/src/EntityItemPropertiesDocs.cpp index 8f8959165c..8195161b77 100644 --- a/libraries/entities/src/EntityItemPropertiesDocs.cpp +++ b/libraries/entities/src/EntityItemPropertiesDocs.cpp @@ -935,7 +935,9 @@ * * @typedef {object} Entities.EntityProperties-Image * @property {Vec3} dimensions=0.1,0.1,0.01 - The dimensions of the entity. - * @property {string} imageURL="" - The URL of the image to use. + * @property {string} imageURL="" - The URL of the image to use. It can also contain a base64 encoded image, in the same format as glTF. + * For network transmitted entities there's about 1000-character limit for the length of this field. For base64 image + * the property string needs to begin with `data:image/png;base64,`, `data:image/jpeg;base64,` or `data:image/webp;base64,`. * @property {boolean} emissive=false - true if the image should be emissive (unlit), false if it * shouldn't. * @property {boolean} keepAspectRatio=true - true if the image should maintain its aspect ratio, diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index ab3233e639..2b14e417df 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -2,9 +2,9 @@ // EntityTypes.h // libraries/entities/src // -// Created by Brad Hefta-Gaub on 12/4/13. +// Created by Brad Hefta-Gaub on December 4th, 2013. // Copyright 2013 High Fidelity, Inc. -// Copyright 2023 Overte e.V. +// Copyright 2023-2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -91,7 +91,7 @@ class EntityTypes { * "Material"Modifies the existing materials on entities and avatars. * {@link Entities.EntityProperties-Material|EntityProperties-Material} * "Sound"Plays a sound. - * {@link Entities.EntityProperties-Material|EntityProperties-Sound} + * {@link Entities.EntityProperties-Sound|EntityProperties-Sound} * * * @typedef {string} Entities.EntityType diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 3cf8184913..a4e6bf6e05 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -288,6 +288,10 @@ class Batch { _glUniformMatrix3fv(location, 1, false, glm::value_ptr(v)); } + void _glUniform(int location, const glm::mat4& v) { + _glUniformMatrix4fv(location, 1, false, glm::value_ptr(v)); + } + // Maybe useful but shoudln't be public. Please convince me otherwise // Well porting to gles i need it... void runLambda(std::function f); diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 907f9ff392..a6f527e657 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -24,6 +24,7 @@ #include "Forward.h" #include "Resource.h" #include "Metric.h" +#include "SerDes.h" const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; @@ -91,6 +92,37 @@ class SphericalHarmonics { }; typedef std::shared_ptr< SphericalHarmonics > SHPointer; + +inline DataSerializer &operator<<(DataSerializer &ser, const SphericalHarmonics &h) { + DataSerializer::SizeTracker tracker(ser); + + ser << h.L00 << h.spare0; + ser << h.L1m1 << h.spare1; + ser << h.L10 << h.spare2; + ser << h.L11 << h.spare3; + ser << h.L2m2 << h.spare4; + ser << h.L2m1 << h.spare5; + ser << h.L20 << h.spare6; + ser << h.L21 << h.spare7; + ser << h.L22 << h.spare8; + return ser; +} + +inline DataDeserializer &operator>>(DataDeserializer &des, SphericalHarmonics &h) { + DataDeserializer::SizeTracker tracker(des); + + des >> h.L00 >> h.spare0; + des >> h.L1m1 >> h.spare1; + des >> h.L10 >> h.spare2; + des >> h.L11 >> h.spare3; + des >> h.L2m2 >> h.spare4; + des >> h.L2m1 >> h.spare5; + des >> h.L20 >> h.spare6; + des >> h.L21 >> h.spare7; + des >> h.L22 >> h.spare8; + return des; +} + class Sampler { public: @@ -136,7 +168,7 @@ class Sampler { uint8 _wrapModeU = WRAP_REPEAT; uint8 _wrapModeV = WRAP_REPEAT; uint8 _wrapModeW = WRAP_REPEAT; - + uint8 _mipOffset = 0; uint8 _minMip = 0; uint8 _maxMip = MAX_MIP_LEVEL; @@ -193,6 +225,35 @@ class Sampler { friend class Deserializer; }; +inline DataSerializer &operator<<(DataSerializer &ser, const Sampler::Desc &d) { + DataSerializer::SizeTracker tracker(ser); + ser << d._borderColor; + ser << d._maxAnisotropy; + ser << d._filter; + ser << d._comparisonFunc; + ser << d._wrapModeU; + ser << d._wrapModeV; + ser << d._wrapModeW; + ser << d._mipOffset; + ser << d._minMip; + ser << d._maxMip; + return ser; +} + +inline DataDeserializer &operator>>(DataDeserializer &dsr, Sampler::Desc &d) { + DataDeserializer::SizeTracker tracker(dsr); + dsr >> d._borderColor; + dsr >> d._maxAnisotropy; + dsr >> d._filter; + dsr >> d._comparisonFunc; + dsr >> d._wrapModeU; + dsr >> d._wrapModeV; + dsr >> d._wrapModeW; + dsr >> d._mipOffset; + dsr >> d._minMip; + dsr >> d._maxMip; + return dsr; +} enum class TextureUsageType : uint8 { RENDERBUFFER, // Used as attachments to a framebuffer RESOURCE, // Resource textures, like materials... subject to memory manipulation @@ -230,7 +291,7 @@ class Texture : public Resource { NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 - NUM_FLAGS, + NUM_FLAGS, }; typedef std::bitset Flags; @@ -478,7 +539,7 @@ class Texture : public Resource { uint16 evalMipDepth(uint16 level) const { return std::max(_depth >> level, 1); } // The true size of an image line or surface depends on the format, tiling and padding rules - // + // // Here are the static function to compute the different sizes from parametered dimensions and format // Tile size must be a power of 2 static uint16 evalTiledPadding(uint16 length, int tile) { int tileMinusOne = (tile - 1); return (tileMinusOne - (length + tileMinusOne) % tile); } @@ -507,7 +568,7 @@ class Texture : public Resource { uint32 evalMipFaceNumTexels(uint16 level) const { return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); } uint32 evalMipNumTexels(uint16 level) const { return evalMipFaceNumTexels(level) * getNumFaces(); } - // For convenience assign a source name + // For convenience assign a source name const std::string& source() const { return _source; } void setSource(const std::string& source) { _source = source; } const std::string& sourceHash() const { return _sourceHash; } @@ -633,7 +694,7 @@ class Texture : public Resource { uint16 _maxMipLevel { 0 }; uint16 _minMip { 0 }; - + Type _type { TEX_1D }; Usage _usage; @@ -643,7 +704,7 @@ class Texture : public Resource { bool _isIrradianceValid = false; bool _defined = false; bool _important = false; - + static TexturePointer create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips); diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index c4b674a917..2a4d678208 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -18,6 +18,7 @@ #include #include "GPULogging.h" +#include "SerDes.h" using namespace gpu; @@ -27,71 +28,94 @@ using KtxStorage = Texture::KtxStorage; std::vector, std::shared_ptr>> KtxStorage::_cachedKtxFiles; std::mutex KtxStorage::_cachedKtxFilesMutex; + +/** + * @brief Payload for a KTX (texture) + * + * This contains a ready to use texture. This is both used for the local cache, and for baked textures. + * + * @note The usage for textures means breaking compatibility is a bad idea, and that the implementation + * should just keep on adding extra data at the bottom of the structure, and remain able to read old + * formats. In fact, version 1 KTX can be found in older baked assets. + */ struct GPUKTXPayload { using Version = uint8; static const std::string KEY; static const Version CURRENT_VERSION { 2 }; static const size_t PADDING { 2 }; - static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING }; + static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32_t) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING }; + static_assert(GPUKTXPayload::SIZE == 44, "Packing size may differ between platforms"); - static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned"); Sampler::Desc _samplerDesc; Texture::Usage _usage; TextureUsageType _usageType; glm::ivec2 _originalSize { 0, 0 }; - Byte* serialize(Byte* data) const { - *(Version*)data = CURRENT_VERSION; - data += sizeof(Version); + /** + * @brief Serialize the KTX payload + * + * @warning Be careful modifying this code, as it influences baked assets. + * Backwards compatibility must be maintained. + * + * @param ser Destination serializer + */ + void serialize(DataSerializer &ser) { - memcpy(data, &_samplerDesc, sizeof(Sampler::Desc)); - data += sizeof(Sampler::Desc); + ser << CURRENT_VERSION; - // We can't copy the bitset in Texture::Usage in a crossplateform manner - // So serialize it manually - uint32 usageData = _usage._flags.to_ulong(); - memcpy(data, &usageData, sizeof(uint32)); - data += sizeof(uint32); + ser << _samplerDesc; - memcpy(data, &_usageType, sizeof(TextureUsageType)); - data += sizeof(TextureUsageType); + uint32_t usageData = (uint32_t)_usage._flags.to_ulong(); + ser << usageData; + ser << ((uint8_t)_usageType); + ser << _originalSize; - memcpy(data, glm::value_ptr(_originalSize), sizeof(glm::ivec2)); - data += sizeof(glm::ivec2); + ser.addPadding(PADDING); - return data + PADDING; + assert(ser.length() == GPUKTXPayload::SIZE); } - bool unserialize(const Byte* data, size_t size) { - Version version = *(const Version*)data; - data += sizeof(Version); + /** + * @brief Deserialize the KTX payload + * + * @warning Be careful modifying this code, as it influences baked assets. + * Backwards compatibility must be maintained. + * + * @param dsr Deserializer object + * @return true Successful + * @return false Version check failed + */ + bool unserialize(DataDeserializer &dsr) { + Version version = 0; + uint32_t usageData = 0; + uint8_t usagetype = 0; + + dsr >> version; if (version > CURRENT_VERSION) { // If we try to load a version that we don't know how to parse, // it will render incorrectly + qCWarning(gpulogging) << "KTX version" << version << "is newer than our own," << CURRENT_VERSION; + qCWarning(gpulogging) << dsr; return false; } - memcpy(&_samplerDesc, data, sizeof(Sampler::Desc)); - data += sizeof(Sampler::Desc); + dsr >> _samplerDesc; - // We can't copy the bitset in Texture::Usage in a crossplateform manner - // So unserialize it manually - uint32 usageData; - memcpy(&usageData, data, sizeof(uint32)); - _usage = Texture::Usage(usageData); - data += sizeof(uint32); + dsr >> usageData; + _usage = gpu::Texture::Usage(usageData); - memcpy(&_usageType, data, sizeof(TextureUsageType)); - data += sizeof(TextureUsageType); + dsr >> usagetype; + _usageType = (TextureUsageType)usagetype; if (version >= 2) { - memcpy(&_originalSize, data, sizeof(glm::ivec2)); - data += sizeof(glm::ivec2); + dsr >> _originalSize; } + dsr.skipPadding(PADDING); + return true; } @@ -103,7 +127,8 @@ struct GPUKTXPayload { auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); if (found != keyValues.end()) { auto value = found->_value; - return payload.unserialize(value.data(), value.size()); + DataDeserializer dsr(value.data(), value.size()); + return payload.unserialize(dsr); } return false; } @@ -123,29 +148,24 @@ struct IrradianceKTXPayload { SphericalHarmonics _irradianceSH; - Byte* serialize(Byte* data) const { - *(Version*)data = CURRENT_VERSION; - data += sizeof(Version); - - memcpy(data, &_irradianceSH, sizeof(SphericalHarmonics)); - data += sizeof(SphericalHarmonics); - - return data + PADDING; + void serialize(DataSerializer &ser) const { + ser << CURRENT_VERSION; + ser << _irradianceSH; + ser.addPadding(PADDING); } - bool unserialize(const Byte* data, size_t size) { - if (size != SIZE) { + bool unserialize(DataDeserializer &des) { + Version version; + if (des.length() != SIZE) { return false; } - Version version = *(const Version*)data; + des >> version; if (version != CURRENT_VERSION) { return false; } - data += sizeof(Version); - - memcpy(&_irradianceSH, data, sizeof(SphericalHarmonics)); + des >> _irradianceSH; return true; } @@ -157,7 +177,8 @@ struct IrradianceKTXPayload { auto found = std::find_if(keyValues.begin(), keyValues.end(), isIrradianceKTX); if (found != keyValues.end()) { auto value = found->_value; - return payload.unserialize(value.data(), value.size()); + DataDeserializer des(value.data(), value.size()); + return payload.unserialize(des); } return false; } @@ -467,7 +488,9 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec gpuKeyval._originalSize = originalSize; Byte keyvalPayload[GPUKTXPayload::SIZE]; - gpuKeyval.serialize(keyvalPayload); + DataSerializer ser(keyvalPayload, sizeof(keyvalPayload)); + + gpuKeyval.serialize(ser); ktx::KeyValues keyValues; keyValues.emplace_back(GPUKTXPayload::KEY, (uint32)GPUKTXPayload::SIZE, (ktx::Byte*) &keyvalPayload); @@ -477,7 +500,8 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec irradianceKeyval._irradianceSH = *texture.getIrradiance(); Byte irradianceKeyvalPayload[IrradianceKTXPayload::SIZE]; - irradianceKeyval.serialize(irradianceKeyvalPayload); + DataSerializer ser(irradianceKeyvalPayload, sizeof(irradianceKeyvalPayload)); + irradianceKeyval.serialize(ser); keyValues.emplace_back(IrradianceKTXPayload::KEY, (uint32)IrradianceKTXPayload::SIZE, (ktx::Byte*) &irradianceKeyvalPayload); } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 840fa50a0a..e2d4822543 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -254,6 +254,18 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs if (url.scheme() == RESOURCE_SCHEME) { return getResourceTexture(url); } + + QString urlString = url.toString(); + if (content.isEmpty() && (urlString.startsWith("data:image/jpeg;base64,") + || urlString.startsWith("data:image/png;base64,") + || urlString.startsWith("data:image/webp;base64,"))) { + QString binaryUrl = urlString.split(",")[1]; + auto decodedContent = binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); + if (!decodedContent.isEmpty()) { + return getTexture(url, type, decodedContent, maxNumPixels, sourceChannel); + } + } + QString decodedURL = QUrl::fromPercentEncoding(url.toEncoded()); if (decodedURL.startsWith("{")) { return getTextureByUUID(decodedURL); diff --git a/libraries/model-serializers/src/GLTFSerializer.cpp b/libraries/model-serializers/src/GLTFSerializer.cpp index 1440fb22d2..d4cee367d3 100644 --- a/libraries/model-serializers/src/GLTFSerializer.cpp +++ b/libraries/model-serializers/src/GLTFSerializer.cpp @@ -1335,7 +1335,7 @@ HFMTexture GLTFSerializer::getHFMTexture(const cgltf_texture *texture) { hfmTex.filename = textureUrl.toEncoded().append(QString::number(imageIndex).toUtf8()); } - if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,") || url.contains("data:image/webp;base64,")) { + if (url.startsWith("data:image/jpeg;base64,") || url.startsWith("data:image/png;base64,") || url.startsWith("data:image/webp;base64,")) { hfmTex.content = requestEmbeddedData(url); } } diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index c13d58226b..3745582728 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -17,6 +17,7 @@ #include "OctreeLogging.h" #include "NumericalConstants.h" #include +#include "SerDes.h" bool OctreePacketData::_debug = false; AtomicUIntStat OctreePacketData::_totalBytesOfOctalCodes { 0 }; @@ -847,10 +848,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QByteA } int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, AACube& result) { - aaCubeData cube; - memcpy(&cube, dataBytes, sizeof(aaCubeData)); - result = AACube(cube.corner, cube.scale); - return sizeof(aaCubeData); + DataDeserializer des(dataBytes, sizeof(aaCubeData)); + des >> result; + + return des.length(); } int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QRect& result) { diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 3162ab95e6..7b202ad625 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -356,16 +356,53 @@ void Procedural::prepare(gpu::Batch& batch, } } // Then fill in every reflections the new custom bindings - int customSlot = procedural::slot::uniform::Custom; + size_t customSlot = procedural::slot::uniform::Custom; + _slotMap.clear(); for (const auto& key : _data.uniforms.keys()) { - std::string uniformName = key.toLocal8Bit().data(); - for (auto reflection : allFragmentReflections) { - reflection->uniforms[uniformName] = customSlot; + bool isArrayUniform = false; + size_t numSlots = 0; + const QJsonValue& value = _data.uniforms[key]; + if (value.isDouble()) { + numSlots = 1; + } else if (value.isArray()) { + const QJsonArray valueArray = value.toArray(); + if (valueArray.size() > 0) { + if (valueArray[0].isArray()) { + const size_t valueLength = valueArray[0].toArray().size(); + size_t count = 0; + for (const QJsonValue& value : valueArray) { + if (value.isArray()) { + const QJsonArray innerValueArray = value.toArray(); + if (innerValueArray.size() == valueLength) { + if (valueLength == 3 || valueLength == 4 || valueLength == 9 || valueLength == 16) { + count++; + isArrayUniform = true; + } + } + } + } + numSlots = count; + } else if (valueArray[0].isDouble()) { + numSlots = 1; + } + } } - for (auto reflection : allVertexReflections) { - reflection->uniforms[uniformName] = customSlot; + + if (numSlots > 0) { + std::string uniformName = key.toLocal8Bit().data(); + std::string trueUniformName = uniformName; + if (isArrayUniform) { + trueUniformName += "[0]"; + } + for (auto reflection : allFragmentReflections) { + reflection->uniforms[trueUniformName] = customSlot; + } + for (auto reflection : allVertexReflections) { + reflection->uniforms[trueUniformName] = customSlot; + } + _slotMap[uniformName] = customSlot; + customSlot += numSlots; } - ++customSlot; } } @@ -448,59 +485,138 @@ void Procedural::prepare(gpu::Batch& batch, } } - void Procedural::setupUniforms() { _uniforms.clear(); // Set any userdata specified uniforms - int slot = procedural::slot::uniform::Custom; for (const auto& key : _data.uniforms.keys()) { - std::string uniformName = key.toLocal8Bit().data(); - QJsonValue value = _data.uniforms[key]; + const std::string uniformName = key.toLocal8Bit().data(); + auto slotItr = _slotMap.find(uniformName); + if (slotItr == _slotMap.end()) { + continue; + } + + const size_t slot = slotItr->second; + const QJsonValue& value = _data.uniforms[key]; if (value.isDouble()) { - float v = value.toDouble(); + const float v = value.toDouble(); _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform1f(slot, v); }); } else if (value.isArray()) { - auto valueArray = value.toArray(); - switch (valueArray.size()) { - case 0: - break; - - case 1: { - float v = valueArray[0].toDouble(); - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform1f(slot, v); }); - break; - } - - case 2: { - glm::vec2 v{ valueArray[0].toDouble(), valueArray[1].toDouble() }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform2f(slot, v.x, v.y); }); - break; - } - - case 3: { - glm::vec3 v{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform3f(slot, v.x, v.y, v.z); }); - break; - } - - default: - case 4: { - glm::vec4 v{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - valueArray[3].toDouble(), - }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform4f(slot, v.x, v.y, v.z, v.w); }); - break; - } + const QJsonArray valueArray = value.toArray(); + if (valueArray.size() > 0) { + if (valueArray[0].isArray()) { + const size_t valueLength = valueArray[0].toArray().size(); + std::vector vs; + vs.reserve(valueLength * valueArray.size()); + size_t count = 0; + for (const QJsonValue& value : valueArray) { + if (value.isArray()) { + const QJsonArray innerValueArray = value.toArray(); + if (innerValueArray.size() == valueLength) { + if (valueLength == 3 || valueLength == 4 || valueLength == 9 || valueLength == 16) { + for (size_t i = 0; i < valueLength; i++) { + vs.push_back(innerValueArray[i].toDouble()); + } + count++; + } + } + } + } + if (count > 0) { + switch (valueLength) { + case 3: { + _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniform3fv(slot, count, vs.data()); }); + break; + } + case 4: { + _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniform4fv(slot, count, vs.data()); }); + break; + } + case 9: { + _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniformMatrix3fv(slot, count, false, vs.data()); }); + break; + } + case 16: { + _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniformMatrix4fv(slot, count, false, vs.data()); }); + break; + } + default: + break; + } + } + } else if (valueArray[0].isDouble()) { + switch (valueArray.size()) { + case 1: { + const float v = valueArray[0].toDouble(); + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); + break; + } + case 2: { + const glm::vec2 v{ valueArray[0].toDouble(), valueArray[1].toDouble() }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); + break; + } + case 3: { + const glm::vec3 v{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); + break; + } + case 4: { + const glm::vec4 v{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + valueArray[3].toDouble(), + }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); + break; + } + case 9: { + const glm::mat3 m{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + valueArray[3].toDouble(), + valueArray[4].toDouble(), + valueArray[5].toDouble(), + valueArray[6].toDouble(), + valueArray[7].toDouble(), + valueArray[8].toDouble(), + }; + _uniforms.push_back([slot, m](gpu::Batch& batch) { batch._glUniform(slot, m); }); + break; + } + case 16: { + const glm::mat4 m{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + valueArray[3].toDouble(), + valueArray[4].toDouble(), + valueArray[5].toDouble(), + valueArray[6].toDouble(), + valueArray[7].toDouble(), + valueArray[8].toDouble(), + valueArray[9].toDouble(), + valueArray[10].toDouble(), + valueArray[11].toDouble(), + valueArray[12].toDouble(), + valueArray[13].toDouble(), + valueArray[14].toDouble(), + valueArray[15].toDouble(), + }; + _uniforms.push_back([slot, m](gpu::Batch& batch) { batch._glUniform(slot, m); }); + break; + } + default: + break; + } + } } } - slot++; } _uniforms.push_back([this](gpu::Batch& batch) { @@ -578,4 +694,4 @@ void graphics::ProceduralMaterial::initializeProcedural() { _procedural._transparentFragmentSource = gpu::Shader::getFragmentShaderSource(shader::render_utils::fragment::simple_procedural_translucent); _procedural._errorFallbackFragmentPath = ":" + QUrl("qrc:///shaders/errorShader.frag").path(); -} \ No newline at end of file +} diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 3ca2f41f0b..c1836095a7 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -190,6 +190,7 @@ struct Procedural { NetworkTexturePointer _channels[MAX_PROCEDURAL_TEXTURE_CHANNELS]; std::unordered_map _vertexReplacements; std::unordered_map _fragmentReplacements; + std::unordered_map _slotMap; std::unordered_map _proceduralPipelines; std::unordered_map _errorPipelines; diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index ab1acc3c91..1a76777888 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -4,6 +4,7 @@ // // Created by Niraj Venkat on 7/15/15. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -606,8 +607,8 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte graphics::AmbientOcclusionPointer ambientOcclusion; if (_debug) { ambientOcclusion = _debugAmbientOcclusion; - } else if (ambientOcclusionStage && ambientOcclusionFrame->_ambientOcclusions.size()) { - ambientOcclusion = ambientOcclusionStage->getAmbientOcclusion(ambientOcclusionFrame->_ambientOcclusions.front()); + } else if (ambientOcclusionStage && ambientOcclusionFrame->_elements.size()) { + ambientOcclusion = ambientOcclusionStage->getElement(ambientOcclusionFrame->_elements.front()); } if (!ambientOcclusion || !lightingModel->isAmbientOcclusionEnabled()) { diff --git a/libraries/render-utils/src/AmbientOcclusionStage.cpp b/libraries/render-utils/src/AmbientOcclusionStage.cpp index 6b3763a39c..55fa290ab7 100644 --- a/libraries/render-utils/src/AmbientOcclusionStage.cpp +++ b/libraries/render-utils/src/AmbientOcclusionStage.cpp @@ -7,50 +7,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "AmbientOcclusionStage.h" - -#include - -std::string AmbientOcclusionStage::_stageName { "AMBIENT_OCCLUSION_STAGE" }; -const AmbientOcclusionStage::Index AmbientOcclusionStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; - -AmbientOcclusionStage::Index AmbientOcclusionStage::findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const { - auto found = _ambientOcclusionMap.find(ambientOcclusion); - if (found != _ambientOcclusionMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } -} -AmbientOcclusionStage::Index AmbientOcclusionStage::addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) { - auto found = _ambientOcclusionMap.find(ambientOcclusion); - if (found == _ambientOcclusionMap.end()) { - auto ambientOcclusionId = _ambientOcclusions.newElement(ambientOcclusion); - // Avoid failing to allocate a ambientOcclusion, just pass - if (ambientOcclusionId != INVALID_INDEX) { - // Insert the ambientOcclusion and its index in the reverse map - _ambientOcclusionMap.insert(AmbientOcclusionMap::value_type(ambientOcclusion, ambientOcclusionId)); - } - return ambientOcclusionId; - } else { - return (*found).second; - } -} - -AmbientOcclusionStage::AmbientOcclusionPointer AmbientOcclusionStage::removeAmbientOcclusion(Index index) { - AmbientOcclusionPointer removed = _ambientOcclusions.freeElement(index); - if (removed) { - _ambientOcclusionMap.erase(removed); - } - return removed; -} - -AmbientOcclusionStageSetup::AmbientOcclusionStageSetup() {} +#include "AmbientOcclusionStage.h" -void AmbientOcclusionStageSetup::run(const render::RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(AmbientOcclusionStage::getName()); - if (!stage) { - renderContext->_scene->resetStage(AmbientOcclusionStage::getName(), std::make_shared()); - } -} +template <> +std::string render::PointerStage::_name { "AMBIENT_OCCLUSION_STAGE" }; diff --git a/libraries/render-utils/src/AmbientOcclusionStage.h b/libraries/render-utils/src/AmbientOcclusionStage.h index d5dee344ba..1b54a0828e 100644 --- a/libraries/render-utils/src/AmbientOcclusionStage.h +++ b/libraries/render-utils/src/AmbientOcclusionStage.h @@ -11,74 +11,17 @@ #ifndef hifi_render_utils_AmbientOcclusionStage_h #define hifi_render_utils_AmbientOcclusionStage_h -#include -#include -#include -#include -#include - -#include -#include #include +#include +#include // AmbientOcclusion stage to set up ambientOcclusion-related rendering tasks -class AmbientOcclusionStage : public render::Stage { -public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using AmbientOcclusionPointer = graphics::AmbientOcclusionPointer; - using AmbientOcclusions = render::indexed_container::IndexedPointerVector; - using AmbientOcclusionMap = std::unordered_map; - - using AmbientOcclusionIndices = std::vector; - - Index findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const; - Index addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion); - - AmbientOcclusionPointer removeAmbientOcclusion(Index index); - - bool checkAmbientOcclusionId(Index index) const { return _ambientOcclusions.checkIndex(index); } - - Index getNumAmbientOcclusions() const { return _ambientOcclusions.getNumElements(); } - Index getNumFreeAmbientOcclusions() const { return _ambientOcclusions.getNumFreeIndices(); } - Index getNumAllocatedAmbientOcclusions() const { return _ambientOcclusions.getNumAllocatedIndices(); } - - AmbientOcclusionPointer getAmbientOcclusion(Index ambientOcclusionId) const { - return _ambientOcclusions.get(ambientOcclusionId); - } - - AmbientOcclusions _ambientOcclusions; - AmbientOcclusionMap _ambientOcclusionMap; - - class Frame { - public: - Frame() {} - - void clear() { _ambientOcclusions.clear(); } - - void pushAmbientOcclusion(AmbientOcclusionStage::Index index) { _ambientOcclusions.emplace_back(index); } - - AmbientOcclusionStage::AmbientOcclusionIndices _ambientOcclusions; - }; - using FramePointer = std::shared_ptr; - - Frame _currentFrame; -}; +class AmbientOcclusionStage : public render::PointerStage {}; using AmbientOcclusionStagePointer = std::shared_ptr; -class AmbientOcclusionStageSetup { +class AmbientOcclusionStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - AmbientOcclusionStageSetup(); - void run(const render::RenderContextPointer& renderContext); - -protected: }; #endif diff --git a/libraries/render-utils/src/AssembleLightingStageTask.cpp b/libraries/render-utils/src/AssembleLightingStageTask.cpp index bc040582bd..0646f77d51 100644 --- a/libraries/render-utils/src/AssembleLightingStageTask.cpp +++ b/libraries/render-utils/src/AssembleLightingStageTask.cpp @@ -1,6 +1,7 @@ // // Created by Samuel Gateau on 2018/12/06 // Copyright 2013-2018 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/render-utils/src/BackgroundStage.cpp b/libraries/render-utils/src/BackgroundStage.cpp index 455f356a45..f3f287bdac 100644 --- a/libraries/render-utils/src/BackgroundStage.cpp +++ b/libraries/render-utils/src/BackgroundStage.cpp @@ -3,58 +3,20 @@ // // Created by Sam Gateau on 5/9/2017. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + #include "BackgroundStage.h" #include "DeferredLightingEffect.h" -#include - #include -std::string BackgroundStage::_stageName { "BACKGROUND_STAGE" }; -const BackgroundStage::Index BackgroundStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; - -BackgroundStage::Index BackgroundStage::findBackground(const BackgroundPointer& background) const { - auto found = _backgroundMap.find(background); - if (found != _backgroundMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } - -} - -BackgroundStage::Index BackgroundStage::addBackground(const BackgroundPointer& background) { - - auto found = _backgroundMap.find(background); - if (found == _backgroundMap.end()) { - auto backgroundId = _backgrounds.newElement(background); - // Avoid failing to allocate a background, just pass - if (backgroundId != INVALID_INDEX) { - - // Insert the background and its index in the reverse map - _backgroundMap.insert(BackgroundMap::value_type(background, backgroundId)); - } - return backgroundId; - } else { - return (*found).second; - } -} - - -BackgroundStage::BackgroundPointer BackgroundStage::removeBackground(Index index) { - BackgroundPointer removed = _backgrounds.freeElement(index); - - if (removed) { - _backgroundMap.erase(removed); - } - return removed; -} - +template <> +std::string render::PointerStage::_name { "BACKGROUND_STAGE" }; void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { const auto& lightingModel = inputs.get0(); @@ -66,8 +28,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const auto& backgroundStage = renderContext->_scene->getStage(); const auto& backgroundFrame = inputs.get1(); graphics::SkyboxPointer skybox; - if (backgroundStage && backgroundFrame->_backgrounds.size()) { - const auto& background = backgroundStage->getBackground(backgroundFrame->_backgrounds.front()); + if (backgroundStage && backgroundFrame->_elements.size()) { + const auto& background = backgroundStage->getElement(backgroundFrame->_elements.front()); if (background) { skybox = background->getSkybox(); } @@ -98,8 +60,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, // If we're using forward rendering, we need to calculate haze if (args->_renderMethod == render::Args::RenderMethod::FORWARD) { const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_hazes.size() > 0) { - const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); + if (hazeStage && hazeFrame->_elements.size() > 0) { + const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } @@ -111,14 +73,3 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, args->_batch = nullptr; } } - -BackgroundStageSetup::BackgroundStageSetup() { -} - -void BackgroundStageSetup::run(const render::RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(BackgroundStage::getName()); - if (!stage) { - renderContext->_scene->resetStage(BackgroundStage::getName(), std::make_shared()); - } -} - diff --git a/libraries/render-utils/src/BackgroundStage.h b/libraries/render-utils/src/BackgroundStage.h index 3015b721b1..4da8fbf9fb 100644 --- a/libraries/render-utils/src/BackgroundStage.h +++ b/libraries/render-utils/src/BackgroundStage.h @@ -1,8 +1,9 @@ // // BackgroundStage.h - +// // Created by Sam Gateau on 5/9/2017. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,72 +13,19 @@ #define hifi_render_utils_BackgroundStage_h #include -#include -#include -#include #include -#include "HazeStage.h" +#include +#include "HazeStage.h" #include "LightingModel.h" - // Background stage to set up background-related rendering tasks -class BackgroundStage : public render::Stage { -public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using BackgroundPointer = graphics::SunSkyStagePointer; - using Backgrounds = render::indexed_container::IndexedPointerVector; - using BackgroundMap = std::unordered_map; - - using BackgroundIndices = std::vector; - - - Index findBackground(const BackgroundPointer& background) const; - Index addBackground(const BackgroundPointer& background); - - BackgroundPointer removeBackground(Index index); - - bool checkBackgroundId(Index index) const { return _backgrounds.checkIndex(index); } - - Index getNumBackgrounds() const { return _backgrounds.getNumElements(); } - Index getNumFreeBackgrounds() const { return _backgrounds.getNumFreeIndices(); } - Index getNumAllocatedBackgrounds() const { return _backgrounds.getNumAllocatedIndices(); } - - BackgroundPointer getBackground(Index backgroundId) const { - return _backgrounds.get(backgroundId); - } - - Backgrounds _backgrounds; - BackgroundMap _backgroundMap; - - class Frame { - public: - Frame() {} - - void clear() { _backgrounds.clear(); } - - void pushBackground(BackgroundStage::Index index) { _backgrounds.emplace_back(index); } - - BackgroundStage::BackgroundIndices _backgrounds; - }; - using FramePointer = std::shared_ptr; - - Frame _currentFrame; -}; +class BackgroundStage : public render::PointerStage {}; using BackgroundStagePointer = std::shared_ptr; -class BackgroundStageSetup { +class BackgroundStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - BackgroundStageSetup(); - void run(const render::RenderContextPointer& renderContext); }; class DrawBackgroundStage { diff --git a/libraries/render-utils/src/BloomEffect.cpp b/libraries/render-utils/src/BloomEffect.cpp index d27403440c..763f12cf0f 100644 --- a/libraries/render-utils/src/BloomEffect.cpp +++ b/libraries/render-utils/src/BloomEffect.cpp @@ -4,6 +4,7 @@ // // Created by Olivier Prat on 09/25/17. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -43,8 +44,8 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons const auto lightingModel = inputs.get3(); const auto& bloomStage = renderContext->_scene->getStage(); graphics::BloomPointer bloom; - if (bloomStage && bloomFrame->_blooms.size()) { - bloom = bloomStage->getBloom(bloomFrame->_blooms.front()); + if (bloomStage && bloomFrame->_elements.size()) { + bloom = bloomStage->getElement(bloomFrame->_elements.front()); } if (!bloom || (lightingModel && !lightingModel->isBloomEnabled())) { renderContext->taskFlow.abortTask(); diff --git a/libraries/render-utils/src/BloomStage.cpp b/libraries/render-utils/src/BloomStage.cpp index 2bedfeea96..23ec4596bd 100644 --- a/libraries/render-utils/src/BloomStage.cpp +++ b/libraries/render-utils/src/BloomStage.cpp @@ -3,56 +3,13 @@ // // Created by Sam Gondelman on 8/7/2018 // Copyright 2018 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "BloomStage.h" - -#include "DeferredLightingEffect.h" - -#include - -std::string BloomStage::_stageName { "BLOOM_STAGE" }; -const BloomStage::Index BloomStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; -BloomStage::Index BloomStage::findBloom(const BloomPointer& bloom) const { - auto found = _bloomMap.find(bloom); - if (found != _bloomMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } -} - -BloomStage::Index BloomStage::addBloom(const BloomPointer& bloom) { - auto found = _bloomMap.find(bloom); - if (found == _bloomMap.end()) { - auto bloomId = _blooms.newElement(bloom); - // Avoid failing to allocate a bloom, just pass - if (bloomId != INVALID_INDEX) { - // Insert the bloom and its index in the reverse map - _bloomMap.insert(BloomMap::value_type(bloom, bloomId)); - } - return bloomId; - } else { - return (*found).second; - } -} - -BloomStage::BloomPointer BloomStage::removeBloom(Index index) { - BloomPointer removed = _blooms.freeElement(index); - if (removed) { - _bloomMap.erase(removed); - } - return removed; -} - -BloomStageSetup::BloomStageSetup() {} +#include "BloomStage.h" -void BloomStageSetup::run(const render::RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(BloomStage::getName()); - if (!stage) { - renderContext->_scene->resetStage(BloomStage::getName(), std::make_shared()); - } -} +template <> +std::string render::PointerStage::_name { "BLOOM_STAGE" }; diff --git a/libraries/render-utils/src/BloomStage.h b/libraries/render-utils/src/BloomStage.h index bb03e181af..37e72bacea 100644 --- a/libraries/render-utils/src/BloomStage.h +++ b/libraries/render-utils/src/BloomStage.h @@ -3,6 +3,7 @@ // Created by Sam Gondelman on 8/7/2018 // Copyright 2018 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -11,74 +12,17 @@ #ifndef hifi_render_utils_BloomStage_h #define hifi_render_utils_BloomStage_h -#include -#include -#include -#include -#include - -#include -#include #include +#include +#include // Bloom stage to set up bloom-related rendering tasks -class BloomStage : public render::Stage { -public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using BloomPointer = graphics::BloomPointer; - using Blooms = render::indexed_container::IndexedPointerVector; - using BloomMap = std::unordered_map; - - using BloomIndices = std::vector; - - Index findBloom(const BloomPointer& bloom) const; - Index addBloom(const BloomPointer& bloom); - - BloomPointer removeBloom(Index index); - - bool checkBloomId(Index index) const { return _blooms.checkIndex(index); } - - Index getNumBlooms() const { return _blooms.getNumElements(); } - Index getNumFreeBlooms() const { return _blooms.getNumFreeIndices(); } - Index getNumAllocatedBlooms() const { return _blooms.getNumAllocatedIndices(); } - - BloomPointer getBloom(Index bloomId) const { - return _blooms.get(bloomId); - } - - Blooms _blooms; - BloomMap _bloomMap; - - class Frame { - public: - Frame() {} - - void clear() { _blooms.clear(); } - - void pushBloom(BloomStage::Index index) { _blooms.emplace_back(index); } - - BloomStage::BloomIndices _blooms; - }; - using FramePointer = std::shared_ptr; - - Frame _currentFrame; -}; +class BloomStage : public render::PointerStage {}; using BloomStagePointer = std::shared_ptr; -class BloomStageSetup { +class BloomStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - BloomStageSetup(); - void run(const render::RenderContextPointer& renderContext); - -protected: }; #endif diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0ff1c435e1..f2f6639f88 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -4,6 +4,7 @@ // // Created by Andrzej Kapolka on 9/11/14. // Copyright 2014 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -375,7 +376,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Global directional light, maybe shadow and ambient pass auto lightStage = renderContext->_scene->getStage(); assert(lightStage); - assert(lightStage->getNumLights() > 0); + assert(lightStage->getNumElements() > 0); auto keyLight = lightStage->getCurrentKeyLight(*lightFrame); // Check if keylight casts shadows @@ -391,7 +392,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Global Ambient light graphics::LightPointer ambientLight; if (lightStage && lightFrame->_ambientLights.size()) { - ambientLight = lightStage->getLight(lightFrame->_ambientLights.front()); + ambientLight = lightStage->getElement(lightFrame->_ambientLights.front()); } bool hasAmbientMap = (ambientLight != nullptr); @@ -430,8 +431,8 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Haze const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_hazes.size() > 0) { - const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); + if (hazeStage && hazeFrame->_elements.size() > 0) { + const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } @@ -636,7 +637,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { // Add the global light to the light stage (for later shadow rendering) // Set this light to be the default - _defaultLightID = lightStage->addLight(lp, true); + _defaultLightID = lightStage->addElement(lp, true); } auto backgroundStage = renderContext->_scene->getStage(); @@ -649,7 +650,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { _defaultBackground = background; // Add the global light to the light stage (for later shadow rendering) - _defaultBackgroundID = backgroundStage->addBackground(_defaultBackground); + _defaultBackgroundID = backgroundStage->addElement(_defaultBackground); } } @@ -659,7 +660,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { auto haze = std::make_shared(); _defaultHaze = haze; - _defaultHazeID = hazeStage->addHaze(_defaultHaze); + _defaultHazeID = hazeStage->addElement(_defaultHaze); } } @@ -669,7 +670,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { auto tonemapping = std::make_shared(); _defaultTonemapping = tonemapping; - _defaultTonemappingID = tonemappingStage->addTonemapping(_defaultTonemapping); + _defaultTonemappingID = tonemappingStage->addElement(_defaultTonemapping); } } } diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp index 134f12174b..f7e62d75f4 100644 --- a/libraries/render-utils/src/DrawHaze.cpp +++ b/libraries/render-utils/src/DrawHaze.cpp @@ -40,8 +40,8 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu const auto hazeFrame = inputs.get0(); const auto& hazeStage = renderContext->args->_scene->getStage(); graphics::HazePointer haze; - if (hazeStage && hazeFrame->_hazes.size() > 0) { - haze = hazeStage->getHaze(hazeFrame->_hazes.front()); + if (hazeStage && hazeFrame->_elements.size() > 0) { + haze = hazeStage->getElement(hazeFrame->_elements.front()); } if (!haze) { diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp index 88018924d8..1d25c0a372 100644 --- a/libraries/render-utils/src/FadeEffect.cpp +++ b/libraries/render-utils/src/FadeEffect.cpp @@ -3,6 +3,7 @@ // Created by Olivier Prat on 17/07/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -45,7 +46,7 @@ render::ShapePipeline::ItemSetter FadeEffect::getItemUniformSetter() { const auto& scene = args->_scene; const auto& batch = args->_batch; auto transitionStage = scene->getStage(); - auto& transitionState = transitionStage->getTransition(item.getTransitionId()); + auto& transitionState = transitionStage->getElement(item.getTransitionId()); if (transitionState.paramsBuffer._size != sizeof(gpu::StructBuffer)) { static_assert(sizeof(transitionState.paramsBuffer) == sizeof(gpu::StructBuffer), "Assuming gpu::StructBuffer is a helper class for gpu::BufferView"); diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index 122b543c05..72edbacbf0 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -3,6 +3,7 @@ // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -572,7 +573,7 @@ void FadeJob::run(const render::RenderContextPointer& renderContext, FadeJob::Ou // And now update fade effect for (auto transitionId : *transitionStage) { - auto& state = transitionStage->editTransition(transitionId); + auto& state = transitionStage->editElement(transitionId); #ifdef DEBUG auto& item = scene->getItem(state.itemId); assert(item.getTransitionId() == transitionId); diff --git a/libraries/render-utils/src/HazeStage.cpp b/libraries/render-utils/src/HazeStage.cpp index 9251e1e2f9..49ed7a1ea4 100644 --- a/libraries/render-utils/src/HazeStage.cpp +++ b/libraries/render-utils/src/HazeStage.cpp @@ -3,59 +3,13 @@ // // Created by Nissim Hadar on 9/26/2017. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "HazeStage.h" - -#include "DeferredLightingEffect.h" - -#include - -std::string HazeStage::_stageName { "HAZE_STAGE" }; -const HazeStage::Index HazeStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; - -HazeStage::Index HazeStage::findHaze(const HazePointer& haze) const { - auto found = _hazeMap.find(haze); - if (found != _hazeMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } -} -HazeStage::Index HazeStage::addHaze(const HazePointer& haze) { - auto found = _hazeMap.find(haze); - if (found == _hazeMap.end()) { - auto hazeId = _hazes.newElement(haze); - // Avoid failing to allocate a haze, just pass - if (hazeId != INVALID_INDEX) { - - // Insert the haze and its index in the reverse map - _hazeMap.insert(HazeMap::value_type(haze, hazeId)); - } - return hazeId; - } else { - return (*found).second; - } -} - -HazeStage::HazePointer HazeStage::removeHaze(Index index) { - HazePointer removed = _hazes.freeElement(index); - - if (removed) { - _hazeMap.erase(removed); - } - return removed; -} - -HazeStageSetup::HazeStageSetup() { -} +#include "HazeStage.h" -void HazeStageSetup::run(const render::RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(HazeStage::getName()); - if (!stage) { - renderContext->_scene->resetStage(HazeStage::getName(), std::make_shared()); - } -} \ No newline at end of file +template <> +std::string render::PointerStage::_name { "HAZE_STAGE" }; diff --git a/libraries/render-utils/src/HazeStage.h b/libraries/render-utils/src/HazeStage.h index b1c7d0384c..70a33b27eb 100644 --- a/libraries/render-utils/src/HazeStage.h +++ b/libraries/render-utils/src/HazeStage.h @@ -3,6 +3,7 @@ // Created by Nissim Hadar on 9/26/2017. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -11,74 +12,17 @@ #ifndef hifi_render_utils_HazeStage_h #define hifi_render_utils_HazeStage_h -#include -#include -#include -#include -#include - -#include -#include #include +#include +#include // Haze stage to set up haze-related rendering tasks -class HazeStage : public render::Stage { -public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using HazePointer = graphics::HazePointer; - using Hazes = render::indexed_container::IndexedPointerVector; - using HazeMap = std::unordered_map; - - using HazeIndices = std::vector; - - Index findHaze(const HazePointer& haze) const; - Index addHaze(const HazePointer& haze); - - HazePointer removeHaze(Index index); - - bool checkHazeId(Index index) const { return _hazes.checkIndex(index); } - - Index getNumHazes() const { return _hazes.getNumElements(); } - Index getNumFreeHazes() const { return _hazes.getNumFreeIndices(); } - Index getNumAllocatedHazes() const { return _hazes.getNumAllocatedIndices(); } - - HazePointer getHaze(Index hazeId) const { - return _hazes.get(hazeId); - } - - Hazes _hazes; - HazeMap _hazeMap; - - class Frame { - public: - Frame() {} - - void clear() { _hazes.clear(); } - - void pushHaze(HazeStage::Index index) { _hazes.emplace_back(index); } - - HazeStage::HazeIndices _hazes; - }; - using FramePointer = std::shared_ptr; - - Frame _currentFrame; -}; +class HazeStage : public render::PointerStage {}; using HazeStagePointer = std::shared_ptr; -class HazeStageSetup { +class HazeStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - HazeStageSetup(); - void run(const render::RenderContextPointer& renderContext); - -protected: }; class FetchHazeConfig : public render::Job::Config { diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp index 2525427b61..755d0a60be 100644 --- a/libraries/render-utils/src/HighlightEffect.cpp +++ b/libraries/render-utils/src/HighlightEffect.cpp @@ -161,7 +161,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c if (!inShapes.empty() && !render::HighlightStage::isIndexInvalid(highlightId)) { auto resources = inputs.get1(); - auto& highlight = highlightStage->getHighlight(highlightId); + auto& highlight = highlightStage->getElement(highlightId); RenderArgs* args = renderContext->args; @@ -247,7 +247,7 @@ void DrawHighlight::run(const render::RenderContextPointer& renderContext, const auto highlightStage = renderContext->_scene->getStage(); auto highlightId = _sharedParameters->_highlightIds[_highlightPassIndex]; if (!render::HighlightStage::isIndexInvalid(highlightId)) { - auto& highlight = highlightStage->getHighlight(highlightId); + auto& highlight = highlightStage->getElement(highlightId); auto pipeline = getPipeline(highlight._style); { auto& shaderParameters = _configuration.edit(); @@ -432,7 +432,7 @@ void SelectionToHighlight::run(const render::RenderContextPointer& renderContext auto highlightList = highlightStage->getActiveHighlightIds(); for (auto styleId : highlightList) { - auto highlight = highlightStage->getHighlight(styleId); + auto highlight = highlightStage->getElement(styleId); if (!scene->isSelectionEmpty(highlight._selectionName)) { auto highlightId = highlightStage->getHighlightIdBySelection(highlight._selectionName); @@ -572,8 +572,8 @@ void HighlightCleanup::run(const render::RenderContextPointer& renderContext, co auto highlightStage = scene->getStage(); for (auto index : inputs.get0()) { - std::string selectionName = highlightStage->getHighlight(index)._selectionName; - highlightStage->removeHighlight(index); + std::string selectionName = highlightStage->getElement(index)._selectionName; + highlightStage->removeElement(index); scene->removeSelection(selectionName); } diff --git a/libraries/render-utils/src/LightClusters.cpp b/libraries/render-utils/src/LightClusters.cpp index 3425e08783..f0ec35238f 100644 --- a/libraries/render-utils/src/LightClusters.cpp +++ b/libraries/render-utils/src/LightClusters.cpp @@ -348,7 +348,7 @@ glm::ivec3 LightClusters::updateClusters() { uint32_t numClusteredLights = 0; for (size_t lightNum = 1; lightNum < _visibleLightIndices.size(); ++lightNum) { auto lightId = _visibleLightIndices[lightNum]; - auto light = _lightStage->getLight(lightId); + auto light = _lightStage->getElement(lightId); if (!light) { continue; } @@ -567,9 +567,9 @@ void LightClusteringPass::run(const render::RenderContextPointer& renderContext, output = _lightClusters; auto config = std::static_pointer_cast(renderContext->jobConfig); - config->numSceneLights = lightStage->getNumLights(); - config->numFreeSceneLights = lightStage->getNumFreeLights(); - config->numAllocatedSceneLights = lightStage->getNumAllocatedLights(); + config->numSceneLights = lightStage->getNumElements(); + config->numFreeSceneLights = lightStage->getNumFreeElements(); + config->numAllocatedSceneLights = lightStage->getNumAllocatedElements(); config->setNumInputLights(clusteringStats.x); config->setNumClusteredLights(clusteringStats.y); config->setNumClusteredLightReferences(clusteringStats.z); diff --git a/libraries/render-utils/src/LightClusters.h b/libraries/render-utils/src/LightClusters.h index 6192105914..94e1e37ae3 100644 --- a/libraries/render-utils/src/LightClusters.h +++ b/libraries/render-utils/src/LightClusters.h @@ -95,7 +95,7 @@ class LightClusters { FrustumGrid::Planes _gridPlanes[3]; - LightStage::LightIndices _visibleLightIndices; + render::ElementIndices _visibleLightIndices; gpu::BufferView _lightIndicesBuffer; const uint32_t EMPTY_CLUSTER { 0x0000FFFF }; diff --git a/libraries/render-utils/src/LightPayload.cpp b/libraries/render-utils/src/LightPayload.cpp index d1018982d0..43280c9a2a 100644 --- a/libraries/render-utils/src/LightPayload.cpp +++ b/libraries/render-utils/src/LightPayload.cpp @@ -52,7 +52,7 @@ LightPayload::LightPayload() : LightPayload::~LightPayload() { if (!LightStage::isIndexInvalid(_index)) { if (_stage) { - _stage->removeLight(_index); + _stage->removeElement(_index); } } } @@ -64,7 +64,7 @@ void LightPayload::render(RenderArgs* args) { } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { - _index = _stage->addLight(_light); + _index = _stage->addElement(_light); _needUpdate = false; } // Need an update ? @@ -122,7 +122,7 @@ _light(std::make_shared()) KeyLightPayload::~KeyLightPayload() { if (!LightStage::isIndexInvalid(_index)) { if (_stage) { - _stage->removeLight(_index); + _stage->removeElement(_index); } } } @@ -134,7 +134,7 @@ void KeyLightPayload::render(RenderArgs* args) { } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { - _index = _stage->addLight(_light); + _index = _stage->addElement(_light); _needUpdate = false; } // Need an update ? diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index 58f32cdc4e..08e5e51459 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -4,6 +4,7 @@ // // Created by Zach Pomerantz on 1/14/2015. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -15,7 +16,9 @@ #include "ViewFrustum.h" -std::string LightStage::_stageName { "LIGHT_STAGE" }; +template <> +std::string render::PointerStage::_name { "LIGHT_STAGE" }; + // The bias matrix goes from homogeneous coordinates to UV coords (see http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#basic-shader) const glm::mat4 LightStage::Shadow::_biasMatrix { 0.5, 0.0, 0.0, 0.0, @@ -24,8 +27,6 @@ const glm::mat4 LightStage::Shadow::_biasMatrix { 0.5, 0.5, 0.5, 1.0 }; const int LightStage::Shadow::MAP_SIZE = 1024; -const LightStage::Index LightStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; - LightStage::LightStage() { // Add off lights const LightPointer ambientOffLight { std::make_shared() }; @@ -33,28 +34,28 @@ LightStage::LightStage() { ambientOffLight->setColor(graphics::Vec3(0.0)); ambientOffLight->setIntensity(0.0f); ambientOffLight->setType(graphics::Light::Type::AMBIENT); - _ambientOffLightId = addLight(ambientOffLight); + _ambientOffLightId = addElement(ambientOffLight); const LightPointer pointOffLight { std::make_shared() }; pointOffLight->setAmbientIntensity(0.0f); pointOffLight->setColor(graphics::Vec3(0.0)); pointOffLight->setIntensity(0.0f); pointOffLight->setType(graphics::Light::Type::POINT); - _pointOffLightId = addLight(pointOffLight); + _pointOffLightId = addElement(pointOffLight); const LightPointer spotOffLight { std::make_shared() }; spotOffLight->setAmbientIntensity(0.0f); spotOffLight->setColor(graphics::Vec3(0.0)); spotOffLight->setIntensity(0.0f); spotOffLight->setType(graphics::Light::Type::SPOT); - _spotOffLightId = addLight(spotOffLight); + _spotOffLightId = addElement(spotOffLight); const LightPointer sunOffLight { std::make_shared() }; sunOffLight->setAmbientIntensity(0.0f); sunOffLight->setColor(graphics::Vec3(0.0)); sunOffLight->setIntensity(0.0f); sunOffLight->setType(graphics::Light::Type::SUN); - _sunOffLightId = addLight(sunOffLight); + _sunOffLightId = addElement(sunOffLight); // Set default light to the off ambient light (until changed) _defaultLightId = _ambientOffLightId; @@ -72,7 +73,7 @@ LightStage::Shadow::Schema::Schema() { maxDistance = 20.0f; } -LightStage::Shadow::Cascade::Cascade() : +LightStage::Shadow::Cascade::Cascade() : _frustum{ std::make_shared() }, _minDistance{ 0.0f }, _maxDistance{ 20.0f } { @@ -88,7 +89,7 @@ const glm::mat4& LightStage::Shadow::Cascade::getProjection() const { float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFrustum, const Transform& shadowViewInverse, float left, float right, float bottom, float top, float viewMaxShadowDistance) const { - // Far distance should be extended to the intersection of the infinitely extruded shadow frustum + // Far distance should be extended to the intersection of the infinitely extruded shadow frustum // with the view frustum side planes. To do so, we generate 10 triangles in shadow space which are the result of // tesselating the side and far faces of the view frustum and clip them with the 4 side planes of the // shadow frustum. The resulting clipped triangle vertices with the farthest Z gives the desired @@ -121,7 +122,7 @@ float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFru return far; } -LightStage::Shadow::Shadow(graphics::LightPointer light, unsigned int cascadeCount) : +LightStage::Shadow::Shadow(graphics::LightPointer light, unsigned int cascadeCount) : _light{ light } { cascadeCount = std::min(cascadeCount, (unsigned int)SHADOW_CASCADE_MAX_COUNT); Schema schema; @@ -323,21 +324,12 @@ void LightStage::Shadow::setCascadeFrustum(unsigned int cascadeIndex, const View schemaCascade.reprojection = _biasMatrix * shadowFrustum.getProjection() * viewInverse.getMatrix(); } -LightStage::Index LightStage::findLight(const LightPointer& light) const { - auto found = _lightMap.find(light); - if (found != _lightMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } -} - -LightStage::Index LightStage::addLight(const LightPointer& light, const bool shouldSetAsDefault) { +LightStage::Index LightStage::addElement(const LightPointer& light, const bool shouldSetAsDefault) { Index lightId; - auto found = _lightMap.find(light); - if (found == _lightMap.end()) { - lightId = _lights.newElement(light); + auto found = _elementMap.find(light); + if (found == _elementMap.end()) { + lightId = _elements.newElement(light); // Avoid failing to allocate a light, just pass if (lightId != INVALID_INDEX) { @@ -349,8 +341,8 @@ LightStage::Index LightStage::addLight(const LightPointer& light, const bool sho _descs[lightId] = Desc(); } - // INsert the light and its index in the reverese map - _lightMap.insert(LightMap::value_type(light, lightId)); + // Insert the light and its index in the reverese map + _elementMap[light] = lightId; updateLightArrayBuffer(lightId); } @@ -365,30 +357,30 @@ LightStage::Index LightStage::addLight(const LightPointer& light, const bool sho return lightId; } -LightStage::LightPointer LightStage::removeLight(Index index) { - LightPointer removedLight = _lights.freeElement(index); +LightStage::LightPointer LightStage::removeElement(Index index) { + LightPointer removedLight = _elements.freeElement(index); if (removedLight) { - _lightMap.erase(removedLight); + _elementMap.erase(removedLight); _descs[index] = Desc(); } assert(_descs.size() <= (size_t)index || _descs[index].shadowId == INVALID_INDEX); return removedLight; } -LightStage::LightPointer LightStage::getCurrentKeyLight(const LightStage::Frame& frame) const { +LightStage::LightPointer LightStage::getCurrentKeyLight(const LightFrame& frame) const { Index keyLightId { _defaultLightId }; if (!frame._sunLights.empty()) { keyLightId = frame._sunLights.front(); } - return _lights.get(keyLightId); + return _elements.get(keyLightId); } -LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightStage::Frame& frame) const { +LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightFrame& frame) const { Index keyLightId { _defaultLightId }; if (!frame._ambientLights.empty()) { keyLightId = frame._ambientLights.front(); } - return _lights.get(keyLightId); + return _elements.get(keyLightId); } void LightStage::updateLightArrayBuffer(Index lightId) { @@ -397,14 +389,12 @@ void LightStage::updateLightArrayBuffer(Index lightId) { _lightArrayBuffer = std::make_shared(lightSize); } - assert(checkLightId(lightId)); - if (lightId > (Index)_lightArrayBuffer->getNumTypedElements()) { _lightArrayBuffer->resize(lightSize * (lightId + 10)); } // lightArray is big enough so we can remap - auto light = _lights._elements[lightId]; + auto light = _elements._elements[lightId]; if (light) { const auto& lightSchema = light->getLightSchemaBuffer().get(); _lightArrayBuffer->setSubData(lightId, lightSchema); @@ -412,17 +402,3 @@ void LightStage::updateLightArrayBuffer(Index lightId) { // this should not happen ? } } - -LightStageSetup::LightStageSetup() { -} - -void LightStageSetup::run(const render::RenderContextPointer& renderContext) { - if (renderContext->_scene) { - auto stage = renderContext->_scene->getStage(LightStage::getName()); - if (!stage) { - stage = std::make_shared(); - renderContext->_scene->resetStage(LightStage::getName(), stage); - } - } -} - diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index 36e62c614f..5b0a90ddb6 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -4,6 +4,7 @@ // // Created by Zach Pomerantz on 1/14/2015. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,34 +13,44 @@ #ifndef hifi_render_utils_LightStage_h #define hifi_render_utils_LightStage_h -#include -#include - #include - #include - -#include #include -#include +#include class ViewFrustum; -// Light stage to set up light-related rendering tasks -class LightStage : public render::Stage { +class LightFrame { public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } + LightFrame() {} using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using LightPointer = graphics::LightPointer; - using Lights = render::indexed_container::IndexedPointerVector; - using LightMap = std::unordered_map; - using LightIndices = std::vector; + void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); } + void pushLight(Index index, graphics::Light::Type type) { + switch (type) { + case graphics::Light::POINT: { pushPointLight(index); break; } + case graphics::Light::SPOT: { pushSpotLight(index); break; } + case graphics::Light::SUN: { pushSunLight(index); break; } + case graphics::Light::AMBIENT: { pushAmbientLight(index); break; } + default: { break; } + } + } + void pushPointLight(Index index) { _pointLights.emplace_back(index); } + void pushSpotLight(Index index) { _spotLights.emplace_back(index); } + void pushSunLight(Index index) { _sunLights.emplace_back(index); } + void pushAmbientLight(Index index) { _ambientLights.emplace_back(index); } + + render::ElementIndices _pointLights; + render::ElementIndices _spotLights; + render::ElementIndices _sunLights; + render::ElementIndices _ambientLights; +}; + +// Light stage to set up light-related rendering tasks +class LightStage : public render::PointerStage { +public: + using LightPointer = graphics::LightPointer; class Shadow { public: @@ -74,9 +85,9 @@ class LightStage : public render::Stage { float left, float right, float bottom, float top, float viewMaxShadowDistance) const; }; - Shadow(graphics::LightPointer light, unsigned int cascadeCount = 1); + Shadow(LightPointer light, unsigned int cascadeCount = 1); - void setLight(graphics::LightPointer light); + void setLight(LightPointer light); void setKeylightFrustum(const ViewFrustum& viewFrustum, float nearDepth = 1.0f, float farDepth = 1000.0f); @@ -93,83 +104,46 @@ class LightStage : public render::Stage { float getMaxDistance() const { return _maxDistance; } void setMaxDistance(float value); - const graphics::LightPointer& getLight() const { return _light; } + const LightPointer& getLight() const { return _light; } gpu::TexturePointer map; #include "Shadows_shared.slh" class Schema : public ShadowParameters { public: - Schema(); - }; + protected: using Cascades = std::vector; static const glm::mat4 _biasMatrix; - graphics::LightPointer _light; + LightPointer _light; float _maxDistance{ 0.0f }; Cascades _cascades; UniformBufferView _schemaBuffer = nullptr; }; - using ShadowPointer = std::shared_ptr; - Index findLight(const LightPointer& light) const; - Index addLight(const LightPointer& light, const bool shouldSetAsDefault = false); - - Index getDefaultLight() { return _defaultLightId; } - - LightPointer removeLight(Index index); - - bool checkLightId(Index index) const { return _lights.checkIndex(index); } + Index addElement(const LightPointer& light) override { return addElement(light, false); } + Index addElement(const LightPointer& light, const bool shouldSetAsDefault); + LightPointer removeElement(Index index) override; - Index getNumLights() const { return _lights.getNumElements(); } - Index getNumFreeLights() const { return _lights.getNumFreeIndices(); } - Index getNumAllocatedLights() const { return _lights.getNumAllocatedIndices(); } - - LightPointer getLight(Index lightId) const { return _lights.get(lightId); } + Index getDefaultLight() { return _defaultLightId; } LightStage(); gpu::BufferPointer getLightArrayBuffer() const { return _lightArrayBuffer; } void updateLightArrayBuffer(Index lightId); - class Frame { - public: - Frame() {} - - void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); } - void pushLight(LightStage::Index index, graphics::Light::Type type) { - switch (type) { - case graphics::Light::POINT: { pushPointLight(index); break; } - case graphics::Light::SPOT: { pushSpotLight(index); break; } - case graphics::Light::SUN: { pushSunLight(index); break; } - case graphics::Light::AMBIENT: { pushAmbientLight(index); break; } - default: { break; } - } - } - void pushPointLight(LightStage::Index index) { _pointLights.emplace_back(index); } - void pushSpotLight(LightStage::Index index) { _spotLights.emplace_back(index); } - void pushSunLight(LightStage::Index index) { _sunLights.emplace_back(index); } - void pushAmbientLight(LightStage::Index index) { _ambientLights.emplace_back(index); } - - LightStage::LightIndices _pointLights; - LightStage::LightIndices _spotLights; - LightStage::LightIndices _sunLights; - LightStage::LightIndices _ambientLights; - }; - using FramePointer = std::shared_ptr; - class ShadowFrame { public: ShadowFrame() {} - + void clear() {} - + using Object = ShadowPointer; using Objects = std::vector; @@ -177,20 +151,17 @@ class LightStage : public render::Stage { _objects.emplace_back(shadow); } - Objects _objects; }; using ShadowFramePointer = std::shared_ptr; - Frame _currentFrame; - Index getAmbientOffLight() { return _ambientOffLightId; } Index getPointOffLight() { return _pointOffLightId; } Index getSpotOffLight() { return _spotOffLightId; } Index getSunOffLight() { return _sunOffLightId; } - LightPointer getCurrentKeyLight(const LightStage::Frame& frame) const; - LightPointer getCurrentAmbientLight(const LightStage::Frame& frame) const; + LightPointer getCurrentKeyLight(const LightFrame& frame) const; + LightPointer getCurrentAmbientLight(const LightFrame& frame) const; protected: @@ -201,9 +172,7 @@ class LightStage : public render::Stage { gpu::BufferPointer _lightArrayBuffer; - Lights _lights; Descs _descs; - LightMap _lightMap; // define off lights Index _ambientOffLightId; @@ -216,16 +185,9 @@ class LightStage : public render::Stage { }; using LightStagePointer = std::shared_ptr; - -class LightStageSetup { +class LightStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - LightStageSetup(); - void run(const render::RenderContextPointer& renderContext); - -protected: }; - #endif diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index c79ac87551..ba0460417c 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -1,6 +1,9 @@ // +// RenderCommonTask.cpp +// // Created by Bradley Austin Davis on 2018/01/09 // Copyright 2013-2018 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -78,9 +81,9 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& graphics::HazePointer haze; const auto& hazeStage = renderContext->args->_scene->getStage(); - if (hazeStage && hazeFrame->_hazes.size() > 0) { + if (hazeStage && hazeFrame->_elements.size() > 0) { // We use _hazes.back() here because the last haze object will always have haze disabled. - haze = hazeStage->getHaze(hazeFrame->_hazes.back()); + haze = hazeStage->getElement(hazeFrame->_elements.back()); } // Clear the framebuffer without stereo diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index 255fcb6392..5d6395aceb 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -1,6 +1,9 @@ // +// RenderCommonTask.h +// // Created by Bradley Austin Davis on 2018/01/09 // Copyright 2013-2018 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 05253178f1..261658039c 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -5,6 +5,7 @@ // // Created by Sam Gateau on 5/29/15. // Copyright 2016 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -511,8 +512,8 @@ void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, c // Setup haze if current zone has haze const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_hazes.size() > 0) { - const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); + if (hazeStage && hazeFrame->_elements.size() > 0) { + const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 74cdf1b044..85ea0facbe 100644 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -5,6 +5,7 @@ // // Created by Zach Pomerantz on 12/13/2016. // Copyright 2016 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -271,8 +272,8 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i graphics::HazePointer haze; const auto& hazeStage = renderContext->args->_scene->getStage(); - if (hazeStage && hazeFrame->_hazes.size() > 0) { - haze = hazeStage->getHaze(hazeFrame->_hazes.front()); + if (hazeStage && hazeFrame->_elements.size() > 0) { + haze = hazeStage->getElement(hazeFrame->_elements.front()); } gpu::doInBatch("DrawForward::run", args->_context, [&](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 9f1cf8421a..5964a1737c 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -473,8 +473,8 @@ void DebugSubsurfaceScattering::run(const render::RenderContextPointer& renderCo auto lightStage = renderContext->_scene->getStage(); assert(lightStage); - // const auto light = DependencyManager::get()->getLightStage()->getLight(0); - const auto light = lightStage->getLight(0); + // const auto light = DependencyManager::get()->getLightStage()->getElement(0); + const auto light = lightStage->getElement(0); if (!_debugParams) { _debugParams = std::make_shared(sizeof(glm::vec4), nullptr); _debugParams->setSubData(0, _debugCursorTexcoord); diff --git a/libraries/render-utils/src/ToneMapAndResampleTask.cpp b/libraries/render-utils/src/ToneMapAndResampleTask.cpp index d906d82aa7..c72776f1b6 100644 --- a/libraries/render-utils/src/ToneMapAndResampleTask.cpp +++ b/libraries/render-utils/src/ToneMapAndResampleTask.cpp @@ -4,6 +4,7 @@ // // Created by Anna Brewer on 7/3/19. // Copyright 2019 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -74,8 +75,8 @@ void ToneMapAndResample::run(const RenderContextPointer& renderContext, const In const auto& tonemappingStage = renderContext->_scene->getStage(); graphics::TonemappingPointer tonemapping; - if (tonemappingStage && tonemappingFrame->_tonemappings.size()) { - tonemapping = tonemappingStage->getTonemapping(tonemappingFrame->_tonemappings.front()); + if (tonemappingStage && tonemappingFrame->_elements.size()) { + tonemapping = tonemappingStage->getElement(tonemappingFrame->_elements.front()); } if (_debug) { diff --git a/libraries/render-utils/src/TonemappingStage.cpp b/libraries/render-utils/src/TonemappingStage.cpp index 9b6029ca1b..1bb382b77d 100644 --- a/libraries/render-utils/src/TonemappingStage.cpp +++ b/libraries/render-utils/src/TonemappingStage.cpp @@ -7,50 +7,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "TonemappingStage.h" - -#include - -std::string TonemappingStage::_stageName { "TONEMAPPING_STAGE" }; -const TonemappingStage::Index TonemappingStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; - -TonemappingStage::Index TonemappingStage::findTonemapping(const TonemappingPointer& tonemapping) const { - auto found = _tonemappingMap.find(tonemapping); - if (found != _tonemappingMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } -} -TonemappingStage::Index TonemappingStage::addTonemapping(const TonemappingPointer& tonemapping) { - auto found = _tonemappingMap.find(tonemapping); - if (found == _tonemappingMap.end()) { - auto tonemappingId = _tonemappings.newElement(tonemapping); - // Avoid failing to allocate a tonemapping, just pass - if (tonemappingId != INVALID_INDEX) { - // Insert the tonemapping and its index in the reverse map - _tonemappingMap.insert(TonemappingMap::value_type(tonemapping, tonemappingId)); - } - return tonemappingId; - } else { - return (*found).second; - } -} - -TonemappingStage::TonemappingPointer TonemappingStage::removeTonemapping(Index index) { - TonemappingPointer removed = _tonemappings.freeElement(index); - if (removed) { - _tonemappingMap.erase(removed); - } - return removed; -} - -TonemappingStageSetup::TonemappingStageSetup() {} +#include "TonemappingStage.h" -void TonemappingStageSetup::run(const render::RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(TonemappingStage::getName()); - if (!stage) { - renderContext->_scene->resetStage(TonemappingStage::getName(), std::make_shared()); - } -} +template <> +std::string render::PointerStage::_name { "TONEMAPPING_STAGE" }; diff --git a/libraries/render-utils/src/TonemappingStage.h b/libraries/render-utils/src/TonemappingStage.h index 15161094a2..ee05c0ace4 100644 --- a/libraries/render-utils/src/TonemappingStage.h +++ b/libraries/render-utils/src/TonemappingStage.h @@ -11,74 +11,17 @@ #ifndef hifi_render_utils_TonemappingStage_h #define hifi_render_utils_TonemappingStage_h -#include -#include -#include -#include -#include - -#include -#include #include +#include +#include // Tonemapping stage to set up tonemapping-related rendering tasks -class TonemappingStage : public render::Stage { -public: - static std::string _stageName; - static const std::string& getName() { return _stageName; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - using TonemappingPointer = graphics::TonemappingPointer; - using Tonemappings = render::indexed_container::IndexedPointerVector; - using TonemappingMap = std::unordered_map; - - using TonemappingIndices = std::vector; - - Index findTonemapping(const TonemappingPointer& tonemapping) const; - Index addTonemapping(const TonemappingPointer& tonemapping); - - TonemappingPointer removeTonemapping(Index index); - - bool checkTonemappingId(Index index) const { return _tonemappings.checkIndex(index); } - - Index getNumTonemappings() const { return _tonemappings.getNumElements(); } - Index getNumFreeTonemappings() const { return _tonemappings.getNumFreeIndices(); } - Index getNumAllocatedTonemappings() const { return _tonemappings.getNumAllocatedIndices(); } - - TonemappingPointer getTonemapping(Index tonemappingId) const { - return _tonemappings.get(tonemappingId); - } - - Tonemappings _tonemappings; - TonemappingMap _tonemappingMap; - - class Frame { - public: - Frame() {} - - void clear() { _tonemappings.clear(); } - - void pushTonemapping(TonemappingStage::Index index) { _tonemappings.emplace_back(index); } - - TonemappingStage::TonemappingIndices _tonemappings; - }; - using FramePointer = std::shared_ptr; - - Frame _currentFrame; -}; +class TonemappingStage : public render::PointerStage {}; using TonemappingStagePointer = std::shared_ptr; -class TonemappingStageSetup { +class TonemappingStageSetup : public render::StageSetup { public: using JobModel = render::Job::Model; - - TonemappingStageSetup(); - void run(const render::RenderContextPointer& renderContext); - -protected: }; #endif diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp index a8fb349e4d..5d958d8a84 100644 --- a/libraries/render-utils/src/ZoneRenderer.cpp +++ b/libraries/render-utils/src/ZoneRenderer.cpp @@ -4,6 +4,7 @@ // // Created by Sam Gateau on 4/4/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -86,11 +87,11 @@ void SetupZones::run(const RenderContextPointer& context, const Input& input) { // Finally add the default lights and background: lightStage->_currentFrame.pushSunLight(lightStage->getDefaultLight()); lightStage->_currentFrame.pushAmbientLight(lightStage->getDefaultLight()); - backgroundStage->_currentFrame.pushBackground(0); - hazeStage->_currentFrame.pushHaze(0); - bloomStage->_currentFrame.pushBloom(INVALID_INDEX); - tonemappingStage->_currentFrame.pushTonemapping(0); - ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX); + backgroundStage->_currentFrame.pushElement(0); + hazeStage->_currentFrame.pushElement(0); + bloomStage->_currentFrame.pushElement(INVALID_INDEX); + tonemappingStage->_currentFrame.pushElement(0); + ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX); } gpu::PipelinePointer DebugZoneLighting::_keyLightPipeline; @@ -144,22 +145,22 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I std::vector keyLightStack; if (lightStage && lightFrame->_sunLights.size()) { for (auto index : lightFrame->_sunLights) { - keyLightStack.push_back(lightStage->getLight(index)); + keyLightStack.push_back(lightStage->getElement(index)); } } std::vector ambientLightStack; if (lightStage && lightFrame->_ambientLights.size()) { for (auto index : lightFrame->_ambientLights) { - ambientLightStack.push_back(lightStage->getLight(index)); + ambientLightStack.push_back(lightStage->getElement(index)); } } auto backgroundStage = context->_scene->getStage(BackgroundStage::getName()); std::vector skyboxStack; - if (backgroundStage && backgroundFrame->_backgrounds.size()) { - for (auto index : backgroundFrame->_backgrounds) { - auto background = backgroundStage->getBackground(index); + if (backgroundStage && backgroundFrame->_elements.size()) { + for (auto index : backgroundFrame->_elements) { + auto background = backgroundStage->getElement(index); if (background) { skyboxStack.push_back(background->getSkybox()); } diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh index c927070a4d..5471415e1c 100644 --- a/libraries/render-utils/src/sdf_text3D.slh +++ b/libraries/render-utils/src/sdf_text3D.slh @@ -95,8 +95,8 @@ vec4 evalSDFColor(vec2 texCoord, vec4 glyphBounds) { vec4 evalSDFSuperSampled(vec2 texCoord, vec2 positionMS, vec4 glyphBounds) { // Clip to edges. Note: We don't need to check the top edge. - if (positionMS.x < params.bounds.x || positionMS.x > (params.bounds.x + params.bounds.z) || - positionMS.y < params.bounds.y - params.bounds.w) { + if ((params.bounds.z > 0.0 && (positionMS.x < params.bounds.x || positionMS.x > (params.bounds.x + params.bounds.z))) || + (params.bounds.w > 0.0 && (positionMS.y < params.bounds.y - params.bounds.w))) { return vec4(0.0); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 81badb8440..23699de69a 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -438,7 +438,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm // Draw the token for (const QChar& c : token) { - if (advance.x > rightEdge) { + if (bounds.x != -1 && advance.x > rightEdge) { break; } const Glyph& glyph = _glyphs[c]; diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 8112e279e5..730f6db758 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -71,8 +71,8 @@ class Font : public QObject { bool forward, bool mirror) : str(str), color(color), effectColor(effectColor), origin(origin), bounds(bounds), scale(scale), effectThickness(effectThickness), effect(effect), alignment(alignment), verticalAlignment(verticalAlignment), unlit(unlit), forward(forward), mirror(mirror) {} - DrawProps(const QString& str, const glm::vec4& color, const glm::vec2& origin, const glm::vec2& bounds, bool forward) : - str(str), color(color), origin(origin), bounds(bounds), forward(forward) {} + DrawProps(const QString& str, const glm::vec4& color, const glm::vec2& origin, const glm::vec2& bounds, TextAlignment alignment, bool forward) : + str(str), color(color), origin(origin), bounds(bounds), alignment(alignment), forward(forward) {} const QString& str; const glm::vec4& color; diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index 925cffdae1..d722197205 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -4,6 +4,7 @@ // // Created by Niraj Venkat on 6/29/15. // Copyright 2015 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -163,7 +164,7 @@ void DrawStatus::run(const RenderContextPointer& renderContext, const Input& inp status.setColor(Item::Status::Value::RED); } // Set icon based on transition type - auto& transition = transitionStage->getTransition(transitionID); + auto& transition = transitionStage->getElement(transitionID); switch (transition.eventType) { case Transition::Type::USER_ENTER_DOMAIN: status.setIcon((unsigned char)Item::Status::Icon::USER_TRANSITION_IN); diff --git a/libraries/render/src/render/HighlightStage.cpp b/libraries/render/src/render/HighlightStage.cpp index c9f097b387..07fb5b5635 100644 --- a/libraries/render/src/render/HighlightStage.cpp +++ b/libraries/render/src/render/HighlightStage.cpp @@ -1,33 +1,31 @@ +// +// HighlightStage.cpp +// +// Created by Olivier Prat on 07/07/2017. +// Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + #include "HighlightStage.h" +#include "Engine.h" + using namespace render; -std::string HighlightStage::_name("Highlight"); -const HighlightStage::Index HighlightStage::INVALID_INDEX{ render::indexed_container::INVALID_INDEX }; +template <> +std::string TypedStage::_name { "HIGHLIGHT_STAGE" }; HighlightStage::Index HighlightStage::addHighlight(const std::string& selectionName, const HighlightStyle& style) { - Highlight outline{ selectionName, style }; - Index id; - - id = _highlights.newElement(outline); - _activeHighlightIds.push_back(id); - - return id; + Highlight outline { selectionName, style }; + return addElement(outline); } -void HighlightStage::removeHighlight(Index index) { - HighlightIdList::iterator idIterator = std::find(_activeHighlightIds.begin(), _activeHighlightIds.end(), index); - if (idIterator != _activeHighlightIds.end()) { - _activeHighlightIds.erase(idIterator); - } - if (!_highlights.isElementFreed(index)) { - _highlights.freeElement(index); - } -} - -Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const { - for (auto outlineId : _activeHighlightIds) { - const auto& outline = _highlights.get(outlineId); +HighlightStage::Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const { + for (auto outlineId : _activeElementIDs) { + const auto& outline = _elements.get(outlineId); if (outline._selectionName == selectionName) { return outlineId; } @@ -100,9 +98,6 @@ void HighlightStageConfig::setOccludedFillOpacity(float value) { emit dirty(); } -HighlightStageSetup::HighlightStageSetup() { -} - void HighlightStageSetup::configure(const Config& config) { // Copy the styles here but update the stage with the new styles in run to be sure everything is // thread safe... @@ -112,8 +107,7 @@ void HighlightStageSetup::configure(const Config& config) { void HighlightStageSetup::run(const render::RenderContextPointer& renderContext) { auto stage = renderContext->_scene->getStage(HighlightStage::getName()); if (!stage) { - stage = std::make_shared(); - renderContext->_scene->resetStage(HighlightStage::getName(), stage); + renderContext->_scene->resetStage(HighlightStage::getName(), std::make_shared()); } if (!_styles.empty()) { @@ -127,4 +121,3 @@ void HighlightStageSetup::run(const render::RenderContextPointer& renderContext) _styles.clear(); } } - diff --git a/libraries/render/src/render/HighlightStage.h b/libraries/render/src/render/HighlightStage.h index 91d8cc3f81..d675ed9f6b 100644 --- a/libraries/render/src/render/HighlightStage.h +++ b/libraries/render/src/render/HighlightStage.h @@ -3,6 +3,7 @@ // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -11,56 +12,27 @@ #ifndef hifi_render_utils_HighlightStage_h #define hifi_render_utils_HighlightStage_h -#include "Stage.h" #include "Engine.h" -#include "IndexedContainer.h" #include "HighlightStyle.h" +#include "Stage.h" namespace render { - // Highlight stage to set up HighlightStyle-related effects - class HighlightStage : public Stage { + class Highlight { public: + Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { } - class Highlight { - public: - - Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { } - - std::string _selectionName; - HighlightStyle _style; - - }; - - static const std::string& getName() { return _name; } - - using Index = render::indexed_container::Index; - static const Index INVALID_INDEX; - using HighlightIdList = render::indexed_container::Indices; - - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - bool checkHighlightId(Index index) const { return _highlights.checkIndex(index); } - - const Highlight& getHighlight(Index index) const { return _highlights.get(index); } - Highlight& editHighlight(Index index) { return _highlights.edit(index); } + std::string _selectionName; + HighlightStyle _style; + }; + // Highlight stage to set up HighlightStyle-related effects + class HighlightStage : public TypedStage { + public: Index addHighlight(const std::string& selectionName, const HighlightStyle& style = HighlightStyle()); Index getHighlightIdBySelection(const std::string& selectionName) const; - void removeHighlight(Index index); - - HighlightIdList::iterator begin() { return _activeHighlightIds.begin(); } - HighlightIdList::iterator end() { return _activeHighlightIds.end(); } - const HighlightIdList& getActiveHighlightIds() const { return _activeHighlightIds; } - - private: - - using Highlights = render::indexed_container::IndexedVector; - - static std::string _name; - Highlights _highlights; - HighlightIdList _activeHighlightIds; + const IDList& getActiveHighlightIds() const { return _activeElementIDs; } }; using HighlightStagePointer = std::shared_ptr; @@ -122,7 +94,7 @@ namespace render { using Config = HighlightStageConfig; using JobModel = render::Job::Model; - HighlightStageSetup(); + HighlightStageSetup() {} void configure(const Config& config); void run(const RenderContextPointer& renderContext); diff --git a/libraries/render/src/render/HighlightStyle.h b/libraries/render/src/render/HighlightStyle.h index 138674ffbb..266ce71262 100644 --- a/libraries/render/src/render/HighlightStyle.h +++ b/libraries/render/src/render/HighlightStyle.h @@ -17,6 +17,8 @@ #include +#include + namespace render { // This holds the configuration for a particular outline style diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index a9bb2a5856..3776caef0d 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -440,7 +440,7 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti auto transitionId = item.getTransitionId(); if (!TransitionStage::isIndexInvalid(transitionId)) { - auto& transition = transitionStage->getTransition(transitionId); + auto& transition = transitionStage->getElement(transitionId); func(itemId, &transition); } else { func(itemId, nullptr); @@ -477,7 +477,7 @@ void Scene::resetHighlights(const Transaction::HighlightResets& transactions) { if (HighlightStage::isIndexInvalid(outlineId)) { outlineStage->addHighlight(selectionName, newStyle); } else { - outlineStage->editHighlight(outlineId)._style = newStyle; + outlineStage->editElement(outlineId)._style = newStyle; } } } @@ -490,7 +490,7 @@ void Scene::removeHighlights(const Transaction::HighlightRemoves& transactions) auto outlineId = outlineStage->getHighlightIdBySelection(selectionName); if (!HighlightStage::isIndexInvalid(outlineId)) { - outlineStage->removeHighlight(outlineId); + outlineStage->removeElement(outlineId); } } } @@ -505,7 +505,7 @@ void Scene::queryHighlights(const Transaction::HighlightQueries& transactions) { auto outlineId = outlineStage->getHighlightIdBySelection(selectionName); if (!HighlightStage::isIndexInvalid(outlineId)) { - func(&outlineStage->editHighlight(outlineId)._style); + func(&outlineStage->editElement(outlineId)._style); } else { func(nullptr); } @@ -559,7 +559,7 @@ void Scene::removeItemTransition(ItemID itemId) { auto& item = _items[itemId]; TransitionStage::Index transitionId = item.getTransitionId(); if (!render::TransitionStage::isIndexInvalid(transitionId)) { - const auto& transition = transitionStage->getTransition(transitionId); + const auto& transition = transitionStage->getElement(transitionId); const auto transitionOwner = transition.itemId; if (transitionOwner == itemId) { // No more items will be using this transition. Clean it up. @@ -570,7 +570,7 @@ void Scene::removeItemTransition(ItemID itemId) { } } _transitionFinishedOperatorMap.erase(transitionId); - transitionStage->removeTransition(transitionId); + transitionStage->removeElement(transitionId); } setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); diff --git a/libraries/render/src/render/Stage.cpp b/libraries/render/src/render/Stage.cpp index 1ee9b1d6ff..d5335f07ed 100644 --- a/libraries/render/src/render/Stage.cpp +++ b/libraries/render/src/render/Stage.cpp @@ -4,23 +4,13 @@ // // Created by Sam Gateau on 6/14/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "Stage.h" - using namespace render; - - -Stage::~Stage() { - -} - -Stage::Stage() : - _name() -{ -} - +const Stage::Index Stage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; diff --git a/libraries/render/src/render/Stage.h b/libraries/render/src/render/Stage.h index 5145810671..2bb81771e5 100644 --- a/libraries/render/src/render/Stage.h +++ b/libraries/render/src/render/Stage.h @@ -4,6 +4,7 @@ // // Created by Sam Gateau on 6/14/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,27 +13,141 @@ #ifndef hifi_render_Stage_h #define hifi_render_Stage_h -#include #include +#include +#include #include +#include "IndexedContainer.h" + namespace render { + using ElementIndices = std::vector; + class Stage { public: + Stage() {} + virtual ~Stage() {} + using Name = std::string; + using Index = indexed_container::Index; + static const Index INVALID_INDEX; + using IDList = indexed_container::Indices; + + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + }; + + using StagePointer = std::shared_ptr; + using StageMap = std::map; + + template + class TypedStage : public Stage { + public: + TypedStage() {} + virtual ~TypedStage() {} + + static const Name& getName() { return _name; } + + bool checkId(Index index) const { return _elements.checkIndex(index); } - Stage(); - virtual ~Stage(); + const T& getElement(Index id) const { return _elements.get(id); } + T& editElement(Index id) { return _elements.edit(id); } + Index addElement(const T& element) { + Index id = _elements.newElement(element); + _activeElementIDs.push_back(id); + return id; + } + void removeElement(Index index) { + IDList::iterator idIterator = std::find(_activeElementIDs.begin(), _activeElementIDs.end(), index); + if (idIterator != _activeElementIDs.end()) { + _activeElementIDs.erase(idIterator); + } + if (!_elements.isElementFreed(index)) { + _elements.freeElement(index); + } + } + + IDList::iterator begin() { return _activeElementIDs.begin(); } + IDList::iterator end() { return _activeElementIDs.end(); } protected: - Name _name; + static Name _name; + + indexed_container::IndexedVector _elements; + IDList _activeElementIDs; }; - using StagePointer = std::shared_ptr; + class Frame { + public: + Frame() {} - using StageMap = std::map; + using Index = indexed_container::Index; + + void clear() { _elements.clear(); } + void pushElement(Index index) { _elements.emplace_back(index); } + + ElementIndices _elements; + }; + + template + class PointerStage : public Stage { + public: + PointerStage() {} + virtual ~PointerStage() {} + + static const Name& getName() { return _name; } + + bool checkId(Index index) const { return _elements.checkIndex(index); } + + Index getNumElements() const { return _elements.getNumElements(); } + Index getNumFreeElements() const { return _elements.getNumFreeIndices(); } + Index getNumAllocatedElements() const { return _elements.getNumAllocatedIndices(); } + P getElement(Index id) const { return _elements.get(id); } + + Index findElement(const P& element) const { + auto found = _elementMap.find(element); + if (found != _elementMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } + } + + virtual Index addElement(const P& element) { + auto found = _elementMap.find(element); + if (found == _elementMap.end()) { + auto id = _elements.newElement(element); + // Avoid failing to allocate an element, just pass + if (id != INVALID_INDEX) { + // Insert the element and its index in the reverse map + _elementMap[element] = id; + } + return id; + } else { + return (*found).second; + } + } + + virtual P removeElement(Index index) { + P removed = _elements.freeElement(index); + + if (removed) { + _elementMap.erase(removed); + } + return removed; + } + + using Frame = F; + using FramePointer = std::shared_ptr; + F _currentFrame; + + protected: + static Name _name; + + indexed_container::IndexedPointerVector _elements; + std::unordered_map _elementMap; + }; } #endif // hifi_render_Stage_h diff --git a/libraries/render/src/render/StageSetup.h b/libraries/render/src/render/StageSetup.h new file mode 100644 index 0000000000..5691bca207 --- /dev/null +++ b/libraries/render/src/render/StageSetup.h @@ -0,0 +1,35 @@ +// +// StageSetup.h +// render/src/render +// +// Created by HifiExperiments on 10/16/24 +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_render_StageSetup_h +#define hifi_render_StageSetup_h + +#include "Engine.h" + +namespace render { + + template + class StageSetup { + public: + StageSetup() {} + + void run(const RenderContextPointer& renderContext) { + if (renderContext->_scene) { + auto stage = renderContext->_scene->getStage(T::getName()); + if (!stage) { + renderContext->_scene->resetStage(T::getName(), std::make_shared()); + } + } + } + }; +} + +#endif // hifi_render_StageSetup_h diff --git a/libraries/render/src/render/TransitionStage.cpp b/libraries/render/src/render/TransitionStage.cpp index 9ddc72f4db..72637cc3d9 100644 --- a/libraries/render/src/render/TransitionStage.cpp +++ b/libraries/render/src/render/TransitionStage.cpp @@ -1,43 +1,25 @@ -#include "TransitionStage.h" +// +// TransitionStage.cpp +// +// Created by Olivier Prat on 07/07/2017. +// Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// -#include +#include "TransitionStage.h" using namespace render; -std::string TransitionStage::_name("Transition"); -const TransitionStage::Index TransitionStage::INVALID_INDEX{ indexed_container::INVALID_INDEX }; +template <> +std::string TypedStage::_name { "TRANSITION_STAGE" }; TransitionStage::Index TransitionStage::addTransition(ItemID itemId, Transition::Type type, ItemID boundId) { Transition transition; - Index id; - transition.eventType = type; transition.itemId = itemId; transition.boundItemId = boundId; - id = _transitions.newElement(transition); - _activeTransitionIds.push_back(id); - - return id; -} - -void TransitionStage::removeTransition(Index index) { - TransitionIdList::iterator idIterator = std::find(_activeTransitionIds.begin(), _activeTransitionIds.end(), index); - if (idIterator != _activeTransitionIds.end()) { - _activeTransitionIds.erase(idIterator); - } - if (!_transitions.isElementFreed(index)) { - _transitions.freeElement(index); - } + return addElement(transition); } - -TransitionStageSetup::TransitionStageSetup() { -} - -void TransitionStageSetup::run(const RenderContextPointer& renderContext) { - auto stage = renderContext->_scene->getStage(TransitionStage::getName()); - if (!stage) { - stage = std::make_shared(); - renderContext->_scene->resetStage(TransitionStage::getName(), stage); - } -} - diff --git a/libraries/render/src/render/TransitionStage.h b/libraries/render/src/render/TransitionStage.h index abfdca9a06..11256fb346 100644 --- a/libraries/render/src/render/TransitionStage.h +++ b/libraries/render/src/render/TransitionStage.h @@ -1,8 +1,9 @@ // // TransitionStage.h - +// // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,55 +13,22 @@ #define hifi_render_TransitionStage_h #include "Stage.h" -#include "IndexedContainer.h" -#include "Engine.h" +#include "StageSetup.h" #include "Transition.h" namespace render { // Transition stage to set up Transition-related effects - class TransitionStage : public render::Stage { + class TransitionStage : public TypedStage { public: - - static const std::string& getName() { return _name; } - - using Index = indexed_container::Index; - static const Index INVALID_INDEX; - using TransitionIdList = indexed_container::Indices; - - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - - bool isTransitionUsed(Index index) const { return _transitions.checkIndex(index) && !_transitions.isElementFreed(index); } - - const Transition& getTransition(Index TransitionId) const { return _transitions.get(TransitionId); } - - Transition& editTransition(Index TransitionId) { return _transitions.edit(TransitionId); } - + bool isTransitionUsed(Index index) const { return _elements.checkIndex(index) && !_elements.isElementFreed(index); } Index addTransition(ItemID itemId, Transition::Type type, ItemID boundId); - void removeTransition(Index index); - - TransitionIdList::iterator begin() { return _activeTransitionIds.begin(); } - TransitionIdList::iterator end() { return _activeTransitionIds.end(); } - - private: - - using Transitions = indexed_container::IndexedVector; - - static std::string _name; - - Transitions _transitions; - TransitionIdList _activeTransitionIds; }; using TransitionStagePointer = std::shared_ptr; - class TransitionStageSetup { + class TransitionStageSetup : public StageSetup { public: - using JobModel = render::Job::Model; - - TransitionStageSetup(); - void run(const RenderContextPointer& renderContext); - - protected: + using JobModel = Job::Model; }; } diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 3c08c9a1bc..bc54bcc034 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -1,12 +1,11 @@ # Copyright 2013-2020, High Fidelity, Inc. -# Copyright 2021-2023 Overte e.V. +# Copyright 2021-2025 Overte e.V. # SPDX-License-Identifier: Apache-2.0 set(TARGET_NAME shared) include_directories("${QT_DIR}/include/QtCore/${QT_VERSION}/QtCore" "${QT_DIR}/include/QtCore/${QT_VERSION}") -# TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network) if (WIN32) diff --git a/libraries/shared/src/AACube.h b/libraries/shared/src/AACube.h index 66b29e3185..27c424cadb 100644 --- a/libraries/shared/src/AACube.h +++ b/libraries/shared/src/AACube.h @@ -20,6 +20,7 @@ #include #include "BoxBase.h" +#include "SerDes.h" class AABox; class Extents; @@ -80,6 +81,10 @@ class AACube { glm::vec3 _corner; float _scale; + + friend DataSerializer& operator<<(DataSerializer &ser, const AACube &cube); + friend DataDeserializer& operator>>(DataDeserializer &des, AACube &cube); + }; inline bool operator==(const AACube& a, const AACube& b) { @@ -99,5 +104,16 @@ inline QDebug operator<<(QDebug debug, const AACube& cube) { return debug; } +inline DataSerializer& operator<<(DataSerializer &ser, const AACube &cube) { + ser << cube._corner; + ser << cube._scale; + return ser; +} + +inline DataDeserializer& operator>>(DataDeserializer &des, AACube &cube) { + des >> cube._corner; + des >> cube._scale; + return des; +} #endif // hifi_AACube_h diff --git a/libraries/shared/src/BlendshapeConstants.h b/libraries/shared/src/BlendshapeConstants.h index 596e7df4ee..b741059146 100644 --- a/libraries/shared/src/BlendshapeConstants.h +++ b/libraries/shared/src/BlendshapeConstants.h @@ -122,6 +122,25 @@ struct BlendshapeOffsetUnpacked { float positionOffsetX, positionOffsetY, positionOffsetZ; float normalOffsetX, normalOffsetY, normalOffsetZ; float tangentOffsetX, tangentOffsetY, tangentOffsetZ; + + /** + * @brief Set all components of all the offsets to zero + * + * @note glm::vec3 is not trivially copyable, so it's not correct to clear it with memset. + */ + void clear() { + positionOffsetX = 0.0f; + positionOffsetY = 0.0f; + positionOffsetZ = 0.0f; + + normalOffsetX = 0.0f; + normalOffsetY = 0.0f; + normalOffsetZ = 0.0f; + + tangentOffsetX = 0.0f; + tangentOffsetY = 0.0f; + tangentOffsetZ = 0.0f; + } }; using BlendshapeOffset = BlendshapeOffsetPacked; diff --git a/libraries/shared/src/PickFilter.h b/libraries/shared/src/PickFilter.h index 1cc1a8b0b5..acf0c70eab 100644 --- a/libraries/shared/src/PickFilter.h +++ b/libraries/shared/src/PickFilter.h @@ -1,6 +1,7 @@ // -// Created by Sam Gondelman on 12/7/18. +// Created by Sam Gondelman on December 7th, 2018. // Copyright 2018 High Fidelity, Inc. +// Copyright 2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -26,7 +27,7 @@ class PickFilter { * PICK_DOMAIN_ENTITIES1Include domain entities when intersecting. * PICK_AVATAR_ENTITIES2Include avatar entities when intersecting. * PICK_LOCAL_ENTITIES4Include local entities when intersecting. - * PICK_AVATATRS8Include avatars when intersecting. + * PICK_AVATARS8Include avatars when intersecting. * PICK_HUD16Include the HUD surface when intersecting in HMD mode. * PICK_INCLUDE_VISIBLE32Include visible objects when intersecting. * PICK_INCLUDE_INVISIBLE64Include invisible objects when intersecting. diff --git a/libraries/shared/src/SerDes.cpp b/libraries/shared/src/SerDes.cpp new file mode 100644 index 0000000000..ad32d7014f --- /dev/null +++ b/libraries/shared/src/SerDes.cpp @@ -0,0 +1,85 @@ +// +// SerDes.h +// +// +// Created by Dale Glass on 5/6/2022 +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "SerDes.h" +const int DataSerializer::DEFAULT_SIZE; +const char DataSerializer::PADDING_CHAR; + + +static void dumpHex(QDebug &debug, const char*buf, size_t len) { + QString literal; + QString hex; + + for(size_t i=0;i(c), 16 ); + if ( hnum.length() == 1 ) { + hnum.prepend("0"); + } + + hex.append(hnum + " "); + + if ( literal.length() == 16 || (i+1 == len) ) { + while( literal.length() < 16 ) { + literal.append(" "); + hex.append(" "); + } + + debug << literal << " " << hex << "\n"; + literal.clear(); + hex.clear(); + } + } +} + + +QDebug operator<<(QDebug debug, const DataSerializer &ser) { + debug << "{ capacity =" << ser.capacity() << "; length = " << ser.length() << "; pos = " << ser.pos() << "}"; + debug << "\n"; + + dumpHex(debug, ser.buffer(), ser.length()); + return debug; +} + + +QDebug operator<<(QDebug debug, const DataDeserializer &des) { + debug << "{ length = " << des.length() << "; pos = " << des.pos() << "}"; + debug << "\n"; + + + dumpHex(debug, des.buffer(), des.length()); + return debug; +} + + +void DataSerializer::changeAllocation(size_t new_size) { + while ( _capacity < new_size) { + _capacity *= 2; + } + + char *new_buf = new char[_capacity]; + assert( *new_buf ); + + memcpy(new_buf, _store, _length); + char *prev_buf = _store; + _store = new_buf; + + delete []prev_buf; +} diff --git a/libraries/shared/src/SerDes.h b/libraries/shared/src/SerDes.h new file mode 100644 index 0000000000..f80d09a60a --- /dev/null +++ b/libraries/shared/src/SerDes.h @@ -0,0 +1,953 @@ +// +// SerDes.h +// +// +// Created by Dale Glass on 5/6/2022 +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#include +#include +#include +#include +#include + +/** + * @brief Data serializer + * + * When encoding, this class takes in data and encodes it into a buffer. No attempt is made to store version numbers, lengths, + * or any other metadata. It's entirely up to the user to use the class in such a way that the process can be + * correctly reversed if variable-length or optional fields are used. + * + * It can operate both on an internal, dynamically-allocated buffer, or an externally provided, fixed-size one. + * If an external store is used, the class will refuse to add data once capacity is reached and set the overflow flag. + * When decoding, this class operates on a fixed size buffer. If an attempt to read past the end is made, the read fails, + * and the overflow flag is set. + * + * The class was written for the maximum simplicity possible and inline friendliness. + * + * Example of encoding: + * + * @code {.cpp} + * uint8_t version = 1; + * uint16_t count = 1; + * glm::vec3 pos{1.5, 2.0, 9.0}; + * + * Serializer ser; + * ser << version; + * ser << count; + * ser << pos; + * + * // Serialized data is in ser.buffer(), ser.length() long. + * @endcode + * + * This object should be modified directly to add support for any primitive and common datatypes in the code. To support serializing/deserializing + * classes and structures, implement a `operator<<` and `operator>>` functions for that object, eg: + * + * @code {.cpp} + * DataSerializer &operator<<(DataSerializer &ser, const Object &o) { + * ser << o._borderColor; + * ser << o._maxAnisotropy; + * ser << o._filter; + * return ser; + * } + * @endcode + * + */ +class DataSerializer { + public: + + /** + * @brief RAII tracker of advance position + * + * When a custom operator<< is implemented for DataSserializer, + * this class allows to easily keep track of how much data has been added and + * adjust the parent's lastAdvance() count on this class' destruction. + * + * @code {.cpp} + * DataSerializer &operator<<(DataSerializer &ser, const Object &o) { + * DataSerializer::SizeTracker tracker(ser); + * + * ser << o._borderColor; + * ser << o._maxAnisotropy; + * ser << o._filter; + * return ser; + * } + * @endcode + */ + class SizeTracker { + public: + SizeTracker(DataSerializer &parent) : _parent(parent) { + _start_pos = _parent.pos(); + } + + ~SizeTracker() { + size_t cur_pos = _parent.pos(); + + if ( cur_pos >= _start_pos ) { + _parent._lastAdvance = cur_pos - _start_pos; + } else { + _parent._lastAdvance = 0; + } + } + + private: + DataSerializer &_parent; + size_t _start_pos = 0; + }; + + /** + * @brief Default size for a dynamically allocated buffer. + * + * Since this is mostly intended to be used for networking, we default to the largest probable MTU here. + */ + static const int DEFAULT_SIZE = 1500; + + /** + * @brief Character to use for padding. + * + * Padding should be ignored, so it doesn't matter what we go with here, but it can be useful to set it + * to something that would be distinctive in a dump. + */ + static const char PADDING_CHAR = (char)0xAA; + + /** + * @brief Construct a dynamically allocated serializer + * + * If constructed this way, an internal buffer will be dynamically allocated and grown as needed. + * + * The buffer is SerDes::DEFAULT_SIZE bytes by default, and doubles in size every time the limit is reached. + */ + DataSerializer() { + _capacity = DEFAULT_SIZE; + _pos = 0; + _length = 0; + _store = new char[_capacity]; + } + + /** + * @brief Construct a statically allocated serializer + * + * If constructed this way, the external buffer will be used to store data. The class will refuse to + * keep adding data if the maximum length is reached, write a critical message to the log, and set + * the overflow flag. + * + * The flag can be read with isOverflow() + * + * @param externalStore External data store + * @param storeLength Length of the data store + */ + DataSerializer(char *externalStore, size_t storeLength) { + _capacity = storeLength; + _length = 0; + _pos = 0; + _storeIsExternal = true; + _store = externalStore; + } + + /** + * @brief Construct a statically allocated serializer + * + * If constructed this way, the external buffer will be used to store data. The class will refuse to + * keep adding data if the maximum length is reached, and set the overflow flag. + * + * The flag can be read with isOverflow() + * + * @param externalStore External data store + * @param storeLength Length of the data store + */ + DataSerializer(uint8_t *externalStore, size_t storeLength) : DataSerializer((char*)externalStore, storeLength) { + + } + + DataSerializer(const DataSerializer &) = delete; + DataSerializer &operator=(const DataSerializer &) = delete; + + + + ~DataSerializer() { + if (!_storeIsExternal) { + delete[] _store; + } + } + + /** + * @brief Adds padding to the output + * + * The bytes will be set to SerDes::PADDING_CHAR, which is a constant in the source code. + * Since padding isn't supposed to be read, it can be any value and is intended to + * be set to something that can be easily recognized in a dump. + * + * @param bytes Number of bytes to add + */ + void addPadding(size_t bytes) { + if (!extendBy(bytes, "padding")) { + return; + } + + // Fill padding with something recognizable. Will keep valgrind happier. + memset(&_store[_pos], PADDING_CHAR, bytes); + _pos += bytes; + } + + /** + * @brief Add an uint8_t to the output + * + * @param c Character to add + * @return SerDes& This object + */ + DataSerializer &operator<<(uint8_t c) { + return *this << int8_t(c); + } + + /** + * @brief Add an int8_t to the output + * + * @param c Character to add + * @return SerDes& This object + */ + DataSerializer &operator<<(int8_t c) { + if (!extendBy(1, "int8_t")) { + return *this; + } + + _store[_pos++] = c; + return *this; + } + + + /////////////////////////////////////////////////////////// + + /** + * @brief Add an uint16_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(uint16_t val) { + return *this << int16_t(val); + } + + /** + * @brief Add an int16_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(int16_t val) { + if (!extendBy(sizeof(val), "int16_t")) { + return *this; + } + + memcpy(&_store[_pos], (char*)&val, sizeof(val)); + _pos += sizeof(val); + return *this; + } + + + + /////////////////////////////////////////////////////////// + + /** + * @brief Add an uint32_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(uint32_t val) { + return *this << int32_t(val); + } + + /** + * @brief Add an int32_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(int32_t val) { + if (!extendBy(sizeof(val), "int32_t")) { + return *this; + } + + memcpy(&_store[_pos], (char*)&val, sizeof(val)); + _pos += sizeof(val); + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Add an uint64_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(uint64_t val) { + return *this << int64_t(val); + } + + /** + * @brief Add an int64_t to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(int64_t val) { + if (!extendBy(sizeof(val), "int64_t")) { + return *this; + } + + memcpy(&_store[_pos], (char*)&val, sizeof(val)); + _pos += sizeof(val); + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Add an float to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(float val) { + if (extendBy(sizeof(val), "float")) { + memcpy(&_store[_pos], (char*)&val, sizeof(val)); + _pos += sizeof(val); + } + return *this; + } + + + /////////////////////////////////////////////////////////// + + + /** + * @brief Add an glm::vec3 to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(glm::vec3 val) { + size_t sz = sizeof(val.x); + if (extendBy(sz*3, "glm::vec3")) { + memcpy(&_store[_pos ], (char*)&val.x, sz); + memcpy(&_store[_pos + sz ], (char*)&val.y, sz); + memcpy(&_store[_pos + sz*2], (char*)&val.z, sz); + + _pos += sz*3; + } + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Add a glm::vec4 to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(glm::vec4 val) { + size_t sz = sizeof(val.x); + if (extendBy(sz*4, "glm::vec4")) { + memcpy(&_store[_pos ], (char*)&val.x, sz); + memcpy(&_store[_pos + sz ], (char*)&val.y, sz); + memcpy(&_store[_pos + sz*2], (char*)&val.z, sz); + memcpy(&_store[_pos + sz*3], (char*)&val.w, sz); + + _pos += sz*4; + } + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Add a glm::ivec2 to the output + * + * @param val Value to add + * @return SerDes& This object + */ + DataSerializer &operator<<(glm::ivec2 val) { + size_t sz = sizeof(val.x); + if (extendBy(sz*2, "glm::ivec2")) { + memcpy(&_store[_pos ], (char*)&val.x, sz); + memcpy(&_store[_pos + sz ], (char*)&val.y, sz); + + _pos += sz*2; + } + return *this; + } + + + /////////////////////////////////////////////////////////// + + /** + * @brief Write a null-terminated string into the buffer + * + * The `\0` at the end of the string is also written. + * + * @param val Value to write + * @return SerDes& This object + */ + DataSerializer &operator<<(const char *val) { + size_t len = strlen(val)+1; + if (extendBy(len, "string")) { + memcpy(&_store[_pos], val, len); + _pos += len; + } + return *this; + } + + /** + * @brief Write a QString into the buffer + * + * The string is encoded in UTF-8 and the `\0` at the end of the string is also written. + * + * @param val Value to write + * @return SerDes& This object + */ + DataSerializer &operator<<(const QString &val) { + return *this << val.toUtf8().constData(); + } + + + /////////////////////////////////////////////////////////// + + /** + * @brief Pointer to the start of the internal buffer. + * + * The allocated amount can be found with capacity(). + * + * The end of the stored data can be found with length(). + * + * @return Pointer to buffer + */ + char *buffer() const { return _store; } + + /** + * @brief Current position in the buffer. Starts at 0. + * + * @return size_t + */ + size_t pos() const { return _pos; } + + /** + * @brief Last position that was written to in the buffer. Starts at 0. + * + * @return size_t + */ + size_t length() const { return _length; } + + /** + * @brief Current capacity of the buffer. + * + * If the buffer is dynamically allocated, it can grow. + * + * If the buffer is static, this is a fixed limit. + * + * @return size_t + */ + size_t capacity() const { return _capacity; } + + /** + * @brief Whether there's any data in the buffer + * + * @return true Something has been written + * @return false The buffer is empty + */ + bool isEmpty() const { return _length == 0; } + + /** + * @brief The buffer size limit has been reached + * + * This can only return true for a statically allocated buffer. + * + * @return true Limit reached + * @return false There is still room + */ + bool isOverflow() const { return _overflow; } + + /** + * @brief Reset the serializer to the start, clear overflow bit. + * + */ + void rewind() { _pos = 0; _overflow = false; _lastAdvance = 0; } + + + /** + * @brief Size of the last advance + * + * This can be used to get how many bytes were added in the last operation. + * It is reset on rewind() + * + * @return size_t + */ + size_t lastAdvance() const { return _lastAdvance; } + + /** + * @brief Dump the contents of this object into QDebug + * + * This produces a dump of the internal state, and an ASCII/hex dump of + * the contents, for debugging. + * + * @param debug Qt QDebug stream + * @param ds This object + * @return QDebug + */ + friend QDebug operator<<(QDebug debug, const DataSerializer &ds); + + private: + bool extendBy(size_t bytes, const QString &type_name) { + //qDebug() << "Extend by" << bytes; + + if ( _capacity < _length + bytes) { + if ( _storeIsExternal ) { + qCritical() << "Serializer trying to write past end of output buffer of" << _capacity << "bytes. Error writing" << bytes << "bytes for" << type_name << " from position " << _pos << ", length " << _length; + _overflow = true; + return false; + } + + changeAllocation(_length + bytes); + } + + _length += bytes; + _lastAdvance = bytes; + return true; + } + + // This is split up here to try to make the class as inline-friendly as possible. + void changeAllocation(size_t new_size); + + char *_store; + bool _storeIsExternal = false; + bool _overflow = false; + size_t _capacity = 0; + size_t _length = 0; + size_t _pos = 0; + size_t _lastAdvance = 0; +}; + +/** + * @brief Data deserializer + * + * This class operates on a fixed size buffer. If an attempt to read past the end is made, the read fails, + * and the overflow flag is set. + * + * The class was written for the maximum simplicity possible and inline friendliness. + * + * Example of decoding: + * + * @code {.cpp} + * // Incoming data has been placed in: + * // char buffer[1024]; + * + * uint8_t version; + * uint16_t count; + * glm::vec3 pos; + * + * DataDeserializer des(buffer, sizeof(buffer)); + * des >> version; + * des >> count; + * des >> pos; + * @endcode + * + * This object should be modified directly to add support for any primitive and common datatypes in the code. To support deserializing + * classes and structures, implement an `operator>>` function for that object, eg: + * + * @code {.cpp} + * DataDeserializer &operator>>(DataDeserializer &des, Object &o) { + * des >> o._borderColor; + * des >> o._maxAnisotropy; + * des >> o._filter; + * return des; + * } + * @endcode + * + */ +class DataDeserializer { + public: + + /** + * @brief RAII tracker of advance position + * + * When a custom operator>> is implemented for DataDeserializer, + * this class allows to easily keep track of how much data has been added and + * adjust the parent's lastAdvance() count on this class' destruction. + * + * @code {.cpp} + * DataDeserializer &operator>>(Deserializer &des, Object &o) { + * DataDeserializer::SizeTracker tracker(des); + * + * des >> o._borderColor; + * des >> o._maxAnisotropy; + * des >> o._filter; + * return des; + * } + * @endcode + */ + class SizeTracker { + public: + SizeTracker(DataDeserializer &parent) : _parent(parent) { + _start_pos = _parent.pos(); + } + + ~SizeTracker() { + size_t cur_pos = _parent.pos(); + + if ( cur_pos >= _start_pos ) { + _parent._lastAdvance = cur_pos - _start_pos; + } else { + _parent._lastAdvance = 0; + } + } + + private: + DataDeserializer &_parent; + size_t _start_pos = 0; + }; + + /** + * @brief Construct a Deserializer + * * + * @param externalStore External data store + * @param storeLength Length of the data store + */ + DataDeserializer(const char *externalStore, size_t storeLength) { + _length = storeLength; + _pos = 0; + _store = externalStore; + _lastAdvance = 0; + } + + /** + * @brief Construct a Deserializer + * + * @param externalStore External data store + * @param storeLength Length of the data store + */ + DataDeserializer(const uint8_t *externalStore, size_t storeLength) : DataDeserializer((const char*)externalStore, storeLength) { + + } + + /** + * @brief Construct a new Deserializer reading data from a Serializer + * + * This is a convenience function for testing. + * + * @param serializer Serializer with data + */ + DataDeserializer(const DataSerializer &serializer) : DataDeserializer(serializer.buffer(), serializer.length()) { + + } + + /** + * @brief Skips padding in the input + * + * @param bytes Number of bytes to skip + */ + void skipPadding(size_t bytes) { + if (!canAdvanceBy(bytes, "padding")) { + return; + } + + _pos += bytes; + _lastAdvance = bytes; + } + + + /** + * @brief Read an uint8_t from the buffer + * + * @param c Character to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(uint8_t &c) { + return *this >> reinterpret_cast(c); + } + + /** + * @brief Read an int8_t from the buffer + * + * @param c Character to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(int8_t &c) { + if ( canAdvanceBy(1, "int8_t") ) { + c = _store[_pos++]; + _lastAdvance = sizeof(c); + } + + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Read an uint16_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(uint16_t &val) { + return *this >> reinterpret_cast(val); + } + + /** + * @brief Read an int16_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(int16_t &val) { + if ( canAdvanceBy(sizeof(val), "int16_t") ) { + memcpy((char*)&val, &_store[_pos], sizeof(val)); + _pos += sizeof(val); + _lastAdvance = sizeof(val); + } + + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Read an uint32_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(uint32_t &val) { + return *this >> reinterpret_cast(val); + } + + /** + * @brief Read an int32_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(int32_t &val) { + if ( canAdvanceBy(sizeof(val), "int32_t") ) { + memcpy((char*)&val, &_store[_pos], sizeof(val)); + _pos += sizeof(val); + _lastAdvance = sizeof(val); + } + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Read an uint64_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(uint64_t &val) { + return *this >> reinterpret_cast(val); + } + + /** + * @brief Read an int64_t from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(int64_t &val) { + if ( canAdvanceBy(sizeof(val), "int64_t") ) { + memcpy((char*)&val, &_store[_pos], sizeof(val)); + _pos += sizeof(val); + _lastAdvance = sizeof(val); + } + return *this; + } + + /////////////////////////////////////////////////////////// + + + /** + * @brief Read an float from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(float &val) { + if ( canAdvanceBy(sizeof(val), "float") ) { + memcpy((char*)&val, &_store[_pos], sizeof(val)); + _pos += sizeof(val); + _lastAdvance = sizeof(val); + } + return *this; + } + + /////////////////////////////////////////////////////////// + + + + + /** + * @brief Read a glm::vec3 from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(glm::vec3 &val) { + size_t sz = sizeof(val.x); + + if ( canAdvanceBy(sz*3, "glm::vec3") ) { + memcpy((char*)&val.x, &_store[_pos ], sz); + memcpy((char*)&val.y, &_store[_pos + sz ], sz); + memcpy((char*)&val.z, &_store[_pos + sz*2], sz); + + _pos += sz*3; + _lastAdvance = sz * 3; + } + + return *this; + } + + /////////////////////////////////////////////////////////// + + + /** + * @brief Read a glm::vec4 from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(glm::vec4 &val) { + size_t sz = sizeof(val.x); + + if ( canAdvanceBy(sz*4, "glm::vec4")) { + memcpy((char*)&val.x, &_store[_pos ], sz); + memcpy((char*)&val.y, &_store[_pos + sz ], sz); + memcpy((char*)&val.z, &_store[_pos + sz*2], sz); + memcpy((char*)&val.w, &_store[_pos + sz*3], sz); + + _pos += sz*4; + _lastAdvance = sz*4; + } + return *this; + } + + /////////////////////////////////////////////////////////// + + + /** + * @brief Read a glm::ivec2 from the buffer + * + * @param val Value to read + * @return SerDes& This object + */ + DataDeserializer &operator>>(glm::ivec2 &val) { + size_t sz = sizeof(val.x); + + if ( canAdvanceBy(sz*2, "glm::ivec2") ) { + memcpy((char*)&val.x, &_store[_pos ], sz); + memcpy((char*)&val.y, &_store[_pos + sz ], sz); + + _pos += sz*2; + _lastAdvance = sz * 2; + } + + return *this; + } + + /////////////////////////////////////////////////////////// + + /** + * @brief Pointer to the start of the internal buffer. + * + * The allocated amount can be found with capacity(). + * + * The end of the stored data can be found with length(). + * + * @return Pointer to buffer + */ + const char *buffer() const { return _store; } + + /** + * @brief Current position in the buffer. Starts at 0. + * + * @return size_t + */ + size_t pos() const { return _pos; } + + /** + * @brief Last position that was written to in the buffer. Starts at 0. + * + * @return size_t + */ + size_t length() const { return _length; } + + /** + * @brief Whether there's any data in the buffer + * + * @return true Something has been written + * @return false The buffer is empty + */ + bool isEmpty() const { return _length == 0; } + + /** + * @brief The buffer size limit has been reached + * + * This can only return true for a statically allocated buffer. + * + * @return true Limit reached + * @return false There is still room + */ + bool isOverflow() const { return _overflow; } + + /** + * @brief Reset the serializer to the start, clear overflow bit. + * + */ + void rewind() { _pos = 0; _overflow = false; _lastAdvance = 0; } + + /** + * @brief Size of the last advance + * + * This can be used to get how many bytes were added in the last operation. + * It is reset on rewind() + * + * @return size_t + */ + size_t lastAdvance() const { return _lastAdvance; } + + /** + * @brief Dump the contents of this object into QDebug + * + * This produces a dump of the internal state, and an ASCII/hex dump of + * the contents, for debugging. + * + * @param debug Qt QDebug stream + * @param ds This object + * @return QDebug + */ + friend QDebug operator<<(QDebug debug, const DataDeserializer &ds); + + private: + bool canAdvanceBy(size_t bytes, const QString &type_name) { + //qDebug() << "Checking advance by" << bytes; + + if ( _length < _pos + bytes) { + qCritical() << "Deserializer trying to read past end of input buffer of" << _length << "bytes, reading" << bytes << "bytes for" << type_name << "from position " << _pos; + _overflow = true; + return false; + } + + return true; + } + + const char *_store; + bool _overflow = false; + size_t _length = 0; + size_t _pos = 0; + size_t _lastAdvance = 0; +}; diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 82cab1c76c..24df1c9e0f 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -261,6 +261,8 @@ visible: false }); + var savedClippingEnabled = false; + function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { // Adjust the position such that the bounding box (registration, dimensions and orientation) lies behind the original // position in the given direction. @@ -1195,6 +1197,7 @@ selectionDisplay.disableTriggerMapping(); tablet.landscape = false; Controller.disableMapping(CONTROLLER_MAPPING_NAME); + Render.cameraClippingEnabled = savedClippingEnabled; } else { if (shouldUseEditTabletApp()) { tablet.loadQMLSource(Script.resolvePath("qml/Edit.qml"), true); @@ -1212,6 +1215,8 @@ print("starting tablet in landscape mode"); tablet.landscape = true; Controller.enableMapping(CONTROLLER_MAPPING_NAME); + savedClippingEnabled = Render.cameraClippingEnabled; + Render.cameraClippingEnabled = false; // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 74de8d1b9b..1fa85ba37a 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -1,7 +1,7 @@ // // domainChat.js // -// Created by Armored Dragon, 2024. +// Created by Armored Dragon, May 17th, 2024. // Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. @@ -51,6 +51,7 @@ activeIcon: Script.resolvePath("./img/icon_black.png"), sortOrder: 8, text: "CHAT", + sortOrder: 8, isActive: appIsVisible, }); diff --git a/scripts/system/graphicsSettings.js b/scripts/system/graphicsSettings.js index df556a91b1..748eafca24 100644 --- a/scripts/system/graphicsSettings.js +++ b/scripts/system/graphicsSettings.js @@ -1,15 +1,20 @@ // // graphicsSettings.js // -// Created by Kalila L. on 8/5/2020 +// Created by Kalila L. on August 5th, 2020 // Copyright 2020 Vircadia contributors. +// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function() { // BEGIN LOCAL_SCOPE - + var channelComm = "Overte-ShowGraphicsIconChanged"; + var appStatus = false; + var GRAPHICS_HIDE_AND_SHOW_SETTING_KEY = "showGraphicsIcon"; + var GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE = true; + var AppUi = Script.require('appUi'); // cellphone-cog MDI @@ -49,22 +54,49 @@ } function startup() { - ui = new AppUi({ - buttonName: BUTTON_NAME, - sortOrder: 8, - normalButton: getIcon(), - activeButton: getIcon().replace('white', 'black'), - home: GRAPHICS_QML_SOURCE - }); + if (!appStatus) { + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 8, + normalButton: getIcon(), + activeButton: getIcon().replace('white', 'black'), + home: GRAPHICS_QML_SOURCE + }); + } + appStatus = true; } function shutdown() { + if (appStatus) { + ui.onScriptEnding(); + appStatus = false; + } + } + + function cleanup() { + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(channelComm); + } + + function onMessageReceived(channel, message, sender, localOnly) { + if (channel === channelComm && localOnly) { + if (Settings.getValue(GRAPHICS_HIDE_AND_SHOW_SETTING_KEY, GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE)) { + startup(); + } else { + shutdown(); + } + } } // // Run the functions. // - startup(); - Script.scriptEnding.connect(shutdown); + if (Settings.getValue(GRAPHICS_HIDE_AND_SHOW_SETTING_KEY, GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE)) { + startup(); + } + Messages.subscribe(channelComm); + Messages.messageReceived.connect(onMessageReceived); + + Script.scriptEnding.connect(cleanup); }()); // END LOCAL_SCOPE diff --git a/scripts/system/places/icons/portalFX.png b/scripts/system/places/icons/portalFX.png new file mode 100644 index 0000000000000000000000000000000000000000..6c781c824befad6e069574c51eecc309f06047d6 GIT binary patch literal 79835 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

r1j|Oqmj~@JCxYf~aRq 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 literal 0 HcmV?d00001 diff --git a/scripts/system/places/places.css b/scripts/system/places/places.css index 37eac2d002..684139a5b8 100644 --- a/scripts/system/places/places.css +++ b/scripts/system/places/places.css @@ -3,7 +3,7 @@ // places.css // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // css for the ui of the Places application. // @@ -750,19 +750,20 @@ font.domain-nbrUser_small { color: #cccccc; padding: 10px; text-align: justify; - text-justify: inter-word; + text-justify: inter-word; } #placeDetail-visitBtn { background: #0000ff; background-image: linear-gradient(to bottom, #0000ff, #000020); border: 0px; - border-radius: 10px; - font-weight: 800; + border-radius: 6px; + font-weight: 700; color: #ffffff; - font-size: 20px; - padding: 3px 22px 3px 22px; + font-size: 14px; + padding: 2px 22px 2px 22px; text-decoration: none; + width: 90%; } #placeDetail-visitBtn:hover { @@ -774,7 +775,57 @@ font.domain-nbrUser_small { #placeDetail-visitBtn-container { width: 100%; text-align: left; - margin-bottom: 40px; + margin-bottom: 8px; +} + +#placeDetail-rezPortalBtn { + background: #0000ff; + background-image: linear-gradient(to bottom, #0000ff, #000020); + border: 0px; + border-radius: 6px; + font-weight: 700; + color: #ffffff; + font-size: 14px; + padding: 2px 22px 2px 22px; + text-decoration: none; + width: 90%; +} + +#placeDetail-rezPortalBtn:hover { + background: #057eff; + background-image: linear-gradient(to bottom, #057eff, #00090f); + text-decoration: none; +} + +#placeDetail-rezPortalBtn-container { + width: 100%; + text-align: left; + margin-bottom: 8px; +} + +#placeDetail-copyPlaceURLBtn { + background: #0000ff; + background-image: linear-gradient(to bottom, #0000ff, #000020); + border: 0px; + border-radius: 6px; + font-weight: 700; + color: #ffffff; + font-size: 14px; + padding: 2px 22px 2px 22px; + text-decoration: none; + width: 90%; +} + +#placeDetail-copyPlaceURLBtn:hover { + background: #057eff; + background-image: linear-gradient(to bottom, #057eff, #00090f); + text-decoration: none; +} + +#placeDetail-copyPlaceURLBtn-container { + width: 100%; + text-align: left; + margin-bottom: 8px; } #placeDetail-placedata { @@ -804,7 +855,7 @@ font.domain-nbrUser_small { #placeDetail-users { font-size: 30px; - font-weight: 600; + font-weight: 600; } #placeDetail-capacity { diff --git a/scripts/system/places/places.html b/scripts/system/places/places.html index fda67f4066..6b7727c2d4 100644 --- a/scripts/system/places/places.html +++ b/scripts/system/places/places.html @@ -4,7 +4,7 @@ // places.html // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // html for the ui of the Places application. // @@ -107,7 +107,7 @@
    ×
    - +
    @@ -118,6 +118,8 @@ * * - * + * * *
    +
    +
    DOMAIN: @@ -502,6 +504,28 @@ } + function rezPortal(name, address, placeID) { + var portalOrder = { + "channel": channel, + "action": "REQUEST_PORTAL", + "name": name, + "address": address, + "placeID": placeID + }; + EventBridge.emitWebEvent(JSON.stringify(portalOrder)); + + } + + function copyPlaceURL(address) { + var portalOrder = { + "channel": channel, + "action": "COPY_URL", + "address": address + }; + EventBridge.emitWebEvent(JSON.stringify(portalOrder)); + + } + function goHome() { var message = { "channel": channel, @@ -751,12 +775,17 @@ } } - document.getElementById("placeDetail-image").src = ""; + var pictureUrl = ""; if (placeDetail.thumbnail === "") { - document.getElementById("placeDetail-image").src = "icons/placeholder_" + placeDetail.metaverseRegion + ".jpg"; + pictureUrl = "icons/placeholder_" + placeDetail.metaverseRegion + ".jpg"; } else { - document.getElementById("placeDetail-image").src = placeDetail.thumbnail; + pictureUrl = placeDetail.thumbnail; } + document.getElementById("placeDetail-image").style.backgroundImage = "url(" + pictureUrl + ")"; + document.getElementById("placeDetail-image").style.backgroundRepeat = "no-repeat"; + document.getElementById("placeDetail-image").style.backgroundPosition = "center center"; + document.getElementById("placeDetail-image").style.backgroundSize = "cover"; + document.getElementById("placeDetail-placeName").innerHTML = placeDetail.name; document.getElementById("placeDetail-managers").innerHTML = "By
        " + placeDetail.managers; document.getElementById("placeDetail-description").innerHTML = placeDetail.description; @@ -766,6 +795,8 @@ placeUrl = "hifi://" + placeDetail.address; } document.getElementById("placeDetail-visitBtn-container").innerHTML = ""; + document.getElementById("placeDetail-rezPortalBtn-container").innerHTML = ""; + document.getElementById("placeDetail-copyPlaceURLBtn-container").innerHTML = ""; document.getElementById("placeDetail-maturity").innerHTML = placeDetail.maturity.toUpperCase(); document.getElementById("placeDetail-maturity").className = placeDetail.maturity + "FilterOn placeMaturity"; document.getElementById("placeDetail-domain").innerHTML = placeDetail.domain.toUpperCase(); diff --git a/scripts/system/places/places.js b/scripts/system/places/places.js index fa22d536b7..5aa8d282b1 100644 --- a/scripts/system/places/places.js +++ b/scripts/system/places/places.js @@ -3,7 +3,7 @@ // places.js // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022-2023 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // Generate an explore app based on the differents source of placename data. // @@ -36,6 +36,12 @@ var APP_ICON_ACTIVE = ROOT + "icons/appicon_a.png"; var appStatus = false; var channel = "com.overte.places"; + + var portalChannelName = "com.overte.places.portalRezzer"; + var MAX_DISTANCE_TO_CONSIDER_PORTAL = 100.0; //in meters + var PORTAL_DURATION_MILLISEC = 45000; //45 sec + var rezzerPortalCount = 0; + var MAX_REZZED_PORTAL = 15; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -92,6 +98,24 @@ Window.location = messageObj.address; } + } else if (messageObj.action === "REQUEST_PORTAL" && (n - timestamp) > INTERCALL_DELAY) { + d = new Date(); + timestamp = d.getTime(); + var portalPosition = Vec3.sum(MyAvatar.feetPosition, Vec3.multiplyQbyV(MyAvatar.orientation, {"x": 0.0, "y": 0.0, "z": -2.0})); + var requestToSend = { + "action": "REZ_PORTAL", + "position": portalPosition, + "url": messageObj.address, + "name": messageObj.name, + "placeID": messageObj.placeID + }; + Messages.sendMessage(portalChannelName, JSON.stringify(requestToSend), false); + + } else if (messageObj.action === "COPY_URL" && (n - timestamp) > INTERCALL_DELAY) { + d = new Date(); + timestamp = d.getTime(); + Window.copyToClipboard(messageObj.address); + Window.displayAnnouncement("Place URL copied."); } else if (messageObj.action === "GO_HOME" && (n - timestamp) > INTERCALL_DELAY) { d = new Date(); timestamp = d.getTime(); @@ -284,8 +308,8 @@ region = "local"; order = "A"; fetch = true; - pinned = false; - currentFound = true; + pinned = false; + currentFound = true; } else { region = "federation"; order = "F"; @@ -555,6 +579,57 @@ } //####### END of seed random library ################ + function onMessageReceived(paramChannel, paramMessage, paramSender, paramLocalOnly) { + if (paramChannel === portalChannelName) { + var instruction = JSON.parse(paramMessage); + if (instruction.action === "REZ_PORTAL") { + generatePortal(instruction.position, instruction.url, instruction.name, instruction.placeID); + } + } + } + + function generatePortal(position, url, name, placeID) { + if (rezzerPortalCount <= MAX_REZZED_PORTAL) { + var TOLERANCE_FACTOR = 1.1; + if (Vec3.distance(MyAvatar.position, position) < MAX_DISTANCE_TO_CONSIDER_PORTAL) { + var height = MyAvatar.userHeight * MyAvatar.scale * TOLERANCE_FACTOR; + + var portalPosition = Vec3.sum(position, {"x": 0.0, "y": height/2, "z": 0.0}); + var dimensions = {"x": height * 0.618, "y": height, "z": height * 0.618}; + var userdata = { + "url": url, + "name": name, + "placeID": placeID + }; + + var portalID = Entities.addEntity({ + "position": portalPosition, + "dimensions": dimensions, + "type": "Shape", + "shape": "Sphere", + "name": "Portal to " + name, + "canCastShadow": false, + "collisionless": true, + "userData": JSON.stringify(userdata), + "script": ROOT + "portal.js", + "visible": "false", + "grab": { + "grabbable": false + } + }, "local"); + rezzerPortalCount = rezzerPortalCount + 1; + + Script.setTimeout(function () { + Entities.deleteEntity(portalID); + rezzerPortalCount = rezzerPortalCount - 1; + if (rezzerPortalCount < 0) { + rezzerPortalCount = 0; + } + }, PORTAL_DURATION_MILLISEC); + } + } + } + function cleanup() { if (appStatus) { @@ -562,9 +637,15 @@ tablet.webEventReceived.disconnect(onAppWebEventReceived); } + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(portalChannelName); + tablet.screenChanged.disconnect(onScreenChanged); tablet.removeButton(button); } + Messages.subscribe(portalChannelName); + Messages.messageReceived.connect(onMessageReceived); + Script.scriptEnding.connect(cleanup); }()); diff --git a/scripts/system/places/portal.js b/scripts/system/places/portal.js new file mode 100644 index 0000000000..c77fbc648d --- /dev/null +++ b/scripts/system/places/portal.js @@ -0,0 +1,201 @@ +// +// portal.js +// +// Created by Alezia Kurdis, January 14th, 2025. +// Copyright 2025, Overte e.V. +// +// 3D portal for Places app. portal spawner. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function(){ + + var ROOT = Script.resolvePath('').split("portal.js")[0]; + var portalURL = ""; + var portalName = ""; + var TP_SOUND = SoundCache.getSound(ROOT + "sounds/teleportSound.mp3"); + + this.preload = function(entityID) { + + var properties = Entities.getEntityProperties(entityID, ["userData", "dimensions"]); + var userDataObj = JSON.parse(properties.userData); + portalURL = userDataObj.url; + portalName = userDataObj.name; + var portalColor = getColorFromPlaceID(userDataObj.placeID); + + var textLocalPosition = {"x": 0.0, "y": (properties.dimensions.y / 2) * 1.2, "z": 0.0}; + var scale = textLocalPosition.y/1.2; + var textID = Entities.addEntity({ + "type": "Text", + "parentID": entityID, + "localPosition": textLocalPosition, + "dimensions": { + "x": 1 * scale, + "y": 0.15 * scale, + "z": 0.01 + }, + "name": portalName, + "text": portalName, + "textColor": portalColor.light, + "lineHeight": 0.10 * scale, + "backgroundAlpha": 0.0, + "unlit": true, + "alignment": "center", + "verticalAlignment": "center", + "canCastShadow": false, + "billboardMode": "yaw", + "grab": { + "grabbable": false + } + },"local"); + + var fxID = Entities.addEntity({ + "type": "ParticleEffect", + "parentID": entityID, + "localPosition": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "name": "PORTAL_FX", + "dimensions": { + "x": 5.2 * scale, + "y": 5.2 * scale, + "z": 5.2 * scale + }, + "grab": { + "grabbable": false + }, + "shapeType": "ellipsoid", + "color": portalColor.light, + "alpha": 0.1, + "textures": ROOT + "icons/portalFX.png", + "maxParticles": 600, + "lifespan": 0.6, + "emitRate": 1000, + "emitSpeed": -1 * scale, + "speedSpread": 0 * scale, + "emitOrientation": { + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "emitDimensions": { + "x": 1.28 * scale, + "y": 2 * scale, + "z": 1.28 * scale + }, + "polarFinish": 3.1415927410125732, + "emitAcceleration": { + "x": 0, + "y": 0, + "z": 0 + }, + "particleRadius": 0.4000000059604645 * scale, + "radiusSpread": 0.30000001192092896 * scale, + "radiusStart": 1 * scale, + "radiusFinish": 0 * scale, + "colorStart": portalColor.saturated, + "colorFinish": { + "red": 255, + "green": 255, + "blue": 255 + }, + "alphaSpread": 0.019999999552965164, + "alphaStart": 0, + "alphaFinish": 0.20000000298023224, + "emitterShouldTrail": true, + "particleSpin": 1.5700000524520874, + "spinSpread": 2.9700000286102295, + "spinStart": 0, + "spinFinish": 0 + },"local"); + + var loopSoundID = Entities.addEntity({ + "type": "Sound", + "parentID": entityID, + "localPosition": {"x": 0.0, "y": 0.0, "z": 0.0}, + "name": "PORTAL SOUND", + "soundURL": ROOT + "sounds/portalSound.mp3", + "volume": 0.15, + "loop": true, + "positional": true, + "localOnly": true + },"local"); + + } + + this.enterEntity = function(entityID) { + var injectorOptions = { + "position": MyAvatar.position, + "volume": 0.3, + "loop": false, + "localOnly": true + }; + var injector = Audio.playSound(TP_SOUND, injectorOptions); + + var timer = Script.setTimeout(function () { + Window.location = portalURL; + Entities.deleteEntity(entityID); + }, 1000); + + }; + + function getColorFromPlaceID(placeID) { + var idIntegerConstant = getStringScore(placeID); + var hue = (idIntegerConstant%360)/360; + var color = hslToRgb(hue, 1, 0.5); + var colorLight = hslToRgb(hue, 1, 0.75); + return { + "saturated": {"red": color[0], "green": color[1], "blue": color[2]}, + "light": {"red": colorLight[0], "green": colorLight[1], "blue": colorLight[2]}, + }; + } + + function getStringScore(str) { + var score = 0; + for (var j = 0; j < str.length; j++){ + score += str.charCodeAt(j); + } + return score; + } + + /* + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h, s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param {number} h The hue + * @param {number} s The saturation + * @param {number} l The lightness + * @return {Array} The RGB representation + */ + function hslToRgb(h, s, l){ + var r, g, b; + + if(s == 0){ + r = g = b = l; // achromatic + }else{ + var hue2rgb = function hue2rgb(p, q, t){ + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + } + +}) diff --git a/scripts/system/places/sounds/portalSound.mp3 b/scripts/system/places/sounds/portalSound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5e7f5a9bd0f170da8994e137599daed2dcd7349b GIT binary patch literal 83172 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

    _$9H7q?sv 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

  • DV!sKC)kcT+!@y3hgJ>X-B*5Z|@C7~~BhN@} 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 F{4aUD&$<8r literal 0 HcmV?d00001 diff --git a/scripts/system/places/sounds/teleportSound.mp3 b/scripts/system/places/sounds/teleportSound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e5000e55b57821d16d313ca575411428a6b033c3 GIT binary patch literal 53079 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 bCDHr0vKlEzMJf~$)y7cIfIx8jKePV #include #include +#include "SerDes.h" QTEST_GUILESS_MAIN(KtxTests) @@ -31,6 +32,19 @@ QString getRootPath() { return result; } +#if 0 +ktx::Byte* serializeSPH(ktx::Byte* data, const gpu::IrradianceKTXPayload &payload) const { + *(ktx::IrradianceKTXPayload::Version*)data = IrradianceKTXPayload::CURRENT_VERSION; + data += sizeof(ktx::IrradianceKTXPayload::Version); + + memcpy(data, &payload._irradianceSH, sizeof(ktx::SphericalHarmonics)); + data += sizeof(SphericalHarmonics); + + return data + PADDING; +} +#endif + + void KtxTests::initTestCase() { } @@ -147,6 +161,14 @@ void KtxTests::testKtxSerialization() { testTexture->setKtxBacking(TEST_IMAGE_KTX.fileName().toStdString()); } + +void KtxTests::testKtxNewSerializationSphericalHarmonics() { + DataSerializer ser; + + +} + + #if 0 static const QString TEST_FOLDER { "H:/ktx_cacheold" }; diff --git a/tests/ktx/src/KtxTests.h b/tests/ktx/src/KtxTests.h index 5627dc313d..c59fc17ccc 100644 --- a/tests/ktx/src/KtxTests.h +++ b/tests/ktx/src/KtxTests.h @@ -16,6 +16,7 @@ private slots: void testKtxEvalFunctions(); void testKhronosCompressionFunctions(); void testKtxSerialization(); + void testKtxNewSerializationSphericalHarmonics(); }; diff --git a/tests/shared/src/SerializerTests.cpp b/tests/shared/src/SerializerTests.cpp new file mode 100644 index 0000000000..ae0198f573 --- /dev/null +++ b/tests/shared/src/SerializerTests.cpp @@ -0,0 +1,232 @@ +// +// SerializerTests.cpp +// +// Copyright 2022 Dale Glass +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "SerializerTests.h" +#include +#include +#include + +QTEST_GUILESS_MAIN(SerializerTests) + + +void SerializerTests::initTestCase() { +} + +void SerializerTests::testCreate() { + DataSerializer s; + QCOMPARE(s.length(), 0); + QCOMPARE(s.capacity(), DataSerializer::DEFAULT_SIZE); + QCOMPARE(s.isEmpty(), true); + + + DataDeserializer d(s); + QCOMPARE(d.length(), 0); +} + +void SerializerTests::testAdd() { + DataSerializer s; + s << (qint8)1; + QCOMPARE(s.length(), 1); + QCOMPARE(s.isEmpty(), false); + + s << (quint8)-1; + QCOMPARE(s.length(), 2); + + s << (qint16)0xaabb; + QCOMPARE(s.length(), 4); + + s << (quint16)-18000; + QCOMPARE(s.length(), 6); + + s << (qint32)0xCCDDEEFF; + QCOMPARE(s.length(), 10); + + s << (quint32)-1818000000; + QCOMPARE(s.length(), 14); + + s << "Hello, world!"; + QCOMPARE(s.length(), 28); + + glm::vec3 v{1.f,2.f,3.f}; + s << v; + QCOMPARE(s.length(), 40); + + s << 1.2345f; + QCOMPARE(s.length(), 44); + + + qDebug() << s; +} + +void SerializerTests::testAddAndRead() { + DataSerializer s; + glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; + glm::vec3 v3_b; + glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; + glm::vec4 v4_b; + glm::ivec2 iv2_a{10, 24}; + glm::ivec2 iv2_b; + float f_a = 1.2345f; + float f_b; + + s << (qint8)1; + s << (qint16)0xaabb; + s << (qint32)0xccddeeff; + s << v3_a; + s << v4_a; + s << iv2_a; + s << f_a; + + qint8 i8; + qint16 i16; + qint32 i32; + + DataDeserializer d(s); + + d >> i8; + d >> i16; + d >> i32; + d >> v3_b; + d >> v4_b; + d >> iv2_b; + d >> f_b; + + qDebug() << d; + + QCOMPARE(i8, (qint8)1); + QCOMPARE(i16, (qint16)0xaabb); + QCOMPARE(i32, (qint32)0xccddeeff); + QCOMPARE(v3_a, v3_b); + QCOMPARE(v4_a, v4_b); + QCOMPARE(iv2_a, iv2_b); + QCOMPARE(f_a, f_b); +} + +void SerializerTests::testReadPastEnd() { + DataSerializer s; + qint8 i8; + qint16 i16; + s << (qint8)1; + + DataDeserializer d(s); + d >> i8; + QCOMPARE(d.pos(), 1); + + d.rewind(); + d >> i16; + QCOMPARE(d.pos(), 0); +} + +void SerializerTests::testWritePastEnd() { + qint8 i8 = 255; + qint16 i16 = 65535; + + + char buf[16]; + + + // 1 byte buffer, we can write 1 byte + memset(buf, 0, sizeof(buf)); + DataSerializer s1(buf, 1); + s1 << i8; + QCOMPARE(s1.pos(), 1); + QCOMPARE(s1.isOverflow(), false); + QCOMPARE(buf[0], i8); + + // 1 byte buffer, we can't write 2 bytes + memset(buf, 0, sizeof(buf)); + DataSerializer s2(buf, 1); + s2 << i16; + QCOMPARE(s2.pos(), 0); + QCOMPARE(s2.isOverflow(), true); + QCOMPARE(buf[0], 0); // We didn't write + QCOMPARE(buf[1], 0); +} + + + + +void SerializerTests::benchmarkEncodingDynamicAlloc() { + QBENCHMARK { + DataSerializer s; + glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; + glm::vec3 v3_b; + glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; + glm::vec4 v4_b; + glm::ivec2 iv2_a{10, 24}; + glm::ivec2 iv2_b; + + s << (qint8)1; + s << (qint16)0xaabb; + s << (qint32)0xccddeeff; + s << v3_a; + s << v4_a; + s << iv2_a; + } +} + +void SerializerTests::benchmarkEncodingStaticAlloc() { + char buf[1024]; + + QBENCHMARK { + DataSerializer s(buf, sizeof(buf)); + glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; + glm::vec3 v3_b; + glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; + glm::vec4 v4_b; + glm::ivec2 iv2_a{10, 24}; + glm::ivec2 iv2_b; + + s << (qint8)1; + s << (qint16)0xaabb; + s << (qint32)0xccddeeff; + s << v3_a; + s << v4_a; + s << iv2_a; + } +} + + +void SerializerTests::benchmarkDecoding() { + DataSerializer s; + qint8 q8 = 1; + qint16 q16 = 0xaabb; + qint32 q32 = 0xccddeeff; + + glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; + glm::vec3 v3_b; + glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; + glm::vec4 v4_b; + glm::ivec2 iv2_a{10, 24}; + glm::ivec2 iv2_b; + + s << q8; + s << q16; + s << q32; + s << v3_a; + s << v4_a; + s << iv2_a; + + + QBENCHMARK { + DataDeserializer d(s); + d >> q8; + d >> q16; + d >> q32; + d >> v3_a; + d >> v4_a; + d >> iv2_a; + } +} + + +void SerializerTests::cleanupTestCase() { +} + diff --git a/tests/shared/src/SerializerTests.h b/tests/shared/src/SerializerTests.h new file mode 100644 index 0000000000..55da84c41a --- /dev/null +++ b/tests/shared/src/SerializerTests.h @@ -0,0 +1,33 @@ +// +// ResourceTests.h +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef overte_SerializerTests_h +#define overte_SerializerTests_h + +#include +#include + +class SerializerTests : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void testCreate(); + void testAdd(); + void testAddAndRead(); + void testReadPastEnd(); + void testWritePastEnd(); + void benchmarkEncodingDynamicAlloc(); + void benchmarkEncodingStaticAlloc(); + void benchmarkDecoding(); + void cleanupTestCase(); +private: + +}; + +#endif // overte_SerializerTests_h diff --git a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 index 12a56ced03..eb26d1b426 100644 --- a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 +++ b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 @@ -1,8 +1,8 @@ -# Copyright 2022-2023 Overte e.V. +# Copyright 2022-2024 Overte e.V. # SPDX-License-Identifier: Apache-2.0 # Docker file for building Overte -# Example build: docker build -t overte/overte-full-build:0.1.1-ubuntu-20.04 -f Dockerfile_build_ubuntu-20.04 . +# Example build: docker build -t overte/overte-full-build:0.1.2-ubuntu-20.04 -f Dockerfile_build_ubuntu-20.04 . FROM ubuntu:20.04 LABEL maintainer="Julian Groß (julian.gro@overte.org)" LABEL description="Development image for full Overte builds" @@ -18,6 +18,8 @@ RUN apt-get update && apt-get -y install tzdata RUN apt-get -y install curl ninja-build git cmake g++ libssl-dev python3-distutils python3-distro mesa-common-dev libgl1-mesa-dev libsystemd-dev # Install server-console build dependencies RUN apt-get -y install npm +# Install Interface dependencies +RUN apt-get -y install pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 vulkan-validationlayers libvulkan-dev libvulkan1 # Install tools for package creation RUN apt-get -y install sudo chrpath binutils dh-make diff --git a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 index a80207471e..a4da419e87 100644 --- a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 +++ b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 @@ -1,8 +1,8 @@ -# Copyright 2022-2023 Overte e.V. +# Copyright 2022-2024 Overte e.V. # SPDX-License-Identifier: Apache-2.0 # Docker file for building Overte -# Example build: docker build -t overte/overte-full-build:0.1.1-ubuntu-22.04 -f Dockerfile_build_ubuntu-22.04 . +# Example build: docker build -t overte/overte-full-build:0.1.2-ubuntu-22.04 -f Dockerfile_build_ubuntu-22.04 . FROM ubuntu:22.04 LABEL maintainer="Julian Groß (julian.gro@overte.org)" LABEL description="Development image for full Overte builds" @@ -19,7 +19,7 @@ RUN apt-get -y install curl ninja-build git cmake g++ libssl-dev libqt5websocket # Install Overte tools build dependencies RUN apt-get -y install libqt5webchannel5-dev qtwebengine5-dev libqt5xmlpatterns5-dev # Install Overte Interface build dependencies -RUN apt-get -y install libqt5svg5-dev qttools5-dev +RUN apt-get -y install libqt5svg5-dev qttools5-dev vulkan-validationlayers libvulkan-dev libvulkan1 libqt5x11extras5-dev qtbase5-private-dev # Install server-console build dependencies RUN apt-get -y install npm diff --git a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 deleted file mode 100644 index ef5ef24819..0000000000 --- a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022-2024 Overte e.V. -# SPDX-License-Identifier: Apache-2.0 - -# Docker file for building Overte Server -# Example build: docker build -t overte/overte-server-build:0.1.4-fedora-39 -f Dockerfile_build_fedora-39 . -FROM fedora:39 -LABEL maintainer="Julian Groß (julian.gro@overte.org)" -LABEL description="Development image for Overte Domain server and assignment clients." - -# Install Overte domain-server and assignment-client build dependencies -RUN dnf -y install curl ninja-build git cmake gcc gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel python3.11 - -# Install additional build tools -RUN dnf -y install zip unzip - -# Install tools for package creation -RUN dnf -y install chrpath rpmdevtools - -# Install tools needed for our Github Actions Workflow -Run dnf -y install python3-boto3 python3-pygithub diff --git a/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 b/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 index 8c7e32f360..5639d662b6 100644 --- a/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 +++ b/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 @@ -4,11 +4,11 @@ # - Check which commit you are building https://invent.kde.org/qt/qt/qt5/-/tree/kde/5.15 # - Adjust this file to include the commit hash you are building, the date, the number of threads you want to use (-j10), the platform, and the Qt and QtWebEngine versions. # Keep in mind that building Qt requires a lot of memory. You should have over 1.2GiB of system memory available per thread. -# - Run the build process with something like `PROGRESS_NO_TRUNC=1 DOCKER_BUILDKIT=1 BUILDKIT_STEP_LOG_MAX_SIZE=-1 docker build --progress plain -t overte-qt5:5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371 -f Dockerfile_Ubuntu_20.04_Qt5 .` +# - Run the build process with something like `PROGRESS_NO_TRUNC=1 DOCKER_BUILDKIT=1 BUILDKIT_STEP_LOG_MAX_SIZE=-1 docker build --progress plain -t overte-qt5:5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3 -f Dockerfile_Ubuntu_20.04_Qt5 .` # Buildkit is used to cache intermittent steps in case you need to modify something afterwards. # - Once the build has completed, create a container from the image and export the created Qt package. -# `docker create --name extract overte-qt5:5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371` -# `docker cp extract:qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz /path/on/host` +# `docker create --name extract overte-qt5:5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3` +# `docker cp extract:qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz /path/on/host` # `docker rm extract` FROM ubuntu:20.04 @@ -47,7 +47,7 @@ RUN apt-get -y install git python gperf flex bison pkg-config mesa-utils libgl1- RUN mkdir qt5-install && mkdir qt5-build WORKDIR qt5-build -RUN ../qt5/configure -force-debug-info -release -opensource -confirm-license -platform linux-g++ -recheck-all -nomake tests -nomake examples -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtcharts -skip qtx11extras -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -skip qtlottie -skip qtquick3d -skip qtpim -skip qtdocgallery -no-warnings-are-errors -no-pch -no-icu -prefix ../qt5-install +RUN ../qt5/configure -force-debug-info -release -opensource -confirm-license -platform linux-g++ -recheck-all -nomake tests -nomake examples -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtcharts -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -skip qtlottie -skip qtquick3d -skip qtpim -skip qtdocgallery -no-warnings-are-errors -no-pch -no-icu -prefix ../qt5-install RUN NINJAFLAGS='-j6' make -j6 @@ -58,12 +58,12 @@ WORKDIR ../../qt5-install RUN find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \; # Overwrite QtWebengine version to work around version conflicts -RUN find . -name \Qt5WebEngine*Config.cmake -exec sed -i '' -e 's/5\.15\.17/5\.15\.14/g' {} \; -RUN cp lib/libQt5WebEngine.so.5.15.17 lib/libQt5WebEngine.so.5.15.14 -RUN cp lib/libQt5WebEngineCore.so.5.15.17 lib/libQt5WebEngineCore.so.5.15.14 -RUN cp lib/libQt5WebEngineWidgets.so.5.15.17 lib/libQt5WebEngineWidgets.so.5.15.14 -RUN cp lib/libQt5Pdf.so.5.15.17 lib/libQt5Pdf.so.5.15.14 -RUN cp lib/libQt5PdfWidgets.so.5.15.17 lib/libQt5PdfWidgets.so.5.15.14 +RUN find . -name \Qt5WebEngine*Config.cmake -exec sed -i '' -e 's/5\.15\.19/5\.15\.16/g' {} \; +RUN cp lib/libQt5WebEngine.so.5.15.19 lib/libQt5WebEngine.so.5.15.16 +RUN cp lib/libQt5WebEngineCore.so.5.15.19 lib/libQt5WebEngineCore.so.5.15.16 +RUN cp lib/libQt5WebEngineWidgets.so.5.15.19 lib/libQt5WebEngineWidgets.so.5.15.16 +RUN cp lib/libQt5Pdf.so.5.15.19 lib/libQt5Pdf.so.5.15.16 +RUN cp lib/libQt5PdfWidgets.so.5.15.19 lib/libQt5PdfWidgets.so.5.15.16 COPY ./qt.conf ./bin/ @@ -71,4 +71,4 @@ COPY ./qt.conf ./bin/ RUN cp ../qt5-build/config.summary ./ WORKDIR .. -RUN XZ_OPT='-T0' tar -Jcvf qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz qt5-install +RUN XZ_OPT='-T0' tar -Jcvf qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz qt5-install From d46c3600ac4de2bde10e625fc91ddb55edfe95a8 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Wed, 12 Feb 2025 10:43:38 -0600 Subject: [PATCH 25/25] Revert "Rebase" (#117) This reverts commit 6cc11fdd402055044c4ed6de9b3196320d5f6382. --- .github/workflows/linux_server_build.yml | 16 +- .github/workflows/master_build.yml | 4 +- .github/workflows/master_deploy_apidocs.yml | 2 +- .github/workflows/master_deploy_doxygen.yml | 2 +- .github/workflows/pr_build.yml | 8 +- .github/workflows/release_build.yml | 1 + .gitignore | 1 - CHANGELOG.md | 84 +- hifi_qt.py | 2 +- .../dialogs/graphics/GraphicsSettings.qml | 30 - interface/src/Application.cpp | 56 +- interface/src/Application.h | 11 - interface/src/CameraRootTransformNode.cpp | 48 - interface/src/CameraRootTransformNode.h | 20 - interface/src/avatar/MyAvatar.cpp | 13 - interface/src/avatar/MyAvatar.h | 4 - .../src/raypick/PickScriptingInterface.cpp | 4 - .../scripting/RenderScriptingInterface.cpp | 7 - .../src/scripting/RenderScriptingInterface.h | 18 +- interface/src/ui/PreferencesDialog.cpp | 13 +- .../src/avatars-renderer/Avatar.cpp | 31 +- .../src/RenderableZoneEntityItem.cpp | 47 +- .../entities/src/EntityItemPropertiesDocs.cpp | 4 +- libraries/entities/src/EntityTypes.h | 6 +- libraries/gpu/src/gpu/Batch.h | 4 - libraries/gpu/src/gpu/Texture.h | 73 +- libraries/gpu/src/gpu/Texture_ktx.cpp | 124 +-- .../src/material-networking/TextureCache.cpp | 12 - .../model-serializers/src/GLTFSerializer.cpp | 2 +- libraries/octree/src/OctreePacketData.cpp | 9 +- .../procedural/src/procedural/Procedural.cpp | 220 +--- .../procedural/src/procedural/Procedural.h | 1 - .../src/AmbientOcclusionEffect.cpp | 5 +- .../src/AmbientOcclusionStage.cpp | 48 +- .../render-utils/src/AmbientOcclusionStage.h | 65 +- .../src/AssembleLightingStageTask.cpp | 1 - .../render-utils/src/BackgroundStage.cpp | 65 +- libraries/render-utils/src/BackgroundStage.h | 64 +- libraries/render-utils/src/BloomEffect.cpp | 5 +- libraries/render-utils/src/BloomStage.cpp | 51 +- libraries/render-utils/src/BloomStage.h | 66 +- .../src/DeferredLightingEffect.cpp | 17 +- libraries/render-utils/src/DrawHaze.cpp | 4 +- libraries/render-utils/src/FadeEffect.cpp | 3 +- libraries/render-utils/src/FadeEffectJobs.cpp | 3 +- libraries/render-utils/src/HazeStage.cpp | 54 +- libraries/render-utils/src/HazeStage.h | 66 +- .../render-utils/src/HighlightEffect.cpp | 10 +- libraries/render-utils/src/LightClusters.cpp | 8 +- libraries/render-utils/src/LightClusters.h | 2 +- libraries/render-utils/src/LightPayload.cpp | 8 +- libraries/render-utils/src/LightStage.cpp | 74 +- libraries/render-utils/src/LightStage.h | 124 ++- .../render-utils/src/RenderCommonTask.cpp | 7 +- libraries/render-utils/src/RenderCommonTask.h | 3 - .../render-utils/src/RenderDeferredTask.cpp | 5 +- .../render-utils/src/RenderForwardTask.cpp | 5 +- .../render-utils/src/SubsurfaceScattering.cpp | 4 +- .../src/ToneMapAndResampleTask.cpp | 5 +- .../render-utils/src/TonemappingStage.cpp | 48 +- libraries/render-utils/src/TonemappingStage.h | 65 +- libraries/render-utils/src/ZoneRenderer.cpp | 21 +- libraries/render-utils/src/sdf_text3D.slh | 4 +- libraries/render-utils/src/text/Font.cpp | 2 +- libraries/render-utils/src/text/Font.h | 4 +- libraries/render/src/render/DrawStatus.cpp | 3 +- .../render/src/render/HighlightStage.cpp | 49 +- libraries/render/src/render/HighlightStage.h | 52 +- libraries/render/src/render/HighlightStyle.h | 2 - libraries/render/src/render/Scene.cpp | 12 +- libraries/render/src/render/Stage.cpp | 14 +- libraries/render/src/render/Stage.h | 127 +-- libraries/render/src/render/StageSetup.h | 35 - .../render/src/render/TransitionStage.cpp | 46 +- libraries/render/src/render/TransitionStage.h | 46 +- libraries/shared/CMakeLists.txt | 3 +- libraries/shared/src/AACube.h | 16 - libraries/shared/src/BlendshapeConstants.h | 19 - libraries/shared/src/PickFilter.h | 5 +- libraries/shared/src/SerDes.cpp | 85 -- libraries/shared/src/SerDes.h | 953 ------------------ scripts/system/create/edit.js | 5 - scripts/system/domainChat/domainChat.js | 3 +- scripts/system/graphicsSettings.js | 54 +- scripts/system/places/icons/portalFX.png | Bin 79835 -> 0 bytes scripts/system/places/places.css | 67 +- scripts/system/places/places.html | 41 +- scripts/system/places/places.js | 87 +- scripts/system/places/portal.js | 201 ---- scripts/system/places/sounds/portalSound.mp3 | Bin 83172 -> 0 bytes .../system/places/sounds/teleportSound.mp3 | Bin 53079 -> 0 bytes server-console/src/main.js | 2 +- tests/ktx/src/KtxTests.cpp | 22 - tests/ktx/src/KtxTests.h | 1 - tests/shared/src/SerializerTests.cpp | 232 ----- tests/shared/src/SerializerTests.h | 33 - .../linux-ci/Dockerfile_build_ubuntu-20.04 | 6 +- .../linux-ci/Dockerfile_build_ubuntu-22.04 | 6 +- .../rpm_package/Dockerfile_build_fedora-39 | 20 + tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 | 22 +- 100 files changed, 1156 insertions(+), 2906 deletions(-) delete mode 100644 interface/src/CameraRootTransformNode.cpp delete mode 100644 interface/src/CameraRootTransformNode.h delete mode 100644 libraries/render/src/render/StageSetup.h delete mode 100644 libraries/shared/src/SerDes.cpp delete mode 100644 libraries/shared/src/SerDes.h delete mode 100644 scripts/system/places/icons/portalFX.png delete mode 100644 scripts/system/places/portal.js delete mode 100644 scripts/system/places/sounds/portalSound.mp3 delete mode 100644 scripts/system/places/sounds/teleportSound.mp3 delete mode 100644 tests/shared/src/SerializerTests.cpp delete mode 100644 tests/shared/src/SerializerTests.h create mode 100644 tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 diff --git a/.github/workflows/linux_server_build.yml b/.github/workflows/linux_server_build.yml index 084287c25b..64f1529221 100644 --- a/.github/workflows/linux_server_build.yml +++ b/.github/workflows/linux_server_build.yml @@ -82,6 +82,16 @@ jobs: arch: aarch64 runner: [self_hosted, type-cax41, image-arm-app-docker-ce] + - os: fedora-39 + image: docker.io/overte/overte-server-build:0.1.4-fedora-39-amd64 + arch: amd64 + runner: [self_hosted, type-cx52, image-x86-app-docker-ce] + + - os: fedora-39 + image: docker.io/overte/overte-server-build:0.1.4-fedora-39-aarch64 + arch: aarch64 + runner: [self_hosted, type-cax41, image-arm-app-docker-ce] + - os: fedora-40 image: docker.io/overte/overte-server-build:0.1.4-fedora-40-amd64 arch: amd64 @@ -214,6 +224,10 @@ jobs: else # RPM if [ "${{ matrix.os }}" == "rockylinux-9" ]; then echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.el9.$INSTALLER_EXT" >> $GITHUB_ENV + elif [ "${{ matrix.os }}" == "fedora-38" ]; then + echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc38.$INSTALLER_EXT" >> $GITHUB_ENV + elif [ "${{ matrix.os }}" == "fedora-39" ]; then + echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc39.$INSTALLER_EXT" >> $GITHUB_ENV elif [ "${{ matrix.os }}" == "fedora-40" ]; then echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc40.$INSTALLER_EXT" >> $GITHUB_ENV else @@ -252,7 +266,7 @@ jobs: - name: Archive cmake logs if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: cmake-logs-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.number }}.tar.xz path: cmake-logs-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.number }}.tar.xz diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml index 2704ea16d4..8754c08696 100644 --- a/.github/workflows/master_build.yml +++ b/.github/workflows/master_build.yml @@ -65,7 +65,7 @@ jobs: run: | echo "UPLOAD_PREFIX=build/overte/master" >> $GITHUB_ENV - echo "{github_sha_short}={`echo $GIT_COMMIT | cut -c1-7`}" >> $GITHUB_OUTPUT + echo ::set-output name=github_sha_short::`echo $GIT_COMMIT | cut -c1-7` echo "JOB_NAME=build (${{matrix.os}}, ${{matrix.build_type}})" >> $GITHUB_ENV echo "APP_TARGET_NAME=$APP_NAME" >> $GITHUB_ENV # Linux build variables @@ -82,7 +82,7 @@ jobs: echo "ZIP_ARGS=-r" >> $GITHUB_ENV echo "INSTALLER_EXT=dmg" >> $GITHUB_ENV echo "CMAKE_EXTRA=-DOVERTE_CPU_ARCHITECTURE= -DCMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=OFF -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -DOPENSSL_LIBRARIES=/usr/local/opt/openssl/lib -G Xcode" >> $GITHUB_ENV - echo "symbols_archive=${BUILD_NUMBER}-${{ matrix.build_type }}-mac-symbols.zip" >> $GITHUB_ENV + echo "::set-output name=symbols_archive::${BUILD_NUMBER}-${{ matrix.build_type }}-mac-symbols.zip" echo "APP_TARGET_NAME=Overte" >> $GITHUB_ENV fi # Windows build variables diff --git a/.github/workflows/master_deploy_apidocs.yml b/.github/workflows/master_deploy_apidocs.yml index a4c501ac5a..79d4533b40 100644 --- a/.github/workflows/master_deploy_apidocs.yml +++ b/.github/workflows/master_deploy_apidocs.yml @@ -31,7 +31,7 @@ jobs: jsdoc root.js -r api-mainpage.md -c config.json -d output - name: Deploy API-docs - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.4 with: server: www531.your-server.de protocol: ftps diff --git a/.github/workflows/master_deploy_doxygen.yml b/.github/workflows/master_deploy_doxygen.yml index d2caa15d18..9a12a165a8 100644 --- a/.github/workflows/master_deploy_doxygen.yml +++ b/.github/workflows/master_deploy_doxygen.yml @@ -29,7 +29,7 @@ jobs: doxygen Doxyfile - name: Deploy Doxygen - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: SamKirkland/FTP-Deploy-Action@v4.3.4 with: server: www531.your-server.de protocol: ftps diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index c9d06d5663..b1b8470bf0 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -64,8 +64,8 @@ jobs: runner: [self_hosted, type-cx52, image-x86-app-docker-ce] arch: amd64 build_type: full - # apt-dependencies: # add missing dependencies to docker image when convenient - image: docker.io/overte/overte-full-build:0.1.2-ubuntu-20.04-amd64 + apt-dependencies: pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 # add missing dependencies to docker image when convenient + image: docker.io/overte/overte-full-build:0.1.1-ubuntu-20.04-amd64 # Android builds are currently failing #- os: ubuntu-18.04 # build_type: android @@ -75,7 +75,7 @@ jobs: runner: [self_hosted, type-cax41, image-arm-app-docker-ce] arch: aarch64 build_type: full - image: docker.io/overte/overte-full-build:0.1.2-ubuntu-22.04-aarch64 + image: docker.io/overte/overte-full-build:0.1.1-ubuntu-22.04-aarch64 fail-fast: false runs-on: ${{matrix.runner}} container: ${{matrix.image}} @@ -244,7 +244,7 @@ jobs: - name: Archive cmake logs if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: cmake-logs-${{ matrix.os }}-${{ github.event.number }}.tar.xz path: ./cmake-logs-${{ matrix.os }}-${{ github.event.number }}.tar.xz diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index b72075017c..a678053632 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -56,6 +56,7 @@ jobs: echo "UPLOAD_PREFIX=build/overte/release/" >> $GITHUB_ENV fi + echo ::set-output name=github_sha_short::`echo $GIT_COMMIT | cut -c1-7` echo "JOB_NAME=${{matrix.os}}, ${{matrix.build_type}}" >> $GITHUB_ENV echo "APP_TARGET_NAME=$APP_NAME" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index 6c35659bf3..90ff187027 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,6 @@ local.properties !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -.cache # Workspace *.code-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index c95c32c268..de11f33c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,84 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), This project does **not** adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2024.11.1] 2024.11.23 + -### Fixes -- Hard code link colors in Armored Chat (PR1083) -- ArmoredChat: Alleviate scrolling issue (PR1106) -- Armored Chat: Change the 'open in new window' character (PR1084) -- Fix mouselook ignoring setting. (PR1081) -- Fix controllerScripts uncaught exception. (PR1086) -- Fix wireshark dissector (PR1088) -- Create App: Material Assistant: Add Mtoon, Shader_simple, missing PBR properties and bug fixes. (PR1091) -- Fix login failure handling and improve logging. (PR1093) -- add a setting to workaround the GLES colorspace conversion issue (PR1105) -- Improve model load priority (PR1085) -- fix accidentally clearing url fields when you don't have view permission (PR1138) -- Avatar App: Fixed lingering references to now deleted QML element (PR1155) -- Fix selfie mode movement (PR1127) -- Fix Create App not honoring menu bar actions (PR1123) -- Fix Uuid.NULL behavior (PR1168) -- Rebuild fonts with full charset (NOT -allglyphs) (PR1172) -- fix web entities not accepting keyboard focus (PR1187) -- Fix stutter when an object is fading (PR1185) -- fix density max typo (PR1195) -- Fix ArmoredChat quick_message qml dialog colors on light theme systems (PR1196) -- Fix missing properties in Script API (PR1215) -- Fix ArmoredChat scrolling (PR1210) -- Force enable JSDoc to get scripting console autocomplete working on Windows (PR1219) -- Fix lack of entityHostType property (PR1224) -- Fix access-after-delete on leaving domain with entity scripts (PR1230) -- fix fade out not working in forward rendering (PR1234) -- Fix access-after-delete during entity script engine cleanup (PR1236) -- Fix script-related crashes on exiting a domain (PR1251) -- Update privacy policy link (PR1237) - -### Changes -- Replace Floofchat with ArmoredChat (PR961) -- MouseLook.js refactor (PR1004) -- Custom shader fallbacks (PR1058) -- Update outdated language (PR1102) -- Automated entity property serialization (PR1098) -- Create app: highlight avatar entities (PR1152) -- Update Avatar App icons (PR1141) -- Place App: Weekly promoted place (PR1153) -- Change minimum angular velocity to a lower one (PR1171) -- Places App: Persisted Maturity Filter and Default value for Newbies. (PR1164) - -### Additions -- Mirrors + Portals (PR721) -- Entity tags (PR748) -- Web Entity wantsKeyboardFocus (PR814) -- Audio Zone Properties (PR847) -- Ability to smooth model animations (PR889) -- GPU Particles (PR884) -- Unlit Shapes (PR1041) -- Ambient Light Color (PR1043) -- Dump protocol data (PR1087) -- Sound Entities (PR894) -- Zone properties for tonemapping and ambient occlusion (PR1050) -- Add bloom, haze, AO, and procedural shaders to Graphics settings (PR1053) -- Create App: Revolutionary "Paste" Url buttons for the "Create Model", "Create Material" and "Create Voxels" UI (PR1094) -- Text verticalAlignment, send entity property enums as uint8_t, fix text recalculating too often, fix textSize (PR1111) -- Create app: Grab and Equip (PR1160) -- Create App: Add "Paste" button for NewSoundDialog QML (PR1202) -- Added sounds to all incoming chat messages (PR1250) - -### Removals -- Remove (deprecated) attachments (PR1069) -- Remove unused onFirstRun.js (PR1089) -- Remove Google Poly (PR1137) -- Remove hifi screenshare (PR1165) - -### Build System -- Add CLion-style build directories to .gitignore (PR1135) - -### Security -- Sanitize notificationCore text to prevent XSS (PR1078) - - -## [2024.07.1] 2024.07.12 +## [2024.07.1] 2023.07.12 ### Fixes - Fix more warnings (PR1007) @@ -132,7 +57,7 @@ This project does **not** adhere to [Semantic Versioning](https://semver.org/spe - Remove remnants of RELEASE_NAME. (PR1077) -## [2024.06.1] 2024.06.24 +## [2024.06.1] 2023.06.24 ### Fixes - Fix QNetworkRequest::FollowRedirectsAttribute deprecated warning (PR711) @@ -381,6 +306,9 @@ This project does **not** adhere to [Semantic Versioning](https://semver.org/spe - Added a setting to disable snapshot notifications (PR189) - Added a setting to switch between screenshot formats (PR134) +### Removals +- + ### Build system - Fixed "may be used uninitialized" warning for blendtime (PR269) - Updated SPIRV-Cross to sdk-1.3.231.1 (PR271) diff --git a/hifi_qt.py b/hifi_qt.py index 053ff997b7..b4ab5e9b3c 100644 --- a/hifi_qt.py +++ b/hifi_qt.py @@ -157,7 +157,7 @@ def __init__(self, args): u_major = int( distro.major_version() or '0' ) if distro.id() == 'ubuntu' or distro.id() == 'linuxmint': if (distro.id() == 'ubuntu' and u_major == 20) or distro.id() == 'linuxmint' and u_major == 20: - self.qtUrl = self.assets_url + '/dependencies/qt5/qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz' + self.qtUrl = self.assets_url + '/dependencies/qt5/qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz' elif (distro.id() == 'ubuntu' and u_major > 20) or (distro.id() == 'linuxmint' and u_major > 20): self.__no_qt_package_error() else: diff --git a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml index a0804957be..a928b1379f 100644 --- a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml +++ b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml @@ -658,36 +658,6 @@ Flickable { } } } - Item { - Layout.preferredWidth: parent.width - Layout.preferredHeight: 35 - Layout.topMargin: 16 - - HifiStylesUit.RalewayRegular { - id: enableCameraClippingHeader - text: "3rd Person Camera Clipping" - anchors.left: parent.left - anchors.top: parent.top - width: 200 - height: parent.height - size: 16 - color: "#FFFFFF" - } - - HifiControlsUit.CheckBox { - id: enableCameraClipping - checked: Render.cameraClippingEnabled - boxSize: 16 - spacing: -1 - colorScheme: hifi.colorSchemes.dark - anchors.left: enableCameraClippingHeader.right - anchors.leftMargin: 20 - anchors.top: parent.top - onCheckedChanged: { - Render.cameraClippingEnabled = enableCameraClipping.checked; - } - } - } } ColumnLayout { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c5951a2aff..64aba5ce7e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -241,7 +241,6 @@ #include #include #include -#include #include "ResourceRequestObserver.h" @@ -970,7 +969,6 @@ const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; -const bool DEFAULT_SHOW_GRAPHICS_ICON = true; const QString DEFAULT_CURSOR_NAME = "SYSTEM"; const bool DEFAULT_MINI_TABLET_ENABLED = false; const bool DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED = true; @@ -1005,7 +1003,6 @@ Application::Application( _previousSessionCrashed(false), //setupEssentials(parser, false)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), - _cameraClippingEnabled("cameraClippingEnabled", false), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _firstRun(Settings::firstRun, true), @@ -1013,7 +1010,6 @@ Application::Application( _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), - _showGraphicsIconSetting("showGraphicsIcon", DEFAULT_SHOW_GRAPHICS_ICON), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _awayStateWhenFocusLostInVREnabled("awayStateWhenFocusLostInVREnabled", DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED), _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), @@ -2501,17 +2497,6 @@ void Application::initialize(const QCommandLineParser &parser) { DependencyManager::get()->setPrecisionPicking(rayPickID, value); }); - // Setup the camera clipping ray pick - { - _prevCameraClippingEnabled = _cameraClippingEnabled.get(); - auto cameraRayPick = std::make_shared(Vectors::ZERO, -Vectors::UP, - PickFilter(PickScriptingInterface::getPickEntities() | - PickScriptingInterface::getPickLocalEntities()), - MyAvatar::ZOOM_MAX, 0.0f, _prevCameraClippingEnabled); - cameraRayPick->parentTransform = std::make_shared(); - _cameraClippingRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, cameraRayPick); - } - BillboardModeHelpers::setBillboardRotationOperator([](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos, bool rotate90x) { const glm::quat ROTATE_90X = glm::angleAxis(-(float)M_PI_2, Vectors::RIGHT); @@ -3697,7 +3682,9 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("updateCamera"); + glm::vec3 boomOffset; auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; // The render mode is default or mirror if the camera is in mirror mode, assigned further below renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; @@ -3736,16 +3723,6 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); _myCamera.setPosition(extractTranslation(worldCameraMat)); } else { - float boomLength = myAvatar->getBoomLength(); - if (getCameraClippingEnabled()) { - auto result = - DependencyManager::get()->getPrevPickResultTyped(_cameraClippingRayPickID); - if (result && result->doesIntersect()) { - const float CAMERA_CLIPPING_EPSILON = 0.1f; - boomLength = std::min(boomLength, result->distance - CAMERA_CLIPPING_EPSILON); - } - } - glm::vec3 boomOffset = myAvatar->getModelScale() * boomLength * -IDENTITY_FORWARD; _thirdPersonHMDCameraBoomValid = false; if (mode == CAMERA_MODE_THIRD_PERSON) { _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); @@ -3823,19 +3800,7 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { _myCamera.update(); } - renderArgs._cameraMode = (int8_t)mode; - - const bool shouldEnableCameraClipping = - (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_LOOK_AT || mode == CAMERA_MODE_SELFIE) && !isHMDMode() && - getCameraClippingEnabled(); - if (_prevCameraClippingEnabled != shouldEnableCameraClipping) { - if (shouldEnableCameraClipping) { - DependencyManager::get()->enablePick(_cameraClippingRayPickID); - } else { - DependencyManager::get()->disablePick(_cameraClippingRayPickID); - } - _prevCameraClippingEnabled = shouldEnableCameraClipping; - } + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); } void Application::runTests() { @@ -3850,16 +3815,6 @@ void Application::setFieldOfView(float fov) { } } -void Application::setCameraClippingEnabled(bool enabled) { - _cameraClippingEnabled.set(enabled); - _prevCameraClippingEnabled = enabled; - if (enabled) { - DependencyManager::get()->enablePick(_cameraClippingRayPickID); - } else { - DependencyManager::get()->disablePick(_cameraClippingRayPickID); - } -} - void Application::setHMDTabletScale(float hmdTabletScale) { _hmdTabletScale.set(hmdTabletScale); } @@ -3886,11 +3841,6 @@ void Application::setPreferAvatarFingerOverStylus(bool value) { _preferAvatarFingerOverStylusSetting.set(value); } -void Application::setShowGraphicsIcon(bool value) { - _showGraphicsIconSetting.set(value); - DependencyManager::get< MessagesClient >()->sendLocalMessage("Overte-ShowGraphicsIconChanged", ""); -} - void Application::setPreferredCursor(const QString& cursorName) { qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; diff --git a/interface/src/Application.h b/interface/src/Application.h index 2c7a97a5eb..f3cbd351eb 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -246,9 +246,6 @@ class Application : public QApplication, float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); - bool getCameraClippingEnabled() { return _cameraClippingEnabled.get(); } - void setCameraClippingEnabled(bool enabled); - float getHMDTabletScale() { return _hmdTabletScale.get(); } void setHMDTabletScale(float hmdTabletScale); float getDesktopTabletScale() { return _desktopTabletScale.get(); } @@ -266,9 +263,6 @@ class Application : public QApplication, bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } void setPreferAvatarFingerOverStylus(bool value); - bool getShowGraphicsIcon() { return _showGraphicsIconSetting.get(); } - void setShowGraphicsIcon(bool value); - bool getMiniTabletEnabled() { return _miniTabletEnabledSetting.get(); } void setMiniTabletEnabled(bool enabled); @@ -722,7 +716,6 @@ private slots: Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; - Setting::Handle _cameraClippingEnabled; Setting::Handle _hmdTabletScale; Setting::Handle _desktopTabletScale; Setting::Handle _firstRun; @@ -730,7 +723,6 @@ private slots: Setting::Handle _hmdTabletBecomesToolbarSetting; Setting::Handle _preferStylusOverLaserSetting; Setting::Handle _preferAvatarFingerOverStylusSetting; - Setting::Handle _showGraphicsIconSetting; Setting::Handle _constrainToolbarPosition; Setting::Handle _awayStateWhenFocusLostInVREnabled; Setting::Handle _preferredCursor; @@ -897,8 +889,5 @@ private slots: DiscordPresence* _discordPresence{ nullptr }; bool _profilingInitialized { false }; - - bool _prevCameraClippingEnabled { false }; - unsigned int _cameraClippingRayPickID; }; #endif // hifi_Application_h diff --git a/interface/src/CameraRootTransformNode.cpp b/interface/src/CameraRootTransformNode.cpp deleted file mode 100644 index 596bdab3d3..0000000000 --- a/interface/src/CameraRootTransformNode.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// -// Created by HifiExperiments on 10/30/2024 -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "CameraRootTransformNode.h" - -#include "Application.h" -#include "DependencyManager.h" -#include "avatar/AvatarManager.h" -#include "avatar/MyAvatar.h" - -Transform CameraRootTransformNode::getTransform() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - glm::vec3 pos; - glm::quat ori; - - CameraMode mode = qApp->getCamera().getMode(); - if (mode == CAMERA_MODE_FIRST_PERSON || mode == CAMERA_MODE_THIRD_PERSON) { - pos = myAvatar->getDefaultEyePosition(); - ori = myAvatar->getHeadOrientation(); - } else if (mode == CAMERA_MODE_FIRST_PERSON_LOOK_AT) { - pos = myAvatar->getCameraEyesPosition(0.0f); - ori = myAvatar->getLookAtRotation(); - } else { - ori = myAvatar->getLookAtRotation(); - pos = myAvatar->getLookAtPivotPoint(); - - if (mode == CAMERA_MODE_SELFIE) { - ori = ori * glm::angleAxis(PI, ori * Vectors::UP); - } - } - - ori = ori * glm::angleAxis(-PI / 2.0f, Vectors::RIGHT); - - glm::vec3 scale = glm::vec3(myAvatar->scaleForChildren()); - return Transform(ori, scale, pos); -} - -QVariantMap CameraRootTransformNode::toVariantMap() const { - QVariantMap map; - map["joint"] = "CameraRoot"; - return map; -} diff --git a/interface/src/CameraRootTransformNode.h b/interface/src/CameraRootTransformNode.h deleted file mode 100644 index 6a0f58f42e..0000000000 --- a/interface/src/CameraRootTransformNode.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by HifiExperiments on 10/30/2024 -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_CameraRootTransformNode_h -#define hifi_CameraRootTransformNode_h - -#include "TransformNode.h" - -class CameraRootTransformNode : public TransformNode { -public: - CameraRootTransformNode() {} - Transform getTransform() override; - QVariantMap toVariantMap() const override; -}; - -#endif // hifi_CameraRootTransformNode_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index cbe2b8e77f..1779d64dc6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -75,8 +75,6 @@ #include "WarningsSuppression.h" #include "ScriptPermissions.h" -#include "Application.h" - using namespace std; const float DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES = 30.0f; @@ -228,7 +226,6 @@ MyAvatar::MyAvatar(QThread* thread) : _scaleSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "scale", _targetScale), _yawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "yawSpeed", _yawSpeed), _hmdYawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hmdYawSpeed", _hmdYawSpeed), - _cameraSensitivitySetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "cameraSensitivity", qApp->getCamera().getSensitivity()), _pitchSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "pitchSpeed", _pitchSpeed), _fullAvatarURLSetting(QStringList() << SETTINGS_FULL_PRIVATE_GROUP_NAME << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()), @@ -1327,7 +1324,6 @@ void MyAvatar::saveData() { _scaleSetting.set(_targetScale); _yawSpeedSetting.set(_yawSpeed); _hmdYawSpeedSetting.set(_hmdYawSpeed); - _cameraSensitivitySetting.set(getCameraSensitivity()); _pitchSpeedSetting.set(_pitchSpeed); // only save the fullAvatarURL if it has not been overwritten on command line @@ -2088,7 +2084,6 @@ void MyAvatar::loadData() { _yawSpeed = _yawSpeedSetting.get(_yawSpeed); _hmdYawSpeed = _hmdYawSpeedSetting.get(_hmdYawSpeed); - setCameraSensitivity(_cameraSensitivitySetting.get(getCameraSensitivity())); _pitchSpeed = _pitchSpeedSetting.get(_pitchSpeed); _prefOverrideAnimGraphUrl.set(_animGraphURLSetting.get().toString()); @@ -7007,11 +7002,3 @@ void MyAvatar::resetPointAt() { POINT_BLEND_LINEAR_ALPHA_NAME, POINT_ALPHA_BLENDING); } } - -float MyAvatar::getCameraSensitivity() const { - return qApp->getCamera().getSensitivity(); -} - -void MyAvatar::setCameraSensitivity(float cameraSensitivity) { - qApp->getCamera().setSensitivity(cameraSensitivity); -} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a3586db6db..2777d2c82f 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1407,9 +1407,6 @@ class MyAvatar : public Avatar { float getHMDYawSpeed() const { return _hmdYawSpeed; } void setHMDYawSpeed(float speed) { _hmdYawSpeed = speed; } - float getCameraSensitivity() const; - void setCameraSensitivity(float cameraSensitivity); - static const float ZOOM_MIN; static const float ZOOM_MAX; static const float ZOOM_DEFAULT; @@ -3010,7 +3007,6 @@ private slots: Setting::Handle _scaleSetting; Setting::Handle _yawSpeedSetting; Setting::Handle _hmdYawSpeedSetting; - Setting::Handle _cameraSensitivitySetting; Setting::Handle _pitchSpeedSetting; Setting::Handle _fullAvatarURLSetting; Setting::Handle _fullAvatarModelNameSetting; diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index 221b0fcb63..5323c52faf 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -21,7 +21,6 @@ #include "ParabolaPick.h" #include "CollisionPick.h" -#include "CameraRootTransformNode.h" #include "SpatialParentFinder.h" #include "PickTransformNode.h" #include "MouseTransformNode.h" @@ -538,9 +537,6 @@ void PickScriptingInterface::setParentTransform(std::shared_ptr pick, } else if (joint == "Avatar") { pick->parentTransform = std::make_shared(); return; - } else if (joint == "CameraRoot") { - pick->parentTransform = std::make_shared(); - return; } else { parentUuid = myAvatar->getSessionUUID(); parentJointIndex = myAvatar->getJointIndex(joint); diff --git a/interface/src/scripting/RenderScriptingInterface.cpp b/interface/src/scripting/RenderScriptingInterface.cpp index db3e398547..e126a5734e 100644 --- a/interface/src/scripting/RenderScriptingInterface.cpp +++ b/interface/src/scripting/RenderScriptingInterface.cpp @@ -342,13 +342,6 @@ void RenderScriptingInterface::setVerticalFieldOfView(float fieldOfView) { } } -void RenderScriptingInterface::setCameraClippingEnabled(bool enabled) { - if (qApp->getCameraClippingEnabled() != enabled) { - qApp->setCameraClippingEnabled(enabled); - emit settingsChanged(); - } -} - QStringList RenderScriptingInterface::getScreens() const { QStringList screens; diff --git a/interface/src/scripting/RenderScriptingInterface.h b/interface/src/scripting/RenderScriptingInterface.h index 97d7868a96..56b474cf31 100644 --- a/interface/src/scripting/RenderScriptingInterface.h +++ b/interface/src/scripting/RenderScriptingInterface.h @@ -37,7 +37,6 @@ * they're disabled. * @property {integer} antialiasingMode - The active anti-aliasing mode. * @property {number} viewportResolutionScale - The view port resolution scale, > 0.0. - * @property {boolean} cameraClippingEnabled - true if third person camera clipping is enabled, false if it's disabled. */ class RenderScriptingInterface : public QObject { Q_OBJECT @@ -50,7 +49,6 @@ class RenderScriptingInterface : public QObject { Q_PROPERTY(AntialiasingConfig::Mode antialiasingMode READ getAntialiasingMode WRITE setAntialiasingMode NOTIFY settingsChanged) Q_PROPERTY(float viewportResolutionScale READ getViewportResolutionScale WRITE setViewportResolutionScale NOTIFY settingsChanged) Q_PROPERTY(float verticalFieldOfView READ getVerticalFieldOfView WRITE setVerticalFieldOfView NOTIFY settingsChanged) - Q_PROPERTY(bool cameraClippingEnabled READ getCameraClippingEnabled WRITE setCameraClippingEnabled NOTIFY settingsChanged) public: RenderScriptingInterface(); @@ -263,21 +261,7 @@ public slots: * @function Render.setVerticalFieldOfView * @param {number} fieldOfView - The vertical field of view in degrees to set. */ - void setVerticalFieldOfView(float fieldOfView); - - /*@jsdoc - * Gets whether or not third person camera clipping is enabled. - * @function Render.getCameraClippingEnabled - * @returns {boolean} true if camera clipping is enabled, false if it's disabled. - */ - bool getCameraClippingEnabled() { return qApp->getCameraClippingEnabled(); } - - /*@jsdoc - * Sets whether or not third person camera clipping is enabled. - * @function Render.setCameraClippingEnabled - * @param {boolean} enabled - true to enable third person camera clipping, false to disable. - */ - void setCameraClippingEnabled(bool enabled); + void setVerticalFieldOfView( float fieldOfView ); signals: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 623aede2fa..65e4daa5e0 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -225,13 +225,7 @@ void setupPreferences() { preferences->addPreference(delaySlider); } - { - auto getter = []() -> bool { return qApp->getShowGraphicsIcon(); }; - auto setter = [](bool value) { qApp->setShowGraphicsIcon(value); }; - preferences->addPreference(new CheckPreference(UI_CATEGORY, "Show Graphics icon on tablet and toolbar", getter, setter)); - } - - static const QString VIEW_CATEGORY { "View" }; + static const QString VIEW_CATEGORY{ "View" }; { auto getter = [myAvatar]()->float { return myAvatar->getRealWorldFieldOfView(); }; auto setter = [myAvatar](float value) { myAvatar->setRealWorldFieldOfView(value); }; @@ -249,11 +243,6 @@ void setupPreferences() { preference->setStep(1); preferences->addPreference(preference); } - { - auto getter = []()->bool { return qApp->getCameraClippingEnabled(); }; - auto setter = [](bool value) { qApp->setCameraClippingEnabled(value); }; - preferences->addPreference(new CheckPreference(VIEW_CATEGORY, "Enable 3rd Person Camera Clipping?", getter, setter)); - } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index fb22db3ce0..4b63dcb932 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -994,9 +994,9 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const QString statsFormat = QString("(%1 Kbps, %2 Hz)"); if (!renderedDisplayName.isEmpty()) { - statsFormat.append("\n"); + statsFormat.prepend(" - "); } - renderedDisplayName = statsFormat.arg(QString::number(kilobitsPerSecond, 'f', 2)).arg(getReceiveRate()) + renderedDisplayName; + renderedDisplayName += statsFormat.arg(QString::number(kilobitsPerSecond, 'f', 2)).arg(getReceiveRate()); } // Compute display name extent/position offset @@ -1010,18 +1010,18 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const // Compute background position/size static const float SLIGHTLY_IN_FRONT = 0.1f; static const float BORDER_RELATIVE_SIZE = 0.1f; - // static const float BEVEL_FACTOR = 0.1f; + static const float BEVEL_FACTOR = 0.1f; const int border = BORDER_RELATIVE_SIZE * nameDynamicRect.height(); - // FIXME: Beveled box is broken - // const int left = text_x - border; - // const int bottom = text_y - border; - // const int width = nameDynamicRect.width() + 2.0f * border; + const int left = text_x - border; + const int bottom = text_y - border; + const int width = nameDynamicRect.width() + 2.0f * border; const int height = nameDynamicRect.height() + 2.0f * border; - // const int bevelDistance = BEVEL_FACTOR * height; + const int bevelDistance = BEVEL_FACTOR * height; // Display name and background colors glm::vec4 textColor(0.93f, 0.93f, 0.93f, _displayNameAlpha); - glm::vec4 backgroundColor(0.2f, 0.2f, 0.2f,(_displayNameAlpha / DISPLAYNAME_ALPHA) * DISPLAYNAME_BACKGROUND_ALPHA); + glm::vec4 backgroundColor(0.2f, 0.2f, 0.2f, + (_displayNameAlpha / DISPLAYNAME_ALPHA) * DISPLAYNAME_BACKGROUND_ALPHA); // Compute display name transform auto textTransform = calculateDisplayNameTransform(view, textPosition); @@ -1029,11 +1029,12 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const textTransform.postScale(1.0f / height); batch.setModelTransform(textTransform); - // { - // PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); - // DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true, forward); - // DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, bevelDistance, backgroundColor, _nameRectGeometryID); - // } + { + PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); + DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true, forward); + DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, + bevelDistance, backgroundColor, _nameRectGeometryID); + } // Render actual name QByteArray nameUTF8 = renderedDisplayName.toLocal8Bit(); @@ -1043,7 +1044,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const batch.setModelTransform(textTransform); { PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderText"); - displayNameRenderer->draw(batch, { nameUTF8.data(), textColor, { text_x, -text_y }, glm::vec2(-1.0f), TextAlignment::LEFT, forward }); + displayNameRenderer->draw(batch, { nameUTF8.data(), textColor, { text_x, -text_y }, glm::vec2(-1.0f), forward }); } } } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 593a3c020b..178e122c32 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -4,7 +4,6 @@ // // Created by Clement on 4/22/15. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -40,46 +39,46 @@ ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { if (_stage) { if (!LightStage::isIndexInvalid(_sunIndex)) { - _stage->removeElement(_sunIndex); + _stage->removeLight(_sunIndex); _sunIndex = INVALID_INDEX; } if (!LightStage::isIndexInvalid(_ambientIndex)) { - _stage->removeElement(_ambientIndex); + _stage->removeLight(_ambientIndex); _ambientIndex = INVALID_INDEX; } } if (_backgroundStage) { if (!BackgroundStage::isIndexInvalid(_backgroundIndex)) { - _backgroundStage->removeElement(_backgroundIndex); + _backgroundStage->removeBackground(_backgroundIndex); _backgroundIndex = INVALID_INDEX; } } if (_hazeStage) { if (!HazeStage::isIndexInvalid(_hazeIndex)) { - _hazeStage->removeElement(_hazeIndex); + _hazeStage->removeHaze(_hazeIndex); _hazeIndex = INVALID_INDEX; } } if (_bloomStage) { if (!BloomStage::isIndexInvalid(_bloomIndex)) { - _bloomStage->removeElement(_bloomIndex); + _bloomStage->removeBloom(_bloomIndex); _bloomIndex = INVALID_INDEX; } } if (_tonemappingStage) { if (!TonemappingStage::isIndexInvalid(_tonemappingIndex)) { - _tonemappingStage->removeElement(_tonemappingIndex); + _tonemappingStage->removeTonemapping(_tonemappingIndex); _tonemappingIndex = INVALID_INDEX; } } if (_ambientOcclusionStage) { if (!AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) { - _ambientOcclusionStage->removeElement(_ambientOcclusionIndex); + _ambientOcclusionStage->removeAmbientOcclusion(_ambientOcclusionIndex); _ambientOcclusionIndex = INVALID_INDEX; } } @@ -124,7 +123,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { // Sun if (_needSunUpdate) { if (LightStage::isIndexInvalid(_sunIndex)) { - _sunIndex = _stage->addElement(_sunLight); + _sunIndex = _stage->addLight(_sunLight); } else { _stage->updateLightArrayBuffer(_sunIndex); } @@ -137,7 +136,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { if (_needAmbientUpdate) { if (LightStage::isIndexInvalid(_ambientIndex)) { - _ambientIndex = _stage->addElement(_ambientLight); + _ambientIndex = _stage->addLight(_ambientLight); } else { _stage->updateLightArrayBuffer(_ambientIndex); } @@ -150,7 +149,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { if (_needBackgroundUpdate) { if (BackgroundStage::isIndexInvalid(_backgroundIndex)) { - _backgroundIndex = _backgroundStage->addElement(_background); + _backgroundIndex = _backgroundStage->addBackground(_background); } _needBackgroundUpdate = false; } @@ -159,7 +158,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needHazeUpdate) { if (HazeStage::isIndexInvalid(_hazeIndex)) { - _hazeIndex = _hazeStage->addElement(_haze); + _hazeIndex = _hazeStage->addHaze(_haze); } _needHazeUpdate = false; } @@ -168,7 +167,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needBloomUpdate) { if (BloomStage::isIndexInvalid(_bloomIndex)) { - _bloomIndex = _bloomStage->addElement(_bloom); + _bloomIndex = _bloomStage->addBloom(_bloom); } _needBloomUpdate = false; } @@ -177,7 +176,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needTonemappingUpdate) { if (TonemappingStage::isIndexInvalid(_tonemappingIndex)) { - _tonemappingIndex = _tonemappingStage->addElement(_tonemapping); + _tonemappingIndex = _tonemappingStage->addTonemapping(_tonemapping); } _needTonemappingUpdate = false; } @@ -186,7 +185,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { { if (_needAmbientOcclusionUpdate) { if (AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) { - _ambientOcclusionIndex = _ambientOcclusionStage->addElement(_ambientOcclusion); + _ambientOcclusionIndex = _ambientOcclusionStage->addAmbientOcclusion(_ambientOcclusion); } _needAmbientOcclusionUpdate = false; } @@ -206,9 +205,9 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { } if (_skyboxMode == COMPONENT_MODE_DISABLED) { - _backgroundStage->_currentFrame.pushElement(INVALID_INDEX); + _backgroundStage->_currentFrame.pushBackground(INVALID_INDEX); } else if (_skyboxMode == COMPONENT_MODE_ENABLED) { - _backgroundStage->_currentFrame.pushElement(_backgroundIndex); + _backgroundStage->_currentFrame.pushBackground(_backgroundIndex); } if (_ambientLightMode == COMPONENT_MODE_DISABLED) { @@ -219,25 +218,25 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { // Haze only if the mode is not inherit, as the model deals with on/off if (_hazeMode != COMPONENT_MODE_INHERIT) { - _hazeStage->_currentFrame.pushElement(_hazeIndex); + _hazeStage->_currentFrame.pushHaze(_hazeIndex); } if (_bloomMode == COMPONENT_MODE_DISABLED) { - _bloomStage->_currentFrame.pushElement(INVALID_INDEX); + _bloomStage->_currentFrame.pushBloom(INVALID_INDEX); } else if (_bloomMode == COMPONENT_MODE_ENABLED) { - _bloomStage->_currentFrame.pushElement(_bloomIndex); + _bloomStage->_currentFrame.pushBloom(_bloomIndex); } if (_tonemappingMode == COMPONENT_MODE_DISABLED) { - _tonemappingStage->_currentFrame.pushElement(0); // Use the fallback tonemapping for "off" + _tonemappingStage->_currentFrame.pushTonemapping(0); // Use the fallback tonemapping for "off" } else if (_tonemappingMode == COMPONENT_MODE_ENABLED) { - _tonemappingStage->_currentFrame.pushElement(_tonemappingIndex); + _tonemappingStage->_currentFrame.pushTonemapping(_tonemappingIndex); } if (_ambientOcclusionMode == COMPONENT_MODE_DISABLED) { - _ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX); + _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX); } else if (_ambientOcclusionMode == COMPONENT_MODE_ENABLED) { - _ambientOcclusionStage->_currentFrame.pushElement(_ambientOcclusionIndex); + _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(_ambientOcclusionIndex); } } diff --git a/libraries/entities/src/EntityItemPropertiesDocs.cpp b/libraries/entities/src/EntityItemPropertiesDocs.cpp index 8195161b77..8f8959165c 100644 --- a/libraries/entities/src/EntityItemPropertiesDocs.cpp +++ b/libraries/entities/src/EntityItemPropertiesDocs.cpp @@ -935,9 +935,7 @@ * * @typedef {object} Entities.EntityProperties-Image * @property {Vec3} dimensions=0.1,0.1,0.01 - The dimensions of the entity. - * @property {string} imageURL="" - The URL of the image to use. It can also contain a base64 encoded image, in the same format as glTF. - * For network transmitted entities there's about 1000-character limit for the length of this field. For base64 image - * the property string needs to begin with `data:image/png;base64,`, `data:image/jpeg;base64,` or `data:image/webp;base64,`. + * @property {string} imageURL="" - The URL of the image to use. * @property {boolean} emissive=false - true if the image should be emissive (unlit), false if it * shouldn't. * @property {boolean} keepAspectRatio=true - true if the image should maintain its aspect ratio, diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 2b14e417df..ab3233e639 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -2,9 +2,9 @@ // EntityTypes.h // libraries/entities/src // -// Created by Brad Hefta-Gaub on December 4th, 2013. +// Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 High Fidelity, Inc. -// Copyright 2023-2025 Overte e.V. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -91,7 +91,7 @@ class EntityTypes { *
  • "Material"Modifies the existing materials on entities and avatars.{@link Entities.EntityProperties-Material|EntityProperties-Material}
    "Sound"Plays a sound.{@link Entities.EntityProperties-Sound|EntityProperties-Sound}
    {@link Entities.EntityProperties-Material|EntityProperties-Sound}
    * @typedef {string} Entities.EntityType diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index a4e6bf6e05..3cf8184913 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -288,10 +288,6 @@ class Batch { _glUniformMatrix3fv(location, 1, false, glm::value_ptr(v)); } - void _glUniform(int location, const glm::mat4& v) { - _glUniformMatrix4fv(location, 1, false, glm::value_ptr(v)); - } - // Maybe useful but shoudln't be public. Please convince me otherwise // Well porting to gles i need it... void runLambda(std::function f); diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index a6f527e657..907f9ff392 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -24,7 +24,6 @@ #include "Forward.h" #include "Resource.h" #include "Metric.h" -#include "SerDes.h" const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; @@ -92,37 +91,6 @@ class SphericalHarmonics { }; typedef std::shared_ptr< SphericalHarmonics > SHPointer; - -inline DataSerializer &operator<<(DataSerializer &ser, const SphericalHarmonics &h) { - DataSerializer::SizeTracker tracker(ser); - - ser << h.L00 << h.spare0; - ser << h.L1m1 << h.spare1; - ser << h.L10 << h.spare2; - ser << h.L11 << h.spare3; - ser << h.L2m2 << h.spare4; - ser << h.L2m1 << h.spare5; - ser << h.L20 << h.spare6; - ser << h.L21 << h.spare7; - ser << h.L22 << h.spare8; - return ser; -} - -inline DataDeserializer &operator>>(DataDeserializer &des, SphericalHarmonics &h) { - DataDeserializer::SizeTracker tracker(des); - - des >> h.L00 >> h.spare0; - des >> h.L1m1 >> h.spare1; - des >> h.L10 >> h.spare2; - des >> h.L11 >> h.spare3; - des >> h.L2m2 >> h.spare4; - des >> h.L2m1 >> h.spare5; - des >> h.L20 >> h.spare6; - des >> h.L21 >> h.spare7; - des >> h.L22 >> h.spare8; - return des; -} - class Sampler { public: @@ -168,7 +136,7 @@ class Sampler { uint8 _wrapModeU = WRAP_REPEAT; uint8 _wrapModeV = WRAP_REPEAT; uint8 _wrapModeW = WRAP_REPEAT; - + uint8 _mipOffset = 0; uint8 _minMip = 0; uint8 _maxMip = MAX_MIP_LEVEL; @@ -225,35 +193,6 @@ class Sampler { friend class Deserializer; }; -inline DataSerializer &operator<<(DataSerializer &ser, const Sampler::Desc &d) { - DataSerializer::SizeTracker tracker(ser); - ser << d._borderColor; - ser << d._maxAnisotropy; - ser << d._filter; - ser << d._comparisonFunc; - ser << d._wrapModeU; - ser << d._wrapModeV; - ser << d._wrapModeW; - ser << d._mipOffset; - ser << d._minMip; - ser << d._maxMip; - return ser; -} - -inline DataDeserializer &operator>>(DataDeserializer &dsr, Sampler::Desc &d) { - DataDeserializer::SizeTracker tracker(dsr); - dsr >> d._borderColor; - dsr >> d._maxAnisotropy; - dsr >> d._filter; - dsr >> d._comparisonFunc; - dsr >> d._wrapModeU; - dsr >> d._wrapModeV; - dsr >> d._wrapModeW; - dsr >> d._mipOffset; - dsr >> d._minMip; - dsr >> d._maxMip; - return dsr; -} enum class TextureUsageType : uint8 { RENDERBUFFER, // Used as attachments to a framebuffer RESOURCE, // Resource textures, like materials... subject to memory manipulation @@ -291,7 +230,7 @@ class Texture : public Resource { NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 - NUM_FLAGS, + NUM_FLAGS, }; typedef std::bitset Flags; @@ -539,7 +478,7 @@ class Texture : public Resource { uint16 evalMipDepth(uint16 level) const { return std::max(_depth >> level, 1); } // The true size of an image line or surface depends on the format, tiling and padding rules - // + // // Here are the static function to compute the different sizes from parametered dimensions and format // Tile size must be a power of 2 static uint16 evalTiledPadding(uint16 length, int tile) { int tileMinusOne = (tile - 1); return (tileMinusOne - (length + tileMinusOne) % tile); } @@ -568,7 +507,7 @@ class Texture : public Resource { uint32 evalMipFaceNumTexels(uint16 level) const { return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); } uint32 evalMipNumTexels(uint16 level) const { return evalMipFaceNumTexels(level) * getNumFaces(); } - // For convenience assign a source name + // For convenience assign a source name const std::string& source() const { return _source; } void setSource(const std::string& source) { _source = source; } const std::string& sourceHash() const { return _sourceHash; } @@ -694,7 +633,7 @@ class Texture : public Resource { uint16 _maxMipLevel { 0 }; uint16 _minMip { 0 }; - + Type _type { TEX_1D }; Usage _usage; @@ -704,7 +643,7 @@ class Texture : public Resource { bool _isIrradianceValid = false; bool _defined = false; bool _important = false; - + static TexturePointer create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips); diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 2a4d678208..c4b674a917 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -18,7 +18,6 @@ #include #include "GPULogging.h" -#include "SerDes.h" using namespace gpu; @@ -28,94 +27,71 @@ using KtxStorage = Texture::KtxStorage; std::vector, std::shared_ptr>> KtxStorage::_cachedKtxFiles; std::mutex KtxStorage::_cachedKtxFilesMutex; - -/** - * @brief Payload for a KTX (texture) - * - * This contains a ready to use texture. This is both used for the local cache, and for baked textures. - * - * @note The usage for textures means breaking compatibility is a bad idea, and that the implementation - * should just keep on adding extra data at the bottom of the structure, and remain able to read old - * formats. In fact, version 1 KTX can be found in older baked assets. - */ struct GPUKTXPayload { using Version = uint8; static const std::string KEY; static const Version CURRENT_VERSION { 2 }; static const size_t PADDING { 2 }; - static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32_t) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING }; - + static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING }; static_assert(GPUKTXPayload::SIZE == 44, "Packing size may differ between platforms"); + static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned"); Sampler::Desc _samplerDesc; Texture::Usage _usage; TextureUsageType _usageType; glm::ivec2 _originalSize { 0, 0 }; - /** - * @brief Serialize the KTX payload - * - * @warning Be careful modifying this code, as it influences baked assets. - * Backwards compatibility must be maintained. - * - * @param ser Destination serializer - */ - void serialize(DataSerializer &ser) { + Byte* serialize(Byte* data) const { + *(Version*)data = CURRENT_VERSION; + data += sizeof(Version); - ser << CURRENT_VERSION; + memcpy(data, &_samplerDesc, sizeof(Sampler::Desc)); + data += sizeof(Sampler::Desc); - ser << _samplerDesc; + // We can't copy the bitset in Texture::Usage in a crossplateform manner + // So serialize it manually + uint32 usageData = _usage._flags.to_ulong(); + memcpy(data, &usageData, sizeof(uint32)); + data += sizeof(uint32); - uint32_t usageData = (uint32_t)_usage._flags.to_ulong(); - ser << usageData; - ser << ((uint8_t)_usageType); - ser << _originalSize; + memcpy(data, &_usageType, sizeof(TextureUsageType)); + data += sizeof(TextureUsageType); - ser.addPadding(PADDING); + memcpy(data, glm::value_ptr(_originalSize), sizeof(glm::ivec2)); + data += sizeof(glm::ivec2); - assert(ser.length() == GPUKTXPayload::SIZE); + return data + PADDING; } - /** - * @brief Deserialize the KTX payload - * - * @warning Be careful modifying this code, as it influences baked assets. - * Backwards compatibility must be maintained. - * - * @param dsr Deserializer object - * @return true Successful - * @return false Version check failed - */ - bool unserialize(DataDeserializer &dsr) { - Version version = 0; - uint32_t usageData = 0; - uint8_t usagetype = 0; - - dsr >> version; + bool unserialize(const Byte* data, size_t size) { + Version version = *(const Version*)data; + data += sizeof(Version); if (version > CURRENT_VERSION) { // If we try to load a version that we don't know how to parse, // it will render incorrectly - qCWarning(gpulogging) << "KTX version" << version << "is newer than our own," << CURRENT_VERSION; - qCWarning(gpulogging) << dsr; return false; } - dsr >> _samplerDesc; + memcpy(&_samplerDesc, data, sizeof(Sampler::Desc)); + data += sizeof(Sampler::Desc); - dsr >> usageData; - _usage = gpu::Texture::Usage(usageData); + // We can't copy the bitset in Texture::Usage in a crossplateform manner + // So unserialize it manually + uint32 usageData; + memcpy(&usageData, data, sizeof(uint32)); + _usage = Texture::Usage(usageData); + data += sizeof(uint32); - dsr >> usagetype; - _usageType = (TextureUsageType)usagetype; + memcpy(&_usageType, data, sizeof(TextureUsageType)); + data += sizeof(TextureUsageType); if (version >= 2) { - dsr >> _originalSize; + memcpy(&_originalSize, data, sizeof(glm::ivec2)); + data += sizeof(glm::ivec2); } - dsr.skipPadding(PADDING); - return true; } @@ -127,8 +103,7 @@ struct GPUKTXPayload { auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); if (found != keyValues.end()) { auto value = found->_value; - DataDeserializer dsr(value.data(), value.size()); - return payload.unserialize(dsr); + return payload.unserialize(value.data(), value.size()); } return false; } @@ -148,24 +123,29 @@ struct IrradianceKTXPayload { SphericalHarmonics _irradianceSH; - void serialize(DataSerializer &ser) const { - ser << CURRENT_VERSION; - ser << _irradianceSH; - ser.addPadding(PADDING); + Byte* serialize(Byte* data) const { + *(Version*)data = CURRENT_VERSION; + data += sizeof(Version); + + memcpy(data, &_irradianceSH, sizeof(SphericalHarmonics)); + data += sizeof(SphericalHarmonics); + + return data + PADDING; } - bool unserialize(DataDeserializer &des) { - Version version; - if (des.length() != SIZE) { + bool unserialize(const Byte* data, size_t size) { + if (size != SIZE) { return false; } - des >> version; + Version version = *(const Version*)data; if (version != CURRENT_VERSION) { return false; } + data += sizeof(Version); + + memcpy(&_irradianceSH, data, sizeof(SphericalHarmonics)); - des >> _irradianceSH; return true; } @@ -177,8 +157,7 @@ struct IrradianceKTXPayload { auto found = std::find_if(keyValues.begin(), keyValues.end(), isIrradianceKTX); if (found != keyValues.end()) { auto value = found->_value; - DataDeserializer des(value.data(), value.size()); - return payload.unserialize(des); + return payload.unserialize(value.data(), value.size()); } return false; } @@ -488,9 +467,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec gpuKeyval._originalSize = originalSize; Byte keyvalPayload[GPUKTXPayload::SIZE]; - DataSerializer ser(keyvalPayload, sizeof(keyvalPayload)); - - gpuKeyval.serialize(ser); + gpuKeyval.serialize(keyvalPayload); ktx::KeyValues keyValues; keyValues.emplace_back(GPUKTXPayload::KEY, (uint32)GPUKTXPayload::SIZE, (ktx::Byte*) &keyvalPayload); @@ -500,8 +477,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec irradianceKeyval._irradianceSH = *texture.getIrradiance(); Byte irradianceKeyvalPayload[IrradianceKTXPayload::SIZE]; - DataSerializer ser(irradianceKeyvalPayload, sizeof(irradianceKeyvalPayload)); - irradianceKeyval.serialize(ser); + irradianceKeyval.serialize(irradianceKeyvalPayload); keyValues.emplace_back(IrradianceKTXPayload::KEY, (uint32)IrradianceKTXPayload::SIZE, (ktx::Byte*) &irradianceKeyvalPayload); } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index e2d4822543..840fa50a0a 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -254,18 +254,6 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs if (url.scheme() == RESOURCE_SCHEME) { return getResourceTexture(url); } - - QString urlString = url.toString(); - if (content.isEmpty() && (urlString.startsWith("data:image/jpeg;base64,") - || urlString.startsWith("data:image/png;base64,") - || urlString.startsWith("data:image/webp;base64,"))) { - QString binaryUrl = urlString.split(",")[1]; - auto decodedContent = binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); - if (!decodedContent.isEmpty()) { - return getTexture(url, type, decodedContent, maxNumPixels, sourceChannel); - } - } - QString decodedURL = QUrl::fromPercentEncoding(url.toEncoded()); if (decodedURL.startsWith("{")) { return getTextureByUUID(decodedURL); diff --git a/libraries/model-serializers/src/GLTFSerializer.cpp b/libraries/model-serializers/src/GLTFSerializer.cpp index d4cee367d3..1440fb22d2 100644 --- a/libraries/model-serializers/src/GLTFSerializer.cpp +++ b/libraries/model-serializers/src/GLTFSerializer.cpp @@ -1335,7 +1335,7 @@ HFMTexture GLTFSerializer::getHFMTexture(const cgltf_texture *texture) { hfmTex.filename = textureUrl.toEncoded().append(QString::number(imageIndex).toUtf8()); } - if (url.startsWith("data:image/jpeg;base64,") || url.startsWith("data:image/png;base64,") || url.startsWith("data:image/webp;base64,")) { + if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,") || url.contains("data:image/webp;base64,")) { hfmTex.content = requestEmbeddedData(url); } } diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index 3745582728..c13d58226b 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -17,7 +17,6 @@ #include "OctreeLogging.h" #include "NumericalConstants.h" #include -#include "SerDes.h" bool OctreePacketData::_debug = false; AtomicUIntStat OctreePacketData::_totalBytesOfOctalCodes { 0 }; @@ -848,10 +847,10 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QByteA } int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, AACube& result) { - DataDeserializer des(dataBytes, sizeof(aaCubeData)); - des >> result; - - return des.length(); + aaCubeData cube; + memcpy(&cube, dataBytes, sizeof(aaCubeData)); + result = AACube(cube.corner, cube.scale); + return sizeof(aaCubeData); } int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QRect& result) { diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 7b202ad625..3162ab95e6 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -356,53 +356,16 @@ void Procedural::prepare(gpu::Batch& batch, } } // Then fill in every reflections the new custom bindings - size_t customSlot = procedural::slot::uniform::Custom; - _slotMap.clear(); + int customSlot = procedural::slot::uniform::Custom; for (const auto& key : _data.uniforms.keys()) { - bool isArrayUniform = false; - size_t numSlots = 0; - const QJsonValue& value = _data.uniforms[key]; - if (value.isDouble()) { - numSlots = 1; - } else if (value.isArray()) { - const QJsonArray valueArray = value.toArray(); - if (valueArray.size() > 0) { - if (valueArray[0].isArray()) { - const size_t valueLength = valueArray[0].toArray().size(); - size_t count = 0; - for (const QJsonValue& value : valueArray) { - if (value.isArray()) { - const QJsonArray innerValueArray = value.toArray(); - if (innerValueArray.size() == valueLength) { - if (valueLength == 3 || valueLength == 4 || valueLength == 9 || valueLength == 16) { - count++; - isArrayUniform = true; - } - } - } - } - numSlots = count; - } else if (valueArray[0].isDouble()) { - numSlots = 1; - } - } + std::string uniformName = key.toLocal8Bit().data(); + for (auto reflection : allFragmentReflections) { + reflection->uniforms[uniformName] = customSlot; } - - if (numSlots > 0) { - std::string uniformName = key.toLocal8Bit().data(); - std::string trueUniformName = uniformName; - if (isArrayUniform) { - trueUniformName += "[0]"; - } - for (auto reflection : allFragmentReflections) { - reflection->uniforms[trueUniformName] = customSlot; - } - for (auto reflection : allVertexReflections) { - reflection->uniforms[trueUniformName] = customSlot; - } - _slotMap[uniformName] = customSlot; - customSlot += numSlots; + for (auto reflection : allVertexReflections) { + reflection->uniforms[uniformName] = customSlot; } + ++customSlot; } } @@ -485,138 +448,59 @@ void Procedural::prepare(gpu::Batch& batch, } } + void Procedural::setupUniforms() { _uniforms.clear(); // Set any userdata specified uniforms + int slot = procedural::slot::uniform::Custom; for (const auto& key : _data.uniforms.keys()) { - const std::string uniformName = key.toLocal8Bit().data(); - auto slotItr = _slotMap.find(uniformName); - if (slotItr == _slotMap.end()) { - continue; - } - - const size_t slot = slotItr->second; - const QJsonValue& value = _data.uniforms[key]; + std::string uniformName = key.toLocal8Bit().data(); + QJsonValue value = _data.uniforms[key]; if (value.isDouble()) { - const float v = value.toDouble(); + float v = value.toDouble(); _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform1f(slot, v); }); } else if (value.isArray()) { - const QJsonArray valueArray = value.toArray(); - if (valueArray.size() > 0) { - if (valueArray[0].isArray()) { - const size_t valueLength = valueArray[0].toArray().size(); - std::vector vs; - vs.reserve(valueLength * valueArray.size()); - size_t count = 0; - for (const QJsonValue& value : valueArray) { - if (value.isArray()) { - const QJsonArray innerValueArray = value.toArray(); - if (innerValueArray.size() == valueLength) { - if (valueLength == 3 || valueLength == 4 || valueLength == 9 || valueLength == 16) { - for (size_t i = 0; i < valueLength; i++) { - vs.push_back(innerValueArray[i].toDouble()); - } - count++; - } - } - } - } - if (count > 0) { - switch (valueLength) { - case 3: { - _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniform3fv(slot, count, vs.data()); }); - break; - } - case 4: { - _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniform4fv(slot, count, vs.data()); }); - break; - } - case 9: { - _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniformMatrix3fv(slot, count, false, vs.data()); }); - break; - } - case 16: { - _uniforms.push_back([slot, vs, count](gpu::Batch& batch) { batch._glUniformMatrix4fv(slot, count, false, vs.data()); }); - break; - } - default: - break; - } - } - } else if (valueArray[0].isDouble()) { - switch (valueArray.size()) { - case 1: { - const float v = valueArray[0].toDouble(); - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); - break; - } - case 2: { - const glm::vec2 v{ valueArray[0].toDouble(), valueArray[1].toDouble() }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); - break; - } - case 3: { - const glm::vec3 v{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); - break; - } - case 4: { - const glm::vec4 v{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - valueArray[3].toDouble(), - }; - _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform(slot, v); }); - break; - } - case 9: { - const glm::mat3 m{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - valueArray[3].toDouble(), - valueArray[4].toDouble(), - valueArray[5].toDouble(), - valueArray[6].toDouble(), - valueArray[7].toDouble(), - valueArray[8].toDouble(), - }; - _uniforms.push_back([slot, m](gpu::Batch& batch) { batch._glUniform(slot, m); }); - break; - } - case 16: { - const glm::mat4 m{ - valueArray[0].toDouble(), - valueArray[1].toDouble(), - valueArray[2].toDouble(), - valueArray[3].toDouble(), - valueArray[4].toDouble(), - valueArray[5].toDouble(), - valueArray[6].toDouble(), - valueArray[7].toDouble(), - valueArray[8].toDouble(), - valueArray[9].toDouble(), - valueArray[10].toDouble(), - valueArray[11].toDouble(), - valueArray[12].toDouble(), - valueArray[13].toDouble(), - valueArray[14].toDouble(), - valueArray[15].toDouble(), - }; - _uniforms.push_back([slot, m](gpu::Batch& batch) { batch._glUniform(slot, m); }); - break; - } - default: - break; - } - } + auto valueArray = value.toArray(); + switch (valueArray.size()) { + case 0: + break; + + case 1: { + float v = valueArray[0].toDouble(); + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform1f(slot, v); }); + break; + } + + case 2: { + glm::vec2 v{ valueArray[0].toDouble(), valueArray[1].toDouble() }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform2f(slot, v.x, v.y); }); + break; + } + + case 3: { + glm::vec3 v{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform3f(slot, v.x, v.y, v.z); }); + break; + } + + default: + case 4: { + glm::vec4 v{ + valueArray[0].toDouble(), + valueArray[1].toDouble(), + valueArray[2].toDouble(), + valueArray[3].toDouble(), + }; + _uniforms.push_back([slot, v](gpu::Batch& batch) { batch._glUniform4f(slot, v.x, v.y, v.z, v.w); }); + break; + } } } + slot++; } _uniforms.push_back([this](gpu::Batch& batch) { @@ -694,4 +578,4 @@ void graphics::ProceduralMaterial::initializeProcedural() { _procedural._transparentFragmentSource = gpu::Shader::getFragmentShaderSource(shader::render_utils::fragment::simple_procedural_translucent); _procedural._errorFallbackFragmentPath = ":" + QUrl("qrc:///shaders/errorShader.frag").path(); -} +} \ No newline at end of file diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index c1836095a7..3ca2f41f0b 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -190,7 +190,6 @@ struct Procedural { NetworkTexturePointer _channels[MAX_PROCEDURAL_TEXTURE_CHANNELS]; std::unordered_map _vertexReplacements; std::unordered_map _fragmentReplacements; - std::unordered_map _slotMap; std::unordered_map _proceduralPipelines; std::unordered_map _errorPipelines; diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 1a76777888..ab1acc3c91 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -4,7 +4,6 @@ // // Created by Niraj Venkat on 7/15/15. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -607,8 +606,8 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte graphics::AmbientOcclusionPointer ambientOcclusion; if (_debug) { ambientOcclusion = _debugAmbientOcclusion; - } else if (ambientOcclusionStage && ambientOcclusionFrame->_elements.size()) { - ambientOcclusion = ambientOcclusionStage->getElement(ambientOcclusionFrame->_elements.front()); + } else if (ambientOcclusionStage && ambientOcclusionFrame->_ambientOcclusions.size()) { + ambientOcclusion = ambientOcclusionStage->getAmbientOcclusion(ambientOcclusionFrame->_ambientOcclusions.front()); } if (!ambientOcclusion || !lightingModel->isAmbientOcclusionEnabled()) { diff --git a/libraries/render-utils/src/AmbientOcclusionStage.cpp b/libraries/render-utils/src/AmbientOcclusionStage.cpp index 55fa290ab7..6b3763a39c 100644 --- a/libraries/render-utils/src/AmbientOcclusionStage.cpp +++ b/libraries/render-utils/src/AmbientOcclusionStage.cpp @@ -7,8 +7,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "AmbientOcclusionStage.h" -template <> -std::string render::PointerStage::_name { "AMBIENT_OCCLUSION_STAGE" }; +#include + +std::string AmbientOcclusionStage::_stageName { "AMBIENT_OCCLUSION_STAGE" }; +const AmbientOcclusionStage::Index AmbientOcclusionStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + +AmbientOcclusionStage::Index AmbientOcclusionStage::findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const { + auto found = _ambientOcclusionMap.find(ambientOcclusion); + if (found != _ambientOcclusionMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } +} + +AmbientOcclusionStage::Index AmbientOcclusionStage::addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) { + auto found = _ambientOcclusionMap.find(ambientOcclusion); + if (found == _ambientOcclusionMap.end()) { + auto ambientOcclusionId = _ambientOcclusions.newElement(ambientOcclusion); + // Avoid failing to allocate a ambientOcclusion, just pass + if (ambientOcclusionId != INVALID_INDEX) { + // Insert the ambientOcclusion and its index in the reverse map + _ambientOcclusionMap.insert(AmbientOcclusionMap::value_type(ambientOcclusion, ambientOcclusionId)); + } + return ambientOcclusionId; + } else { + return (*found).second; + } +} + +AmbientOcclusionStage::AmbientOcclusionPointer AmbientOcclusionStage::removeAmbientOcclusion(Index index) { + AmbientOcclusionPointer removed = _ambientOcclusions.freeElement(index); + if (removed) { + _ambientOcclusionMap.erase(removed); + } + return removed; +} + +AmbientOcclusionStageSetup::AmbientOcclusionStageSetup() {} + +void AmbientOcclusionStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(AmbientOcclusionStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(AmbientOcclusionStage::getName(), std::make_shared()); + } +} diff --git a/libraries/render-utils/src/AmbientOcclusionStage.h b/libraries/render-utils/src/AmbientOcclusionStage.h index 1b54a0828e..d5dee344ba 100644 --- a/libraries/render-utils/src/AmbientOcclusionStage.h +++ b/libraries/render-utils/src/AmbientOcclusionStage.h @@ -11,17 +11,74 @@ #ifndef hifi_render_utils_AmbientOcclusionStage_h #define hifi_render_utils_AmbientOcclusionStage_h -#include +#include +#include +#include +#include #include -#include + +#include +#include +#include // AmbientOcclusion stage to set up ambientOcclusion-related rendering tasks -class AmbientOcclusionStage : public render::PointerStage {}; +class AmbientOcclusionStage : public render::Stage { +public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using AmbientOcclusionPointer = graphics::AmbientOcclusionPointer; + using AmbientOcclusions = render::indexed_container::IndexedPointerVector; + using AmbientOcclusionMap = std::unordered_map; + + using AmbientOcclusionIndices = std::vector; + + Index findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const; + Index addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion); + + AmbientOcclusionPointer removeAmbientOcclusion(Index index); + + bool checkAmbientOcclusionId(Index index) const { return _ambientOcclusions.checkIndex(index); } + + Index getNumAmbientOcclusions() const { return _ambientOcclusions.getNumElements(); } + Index getNumFreeAmbientOcclusions() const { return _ambientOcclusions.getNumFreeIndices(); } + Index getNumAllocatedAmbientOcclusions() const { return _ambientOcclusions.getNumAllocatedIndices(); } + + AmbientOcclusionPointer getAmbientOcclusion(Index ambientOcclusionId) const { + return _ambientOcclusions.get(ambientOcclusionId); + } + + AmbientOcclusions _ambientOcclusions; + AmbientOcclusionMap _ambientOcclusionMap; + + class Frame { + public: + Frame() {} + + void clear() { _ambientOcclusions.clear(); } + + void pushAmbientOcclusion(AmbientOcclusionStage::Index index) { _ambientOcclusions.emplace_back(index); } + + AmbientOcclusionStage::AmbientOcclusionIndices _ambientOcclusions; + }; + using FramePointer = std::shared_ptr; + + Frame _currentFrame; +}; using AmbientOcclusionStagePointer = std::shared_ptr; -class AmbientOcclusionStageSetup : public render::StageSetup { +class AmbientOcclusionStageSetup { public: using JobModel = render::Job::Model; + + AmbientOcclusionStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: }; #endif diff --git a/libraries/render-utils/src/AssembleLightingStageTask.cpp b/libraries/render-utils/src/AssembleLightingStageTask.cpp index 0646f77d51..bc040582bd 100644 --- a/libraries/render-utils/src/AssembleLightingStageTask.cpp +++ b/libraries/render-utils/src/AssembleLightingStageTask.cpp @@ -1,7 +1,6 @@ // // Created by Samuel Gateau on 2018/12/06 // Copyright 2013-2018 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/render-utils/src/BackgroundStage.cpp b/libraries/render-utils/src/BackgroundStage.cpp index f3f287bdac..455f356a45 100644 --- a/libraries/render-utils/src/BackgroundStage.cpp +++ b/libraries/render-utils/src/BackgroundStage.cpp @@ -3,20 +3,58 @@ // // Created by Sam Gateau on 5/9/2017. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "BackgroundStage.h" #include "DeferredLightingEffect.h" +#include + #include -template <> -std::string render::PointerStage::_name { "BACKGROUND_STAGE" }; +std::string BackgroundStage::_stageName { "BACKGROUND_STAGE" }; +const BackgroundStage::Index BackgroundStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + +BackgroundStage::Index BackgroundStage::findBackground(const BackgroundPointer& background) const { + auto found = _backgroundMap.find(background); + if (found != _backgroundMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } + +} + +BackgroundStage::Index BackgroundStage::addBackground(const BackgroundPointer& background) { + + auto found = _backgroundMap.find(background); + if (found == _backgroundMap.end()) { + auto backgroundId = _backgrounds.newElement(background); + // Avoid failing to allocate a background, just pass + if (backgroundId != INVALID_INDEX) { + + // Insert the background and its index in the reverse map + _backgroundMap.insert(BackgroundMap::value_type(background, backgroundId)); + } + return backgroundId; + } else { + return (*found).second; + } +} + + +BackgroundStage::BackgroundPointer BackgroundStage::removeBackground(Index index) { + BackgroundPointer removed = _backgrounds.freeElement(index); + + if (removed) { + _backgroundMap.erase(removed); + } + return removed; +} + void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { const auto& lightingModel = inputs.get0(); @@ -28,8 +66,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const auto& backgroundStage = renderContext->_scene->getStage(); const auto& backgroundFrame = inputs.get1(); graphics::SkyboxPointer skybox; - if (backgroundStage && backgroundFrame->_elements.size()) { - const auto& background = backgroundStage->getElement(backgroundFrame->_elements.front()); + if (backgroundStage && backgroundFrame->_backgrounds.size()) { + const auto& background = backgroundStage->getBackground(backgroundFrame->_backgrounds.front()); if (background) { skybox = background->getSkybox(); } @@ -60,8 +98,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, // If we're using forward rendering, we need to calculate haze if (args->_renderMethod == render::Args::RenderMethod::FORWARD) { const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_elements.size() > 0) { - const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); + if (hazeStage && hazeFrame->_hazes.size() > 0) { + const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } @@ -73,3 +111,14 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, args->_batch = nullptr; } } + +BackgroundStageSetup::BackgroundStageSetup() { +} + +void BackgroundStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(BackgroundStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(BackgroundStage::getName(), std::make_shared()); + } +} + diff --git a/libraries/render-utils/src/BackgroundStage.h b/libraries/render-utils/src/BackgroundStage.h index 4da8fbf9fb..3015b721b1 100644 --- a/libraries/render-utils/src/BackgroundStage.h +++ b/libraries/render-utils/src/BackgroundStage.h @@ -1,9 +1,8 @@ // // BackgroundStage.h -// + // Created by Sam Gateau on 5/9/2017. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,19 +12,72 @@ #define hifi_render_utils_BackgroundStage_h #include +#include +#include +#include #include -#include - #include "HazeStage.h" + #include "LightingModel.h" + // Background stage to set up background-related rendering tasks -class BackgroundStage : public render::PointerStage {}; +class BackgroundStage : public render::Stage { +public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using BackgroundPointer = graphics::SunSkyStagePointer; + using Backgrounds = render::indexed_container::IndexedPointerVector; + using BackgroundMap = std::unordered_map; + + using BackgroundIndices = std::vector; + + + Index findBackground(const BackgroundPointer& background) const; + Index addBackground(const BackgroundPointer& background); + + BackgroundPointer removeBackground(Index index); + + bool checkBackgroundId(Index index) const { return _backgrounds.checkIndex(index); } + + Index getNumBackgrounds() const { return _backgrounds.getNumElements(); } + Index getNumFreeBackgrounds() const { return _backgrounds.getNumFreeIndices(); } + Index getNumAllocatedBackgrounds() const { return _backgrounds.getNumAllocatedIndices(); } + + BackgroundPointer getBackground(Index backgroundId) const { + return _backgrounds.get(backgroundId); + } + + Backgrounds _backgrounds; + BackgroundMap _backgroundMap; + + class Frame { + public: + Frame() {} + + void clear() { _backgrounds.clear(); } + + void pushBackground(BackgroundStage::Index index) { _backgrounds.emplace_back(index); } + + BackgroundStage::BackgroundIndices _backgrounds; + }; + using FramePointer = std::shared_ptr; + + Frame _currentFrame; +}; using BackgroundStagePointer = std::shared_ptr; -class BackgroundStageSetup : public render::StageSetup { +class BackgroundStageSetup { public: using JobModel = render::Job::Model; + + BackgroundStageSetup(); + void run(const render::RenderContextPointer& renderContext); }; class DrawBackgroundStage { diff --git a/libraries/render-utils/src/BloomEffect.cpp b/libraries/render-utils/src/BloomEffect.cpp index 763f12cf0f..d27403440c 100644 --- a/libraries/render-utils/src/BloomEffect.cpp +++ b/libraries/render-utils/src/BloomEffect.cpp @@ -4,7 +4,6 @@ // // Created by Olivier Prat on 09/25/17. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -44,8 +43,8 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons const auto lightingModel = inputs.get3(); const auto& bloomStage = renderContext->_scene->getStage(); graphics::BloomPointer bloom; - if (bloomStage && bloomFrame->_elements.size()) { - bloom = bloomStage->getElement(bloomFrame->_elements.front()); + if (bloomStage && bloomFrame->_blooms.size()) { + bloom = bloomStage->getBloom(bloomFrame->_blooms.front()); } if (!bloom || (lightingModel && !lightingModel->isBloomEnabled())) { renderContext->taskFlow.abortTask(); diff --git a/libraries/render-utils/src/BloomStage.cpp b/libraries/render-utils/src/BloomStage.cpp index 23ec4596bd..2bedfeea96 100644 --- a/libraries/render-utils/src/BloomStage.cpp +++ b/libraries/render-utils/src/BloomStage.cpp @@ -3,13 +3,56 @@ // // Created by Sam Gondelman on 8/7/2018 // Copyright 2018 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "BloomStage.h" -template <> -std::string render::PointerStage::_name { "BLOOM_STAGE" }; +#include "DeferredLightingEffect.h" + +#include + +std::string BloomStage::_stageName { "BLOOM_STAGE" }; +const BloomStage::Index BloomStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + +BloomStage::Index BloomStage::findBloom(const BloomPointer& bloom) const { + auto found = _bloomMap.find(bloom); + if (found != _bloomMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } +} + +BloomStage::Index BloomStage::addBloom(const BloomPointer& bloom) { + auto found = _bloomMap.find(bloom); + if (found == _bloomMap.end()) { + auto bloomId = _blooms.newElement(bloom); + // Avoid failing to allocate a bloom, just pass + if (bloomId != INVALID_INDEX) { + // Insert the bloom and its index in the reverse map + _bloomMap.insert(BloomMap::value_type(bloom, bloomId)); + } + return bloomId; + } else { + return (*found).second; + } +} + +BloomStage::BloomPointer BloomStage::removeBloom(Index index) { + BloomPointer removed = _blooms.freeElement(index); + if (removed) { + _bloomMap.erase(removed); + } + return removed; +} + +BloomStageSetup::BloomStageSetup() {} + +void BloomStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(BloomStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(BloomStage::getName(), std::make_shared()); + } +} diff --git a/libraries/render-utils/src/BloomStage.h b/libraries/render-utils/src/BloomStage.h index 37e72bacea..bb03e181af 100644 --- a/libraries/render-utils/src/BloomStage.h +++ b/libraries/render-utils/src/BloomStage.h @@ -3,7 +3,6 @@ // Created by Sam Gondelman on 8/7/2018 // Copyright 2018 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,17 +11,74 @@ #ifndef hifi_render_utils_BloomStage_h #define hifi_render_utils_BloomStage_h -#include +#include +#include +#include +#include #include -#include + +#include +#include +#include // Bloom stage to set up bloom-related rendering tasks -class BloomStage : public render::PointerStage {}; +class BloomStage : public render::Stage { +public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using BloomPointer = graphics::BloomPointer; + using Blooms = render::indexed_container::IndexedPointerVector; + using BloomMap = std::unordered_map; + + using BloomIndices = std::vector; + + Index findBloom(const BloomPointer& bloom) const; + Index addBloom(const BloomPointer& bloom); + + BloomPointer removeBloom(Index index); + + bool checkBloomId(Index index) const { return _blooms.checkIndex(index); } + + Index getNumBlooms() const { return _blooms.getNumElements(); } + Index getNumFreeBlooms() const { return _blooms.getNumFreeIndices(); } + Index getNumAllocatedBlooms() const { return _blooms.getNumAllocatedIndices(); } + + BloomPointer getBloom(Index bloomId) const { + return _blooms.get(bloomId); + } + + Blooms _blooms; + BloomMap _bloomMap; + + class Frame { + public: + Frame() {} + + void clear() { _blooms.clear(); } + + void pushBloom(BloomStage::Index index) { _blooms.emplace_back(index); } + + BloomStage::BloomIndices _blooms; + }; + using FramePointer = std::shared_ptr; + + Frame _currentFrame; +}; using BloomStagePointer = std::shared_ptr; -class BloomStageSetup : public render::StageSetup { +class BloomStageSetup { public: using JobModel = render::Job::Model; + + BloomStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: }; #endif diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index f2f6639f88..0ff1c435e1 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -4,7 +4,6 @@ // // Created by Andrzej Kapolka on 9/11/14. // Copyright 2014 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -376,7 +375,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Global directional light, maybe shadow and ambient pass auto lightStage = renderContext->_scene->getStage(); assert(lightStage); - assert(lightStage->getNumElements() > 0); + assert(lightStage->getNumLights() > 0); auto keyLight = lightStage->getCurrentKeyLight(*lightFrame); // Check if keylight casts shadows @@ -392,7 +391,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Global Ambient light graphics::LightPointer ambientLight; if (lightStage && lightFrame->_ambientLights.size()) { - ambientLight = lightStage->getElement(lightFrame->_ambientLights.front()); + ambientLight = lightStage->getLight(lightFrame->_ambientLights.front()); } bool hasAmbientMap = (ambientLight != nullptr); @@ -431,8 +430,8 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Haze const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_elements.size() > 0) { - const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); + if (hazeStage && hazeFrame->_hazes.size() > 0) { + const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } @@ -637,7 +636,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { // Add the global light to the light stage (for later shadow rendering) // Set this light to be the default - _defaultLightID = lightStage->addElement(lp, true); + _defaultLightID = lightStage->addLight(lp, true); } auto backgroundStage = renderContext->_scene->getStage(); @@ -650,7 +649,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { _defaultBackground = background; // Add the global light to the light stage (for later shadow rendering) - _defaultBackgroundID = backgroundStage->addElement(_defaultBackground); + _defaultBackgroundID = backgroundStage->addBackground(_defaultBackground); } } @@ -660,7 +659,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { auto haze = std::make_shared(); _defaultHaze = haze; - _defaultHazeID = hazeStage->addElement(_defaultHaze); + _defaultHazeID = hazeStage->addHaze(_defaultHaze); } } @@ -670,7 +669,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { auto tonemapping = std::make_shared(); _defaultTonemapping = tonemapping; - _defaultTonemappingID = tonemappingStage->addElement(_defaultTonemapping); + _defaultTonemappingID = tonemappingStage->addTonemapping(_defaultTonemapping); } } } diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp index f7e62d75f4..134f12174b 100644 --- a/libraries/render-utils/src/DrawHaze.cpp +++ b/libraries/render-utils/src/DrawHaze.cpp @@ -40,8 +40,8 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu const auto hazeFrame = inputs.get0(); const auto& hazeStage = renderContext->args->_scene->getStage(); graphics::HazePointer haze; - if (hazeStage && hazeFrame->_elements.size() > 0) { - haze = hazeStage->getElement(hazeFrame->_elements.front()); + if (hazeStage && hazeFrame->_hazes.size() > 0) { + haze = hazeStage->getHaze(hazeFrame->_hazes.front()); } if (!haze) { diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp index 1d25c0a372..88018924d8 100644 --- a/libraries/render-utils/src/FadeEffect.cpp +++ b/libraries/render-utils/src/FadeEffect.cpp @@ -3,7 +3,6 @@ // Created by Olivier Prat on 17/07/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -46,7 +45,7 @@ render::ShapePipeline::ItemSetter FadeEffect::getItemUniformSetter() { const auto& scene = args->_scene; const auto& batch = args->_batch; auto transitionStage = scene->getStage(); - auto& transitionState = transitionStage->getElement(item.getTransitionId()); + auto& transitionState = transitionStage->getTransition(item.getTransitionId()); if (transitionState.paramsBuffer._size != sizeof(gpu::StructBuffer)) { static_assert(sizeof(transitionState.paramsBuffer) == sizeof(gpu::StructBuffer), "Assuming gpu::StructBuffer is a helper class for gpu::BufferView"); diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index 72edbacbf0..122b543c05 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -3,7 +3,6 @@ // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -573,7 +572,7 @@ void FadeJob::run(const render::RenderContextPointer& renderContext, FadeJob::Ou // And now update fade effect for (auto transitionId : *transitionStage) { - auto& state = transitionStage->editElement(transitionId); + auto& state = transitionStage->editTransition(transitionId); #ifdef DEBUG auto& item = scene->getItem(state.itemId); assert(item.getTransitionId() == transitionId); diff --git a/libraries/render-utils/src/HazeStage.cpp b/libraries/render-utils/src/HazeStage.cpp index 49ed7a1ea4..9251e1e2f9 100644 --- a/libraries/render-utils/src/HazeStage.cpp +++ b/libraries/render-utils/src/HazeStage.cpp @@ -3,13 +3,59 @@ // // Created by Nissim Hadar on 9/26/2017. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "HazeStage.h" -template <> -std::string render::PointerStage::_name { "HAZE_STAGE" }; +#include "DeferredLightingEffect.h" + +#include + +std::string HazeStage::_stageName { "HAZE_STAGE" }; +const HazeStage::Index HazeStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + +HazeStage::Index HazeStage::findHaze(const HazePointer& haze) const { + auto found = _hazeMap.find(haze); + if (found != _hazeMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } +} + +HazeStage::Index HazeStage::addHaze(const HazePointer& haze) { + auto found = _hazeMap.find(haze); + if (found == _hazeMap.end()) { + auto hazeId = _hazes.newElement(haze); + // Avoid failing to allocate a haze, just pass + if (hazeId != INVALID_INDEX) { + + // Insert the haze and its index in the reverse map + _hazeMap.insert(HazeMap::value_type(haze, hazeId)); + } + return hazeId; + } else { + return (*found).second; + } +} + +HazeStage::HazePointer HazeStage::removeHaze(Index index) { + HazePointer removed = _hazes.freeElement(index); + + if (removed) { + _hazeMap.erase(removed); + } + return removed; +} + +HazeStageSetup::HazeStageSetup() { +} + +void HazeStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(HazeStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(HazeStage::getName(), std::make_shared()); + } +} \ No newline at end of file diff --git a/libraries/render-utils/src/HazeStage.h b/libraries/render-utils/src/HazeStage.h index 70a33b27eb..b1c7d0384c 100644 --- a/libraries/render-utils/src/HazeStage.h +++ b/libraries/render-utils/src/HazeStage.h @@ -3,7 +3,6 @@ // Created by Nissim Hadar on 9/26/2017. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,17 +11,74 @@ #ifndef hifi_render_utils_HazeStage_h #define hifi_render_utils_HazeStage_h -#include +#include +#include +#include +#include #include -#include + +#include +#include +#include // Haze stage to set up haze-related rendering tasks -class HazeStage : public render::PointerStage {}; +class HazeStage : public render::Stage { +public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using HazePointer = graphics::HazePointer; + using Hazes = render::indexed_container::IndexedPointerVector; + using HazeMap = std::unordered_map; + + using HazeIndices = std::vector; + + Index findHaze(const HazePointer& haze) const; + Index addHaze(const HazePointer& haze); + + HazePointer removeHaze(Index index); + + bool checkHazeId(Index index) const { return _hazes.checkIndex(index); } + + Index getNumHazes() const { return _hazes.getNumElements(); } + Index getNumFreeHazes() const { return _hazes.getNumFreeIndices(); } + Index getNumAllocatedHazes() const { return _hazes.getNumAllocatedIndices(); } + + HazePointer getHaze(Index hazeId) const { + return _hazes.get(hazeId); + } + + Hazes _hazes; + HazeMap _hazeMap; + + class Frame { + public: + Frame() {} + + void clear() { _hazes.clear(); } + + void pushHaze(HazeStage::Index index) { _hazes.emplace_back(index); } + + HazeStage::HazeIndices _hazes; + }; + using FramePointer = std::shared_ptr; + + Frame _currentFrame; +}; using HazeStagePointer = std::shared_ptr; -class HazeStageSetup : public render::StageSetup { +class HazeStageSetup { public: using JobModel = render::Job::Model; + + HazeStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: }; class FetchHazeConfig : public render::Job::Config { diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp index 755d0a60be..2525427b61 100644 --- a/libraries/render-utils/src/HighlightEffect.cpp +++ b/libraries/render-utils/src/HighlightEffect.cpp @@ -161,7 +161,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c if (!inShapes.empty() && !render::HighlightStage::isIndexInvalid(highlightId)) { auto resources = inputs.get1(); - auto& highlight = highlightStage->getElement(highlightId); + auto& highlight = highlightStage->getHighlight(highlightId); RenderArgs* args = renderContext->args; @@ -247,7 +247,7 @@ void DrawHighlight::run(const render::RenderContextPointer& renderContext, const auto highlightStage = renderContext->_scene->getStage(); auto highlightId = _sharedParameters->_highlightIds[_highlightPassIndex]; if (!render::HighlightStage::isIndexInvalid(highlightId)) { - auto& highlight = highlightStage->getElement(highlightId); + auto& highlight = highlightStage->getHighlight(highlightId); auto pipeline = getPipeline(highlight._style); { auto& shaderParameters = _configuration.edit(); @@ -432,7 +432,7 @@ void SelectionToHighlight::run(const render::RenderContextPointer& renderContext auto highlightList = highlightStage->getActiveHighlightIds(); for (auto styleId : highlightList) { - auto highlight = highlightStage->getElement(styleId); + auto highlight = highlightStage->getHighlight(styleId); if (!scene->isSelectionEmpty(highlight._selectionName)) { auto highlightId = highlightStage->getHighlightIdBySelection(highlight._selectionName); @@ -572,8 +572,8 @@ void HighlightCleanup::run(const render::RenderContextPointer& renderContext, co auto highlightStage = scene->getStage(); for (auto index : inputs.get0()) { - std::string selectionName = highlightStage->getElement(index)._selectionName; - highlightStage->removeElement(index); + std::string selectionName = highlightStage->getHighlight(index)._selectionName; + highlightStage->removeHighlight(index); scene->removeSelection(selectionName); } diff --git a/libraries/render-utils/src/LightClusters.cpp b/libraries/render-utils/src/LightClusters.cpp index f0ec35238f..3425e08783 100644 --- a/libraries/render-utils/src/LightClusters.cpp +++ b/libraries/render-utils/src/LightClusters.cpp @@ -348,7 +348,7 @@ glm::ivec3 LightClusters::updateClusters() { uint32_t numClusteredLights = 0; for (size_t lightNum = 1; lightNum < _visibleLightIndices.size(); ++lightNum) { auto lightId = _visibleLightIndices[lightNum]; - auto light = _lightStage->getElement(lightId); + auto light = _lightStage->getLight(lightId); if (!light) { continue; } @@ -567,9 +567,9 @@ void LightClusteringPass::run(const render::RenderContextPointer& renderContext, output = _lightClusters; auto config = std::static_pointer_cast(renderContext->jobConfig); - config->numSceneLights = lightStage->getNumElements(); - config->numFreeSceneLights = lightStage->getNumFreeElements(); - config->numAllocatedSceneLights = lightStage->getNumAllocatedElements(); + config->numSceneLights = lightStage->getNumLights(); + config->numFreeSceneLights = lightStage->getNumFreeLights(); + config->numAllocatedSceneLights = lightStage->getNumAllocatedLights(); config->setNumInputLights(clusteringStats.x); config->setNumClusteredLights(clusteringStats.y); config->setNumClusteredLightReferences(clusteringStats.z); diff --git a/libraries/render-utils/src/LightClusters.h b/libraries/render-utils/src/LightClusters.h index 94e1e37ae3..6192105914 100644 --- a/libraries/render-utils/src/LightClusters.h +++ b/libraries/render-utils/src/LightClusters.h @@ -95,7 +95,7 @@ class LightClusters { FrustumGrid::Planes _gridPlanes[3]; - render::ElementIndices _visibleLightIndices; + LightStage::LightIndices _visibleLightIndices; gpu::BufferView _lightIndicesBuffer; const uint32_t EMPTY_CLUSTER { 0x0000FFFF }; diff --git a/libraries/render-utils/src/LightPayload.cpp b/libraries/render-utils/src/LightPayload.cpp index 43280c9a2a..d1018982d0 100644 --- a/libraries/render-utils/src/LightPayload.cpp +++ b/libraries/render-utils/src/LightPayload.cpp @@ -52,7 +52,7 @@ LightPayload::LightPayload() : LightPayload::~LightPayload() { if (!LightStage::isIndexInvalid(_index)) { if (_stage) { - _stage->removeElement(_index); + _stage->removeLight(_index); } } } @@ -64,7 +64,7 @@ void LightPayload::render(RenderArgs* args) { } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { - _index = _stage->addElement(_light); + _index = _stage->addLight(_light); _needUpdate = false; } // Need an update ? @@ -122,7 +122,7 @@ _light(std::make_shared()) KeyLightPayload::~KeyLightPayload() { if (!LightStage::isIndexInvalid(_index)) { if (_stage) { - _stage->removeElement(_index); + _stage->removeLight(_index); } } } @@ -134,7 +134,7 @@ void KeyLightPayload::render(RenderArgs* args) { } // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_index)) { - _index = _stage->addElement(_light); + _index = _stage->addLight(_light); _needUpdate = false; } // Need an update ? diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index 08e5e51459..58f32cdc4e 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -4,7 +4,6 @@ // // Created by Zach Pomerantz on 1/14/2015. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -16,9 +15,7 @@ #include "ViewFrustum.h" -template <> -std::string render::PointerStage::_name { "LIGHT_STAGE" }; - +std::string LightStage::_stageName { "LIGHT_STAGE" }; // The bias matrix goes from homogeneous coordinates to UV coords (see http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#basic-shader) const glm::mat4 LightStage::Shadow::_biasMatrix { 0.5, 0.0, 0.0, 0.0, @@ -27,6 +24,8 @@ const glm::mat4 LightStage::Shadow::_biasMatrix { 0.5, 0.5, 0.5, 1.0 }; const int LightStage::Shadow::MAP_SIZE = 1024; +const LightStage::Index LightStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + LightStage::LightStage() { // Add off lights const LightPointer ambientOffLight { std::make_shared() }; @@ -34,28 +33,28 @@ LightStage::LightStage() { ambientOffLight->setColor(graphics::Vec3(0.0)); ambientOffLight->setIntensity(0.0f); ambientOffLight->setType(graphics::Light::Type::AMBIENT); - _ambientOffLightId = addElement(ambientOffLight); + _ambientOffLightId = addLight(ambientOffLight); const LightPointer pointOffLight { std::make_shared() }; pointOffLight->setAmbientIntensity(0.0f); pointOffLight->setColor(graphics::Vec3(0.0)); pointOffLight->setIntensity(0.0f); pointOffLight->setType(graphics::Light::Type::POINT); - _pointOffLightId = addElement(pointOffLight); + _pointOffLightId = addLight(pointOffLight); const LightPointer spotOffLight { std::make_shared() }; spotOffLight->setAmbientIntensity(0.0f); spotOffLight->setColor(graphics::Vec3(0.0)); spotOffLight->setIntensity(0.0f); spotOffLight->setType(graphics::Light::Type::SPOT); - _spotOffLightId = addElement(spotOffLight); + _spotOffLightId = addLight(spotOffLight); const LightPointer sunOffLight { std::make_shared() }; sunOffLight->setAmbientIntensity(0.0f); sunOffLight->setColor(graphics::Vec3(0.0)); sunOffLight->setIntensity(0.0f); sunOffLight->setType(graphics::Light::Type::SUN); - _sunOffLightId = addElement(sunOffLight); + _sunOffLightId = addLight(sunOffLight); // Set default light to the off ambient light (until changed) _defaultLightId = _ambientOffLightId; @@ -73,7 +72,7 @@ LightStage::Shadow::Schema::Schema() { maxDistance = 20.0f; } -LightStage::Shadow::Cascade::Cascade() : +LightStage::Shadow::Cascade::Cascade() : _frustum{ std::make_shared() }, _minDistance{ 0.0f }, _maxDistance{ 20.0f } { @@ -89,7 +88,7 @@ const glm::mat4& LightStage::Shadow::Cascade::getProjection() const { float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFrustum, const Transform& shadowViewInverse, float left, float right, float bottom, float top, float viewMaxShadowDistance) const { - // Far distance should be extended to the intersection of the infinitely extruded shadow frustum + // Far distance should be extended to the intersection of the infinitely extruded shadow frustum // with the view frustum side planes. To do so, we generate 10 triangles in shadow space which are the result of // tesselating the side and far faces of the view frustum and clip them with the 4 side planes of the // shadow frustum. The resulting clipped triangle vertices with the farthest Z gives the desired @@ -122,7 +121,7 @@ float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFru return far; } -LightStage::Shadow::Shadow(graphics::LightPointer light, unsigned int cascadeCount) : +LightStage::Shadow::Shadow(graphics::LightPointer light, unsigned int cascadeCount) : _light{ light } { cascadeCount = std::min(cascadeCount, (unsigned int)SHADOW_CASCADE_MAX_COUNT); Schema schema; @@ -324,12 +323,21 @@ void LightStage::Shadow::setCascadeFrustum(unsigned int cascadeIndex, const View schemaCascade.reprojection = _biasMatrix * shadowFrustum.getProjection() * viewInverse.getMatrix(); } -LightStage::Index LightStage::addElement(const LightPointer& light, const bool shouldSetAsDefault) { +LightStage::Index LightStage::findLight(const LightPointer& light) const { + auto found = _lightMap.find(light); + if (found != _lightMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } +} + +LightStage::Index LightStage::addLight(const LightPointer& light, const bool shouldSetAsDefault) { Index lightId; - auto found = _elementMap.find(light); - if (found == _elementMap.end()) { - lightId = _elements.newElement(light); + auto found = _lightMap.find(light); + if (found == _lightMap.end()) { + lightId = _lights.newElement(light); // Avoid failing to allocate a light, just pass if (lightId != INVALID_INDEX) { @@ -341,8 +349,8 @@ LightStage::Index LightStage::addElement(const LightPointer& light, const bool s _descs[lightId] = Desc(); } - // Insert the light and its index in the reverese map - _elementMap[light] = lightId; + // INsert the light and its index in the reverese map + _lightMap.insert(LightMap::value_type(light, lightId)); updateLightArrayBuffer(lightId); } @@ -357,30 +365,30 @@ LightStage::Index LightStage::addElement(const LightPointer& light, const bool s return lightId; } -LightStage::LightPointer LightStage::removeElement(Index index) { - LightPointer removedLight = _elements.freeElement(index); +LightStage::LightPointer LightStage::removeLight(Index index) { + LightPointer removedLight = _lights.freeElement(index); if (removedLight) { - _elementMap.erase(removedLight); + _lightMap.erase(removedLight); _descs[index] = Desc(); } assert(_descs.size() <= (size_t)index || _descs[index].shadowId == INVALID_INDEX); return removedLight; } -LightStage::LightPointer LightStage::getCurrentKeyLight(const LightFrame& frame) const { +LightStage::LightPointer LightStage::getCurrentKeyLight(const LightStage::Frame& frame) const { Index keyLightId { _defaultLightId }; if (!frame._sunLights.empty()) { keyLightId = frame._sunLights.front(); } - return _elements.get(keyLightId); + return _lights.get(keyLightId); } -LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightFrame& frame) const { +LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightStage::Frame& frame) const { Index keyLightId { _defaultLightId }; if (!frame._ambientLights.empty()) { keyLightId = frame._ambientLights.front(); } - return _elements.get(keyLightId); + return _lights.get(keyLightId); } void LightStage::updateLightArrayBuffer(Index lightId) { @@ -389,12 +397,14 @@ void LightStage::updateLightArrayBuffer(Index lightId) { _lightArrayBuffer = std::make_shared(lightSize); } + assert(checkLightId(lightId)); + if (lightId > (Index)_lightArrayBuffer->getNumTypedElements()) { _lightArrayBuffer->resize(lightSize * (lightId + 10)); } // lightArray is big enough so we can remap - auto light = _elements._elements[lightId]; + auto light = _lights._elements[lightId]; if (light) { const auto& lightSchema = light->getLightSchemaBuffer().get(); _lightArrayBuffer->setSubData(lightId, lightSchema); @@ -402,3 +412,17 @@ void LightStage::updateLightArrayBuffer(Index lightId) { // this should not happen ? } } + +LightStageSetup::LightStageSetup() { +} + +void LightStageSetup::run(const render::RenderContextPointer& renderContext) { + if (renderContext->_scene) { + auto stage = renderContext->_scene->getStage(LightStage::getName()); + if (!stage) { + stage = std::make_shared(); + renderContext->_scene->resetStage(LightStage::getName(), stage); + } + } +} + diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index 5b0a90ddb6..36e62c614f 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -4,7 +4,6 @@ // // Created by Zach Pomerantz on 1/14/2015. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,44 +12,34 @@ #ifndef hifi_render_utils_LightStage_h #define hifi_render_utils_LightStage_h +#include +#include + #include + #include + +#include #include -#include +#include class ViewFrustum; -class LightFrame { +// Light stage to set up light-related rendering tasks +class LightStage : public render::Stage { public: - LightFrame() {} + static std::string _stageName; + static const std::string& getName() { return _stageName; } using Index = render::indexed_container::Index; - - void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); } - void pushLight(Index index, graphics::Light::Type type) { - switch (type) { - case graphics::Light::POINT: { pushPointLight(index); break; } - case graphics::Light::SPOT: { pushSpotLight(index); break; } - case graphics::Light::SUN: { pushSunLight(index); break; } - case graphics::Light::AMBIENT: { pushAmbientLight(index); break; } - default: { break; } - } - } - void pushPointLight(Index index) { _pointLights.emplace_back(index); } - void pushSpotLight(Index index) { _spotLights.emplace_back(index); } - void pushSunLight(Index index) { _sunLights.emplace_back(index); } - void pushAmbientLight(Index index) { _ambientLights.emplace_back(index); } - - render::ElementIndices _pointLights; - render::ElementIndices _spotLights; - render::ElementIndices _sunLights; - render::ElementIndices _ambientLights; -}; - -// Light stage to set up light-related rendering tasks -class LightStage : public render::PointerStage { -public: + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + using LightPointer = graphics::LightPointer; + using Lights = render::indexed_container::IndexedPointerVector; + using LightMap = std::unordered_map; + + using LightIndices = std::vector; class Shadow { public: @@ -85,9 +74,9 @@ class LightStage : public render::PointerStage; static const glm::mat4 _biasMatrix; - LightPointer _light; + graphics::LightPointer _light; float _maxDistance{ 0.0f }; Cascades _cascades; UniformBufferView _schemaBuffer = nullptr; }; - using ShadowPointer = std::shared_ptr; - Index addElement(const LightPointer& light) override { return addElement(light, false); } - Index addElement(const LightPointer& light, const bool shouldSetAsDefault); - LightPointer removeElement(Index index) override; + using ShadowPointer = std::shared_ptr; + Index findLight(const LightPointer& light) const; + Index addLight(const LightPointer& light, const bool shouldSetAsDefault = false); + Index getDefaultLight() { return _defaultLightId; } + LightPointer removeLight(Index index); + + bool checkLightId(Index index) const { return _lights.checkIndex(index); } + + Index getNumLights() const { return _lights.getNumElements(); } + Index getNumFreeLights() const { return _lights.getNumFreeIndices(); } + Index getNumAllocatedLights() const { return _lights.getNumAllocatedIndices(); } + + LightPointer getLight(Index lightId) const { return _lights.get(lightId); } + LightStage(); gpu::BufferPointer getLightArrayBuffer() const { return _lightArrayBuffer; } void updateLightArrayBuffer(Index lightId); + class Frame { + public: + Frame() {} + + void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); } + void pushLight(LightStage::Index index, graphics::Light::Type type) { + switch (type) { + case graphics::Light::POINT: { pushPointLight(index); break; } + case graphics::Light::SPOT: { pushSpotLight(index); break; } + case graphics::Light::SUN: { pushSunLight(index); break; } + case graphics::Light::AMBIENT: { pushAmbientLight(index); break; } + default: { break; } + } + } + void pushPointLight(LightStage::Index index) { _pointLights.emplace_back(index); } + void pushSpotLight(LightStage::Index index) { _spotLights.emplace_back(index); } + void pushSunLight(LightStage::Index index) { _sunLights.emplace_back(index); } + void pushAmbientLight(LightStage::Index index) { _ambientLights.emplace_back(index); } + + LightStage::LightIndices _pointLights; + LightStage::LightIndices _spotLights; + LightStage::LightIndices _sunLights; + LightStage::LightIndices _ambientLights; + }; + using FramePointer = std::shared_ptr; + class ShadowFrame { public: ShadowFrame() {} - + void clear() {} - + using Object = ShadowPointer; using Objects = std::vector; @@ -151,17 +177,20 @@ class LightStage : public render::PointerStage; + Frame _currentFrame; + Index getAmbientOffLight() { return _ambientOffLightId; } Index getPointOffLight() { return _pointOffLightId; } Index getSpotOffLight() { return _spotOffLightId; } Index getSunOffLight() { return _sunOffLightId; } - LightPointer getCurrentKeyLight(const LightFrame& frame) const; - LightPointer getCurrentAmbientLight(const LightFrame& frame) const; + LightPointer getCurrentKeyLight(const LightStage::Frame& frame) const; + LightPointer getCurrentAmbientLight(const LightStage::Frame& frame) const; protected: @@ -172,7 +201,9 @@ class LightStage : public render::PointerStage; -class LightStageSetup : public render::StageSetup { + +class LightStageSetup { public: using JobModel = render::Job::Model; + + LightStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: }; + #endif diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index ba0460417c..c79ac87551 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -1,9 +1,6 @@ // -// RenderCommonTask.cpp -// // Created by Bradley Austin Davis on 2018/01/09 // Copyright 2013-2018 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -81,9 +78,9 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& graphics::HazePointer haze; const auto& hazeStage = renderContext->args->_scene->getStage(); - if (hazeStage && hazeFrame->_elements.size() > 0) { + if (hazeStage && hazeFrame->_hazes.size() > 0) { // We use _hazes.back() here because the last haze object will always have haze disabled. - haze = hazeStage->getElement(hazeFrame->_elements.back()); + haze = hazeStage->getHaze(hazeFrame->_hazes.back()); } // Clear the framebuffer without stereo diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index 5d6395aceb..255fcb6392 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -1,9 +1,6 @@ // -// RenderCommonTask.h -// // Created by Bradley Austin Davis on 2018/01/09 // Copyright 2013-2018 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 261658039c..05253178f1 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -5,7 +5,6 @@ // // Created by Sam Gateau on 5/29/15. // Copyright 2016 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -512,8 +511,8 @@ void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, c // Setup haze if current zone has haze const auto& hazeStage = args->_scene->getStage(); - if (hazeStage && hazeFrame->_elements.size() > 0) { - const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front()); + if (hazeStage && hazeFrame->_hazes.size() > 0) { + const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front()); if (hazePointer) { batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer()); } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 85ea0facbe..74cdf1b044 100644 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -5,7 +5,6 @@ // // Created by Zach Pomerantz on 12/13/2016. // Copyright 2016 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -272,8 +271,8 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i graphics::HazePointer haze; const auto& hazeStage = renderContext->args->_scene->getStage(); - if (hazeStage && hazeFrame->_elements.size() > 0) { - haze = hazeStage->getElement(hazeFrame->_elements.front()); + if (hazeStage && hazeFrame->_hazes.size() > 0) { + haze = hazeStage->getHaze(hazeFrame->_hazes.front()); } gpu::doInBatch("DrawForward::run", args->_context, [&](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 5964a1737c..9f1cf8421a 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -473,8 +473,8 @@ void DebugSubsurfaceScattering::run(const render::RenderContextPointer& renderCo auto lightStage = renderContext->_scene->getStage(); assert(lightStage); - // const auto light = DependencyManager::get()->getLightStage()->getElement(0); - const auto light = lightStage->getElement(0); + // const auto light = DependencyManager::get()->getLightStage()->getLight(0); + const auto light = lightStage->getLight(0); if (!_debugParams) { _debugParams = std::make_shared(sizeof(glm::vec4), nullptr); _debugParams->setSubData(0, _debugCursorTexcoord); diff --git a/libraries/render-utils/src/ToneMapAndResampleTask.cpp b/libraries/render-utils/src/ToneMapAndResampleTask.cpp index c72776f1b6..d906d82aa7 100644 --- a/libraries/render-utils/src/ToneMapAndResampleTask.cpp +++ b/libraries/render-utils/src/ToneMapAndResampleTask.cpp @@ -4,7 +4,6 @@ // // Created by Anna Brewer on 7/3/19. // Copyright 2019 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -75,8 +74,8 @@ void ToneMapAndResample::run(const RenderContextPointer& renderContext, const In const auto& tonemappingStage = renderContext->_scene->getStage(); graphics::TonemappingPointer tonemapping; - if (tonemappingStage && tonemappingFrame->_elements.size()) { - tonemapping = tonemappingStage->getElement(tonemappingFrame->_elements.front()); + if (tonemappingStage && tonemappingFrame->_tonemappings.size()) { + tonemapping = tonemappingStage->getTonemapping(tonemappingFrame->_tonemappings.front()); } if (_debug) { diff --git a/libraries/render-utils/src/TonemappingStage.cpp b/libraries/render-utils/src/TonemappingStage.cpp index 1bb382b77d..9b6029ca1b 100644 --- a/libraries/render-utils/src/TonemappingStage.cpp +++ b/libraries/render-utils/src/TonemappingStage.cpp @@ -7,8 +7,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "TonemappingStage.h" -template <> -std::string render::PointerStage::_name { "TONEMAPPING_STAGE" }; +#include + +std::string TonemappingStage::_stageName { "TONEMAPPING_STAGE" }; +const TonemappingStage::Index TonemappingStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + +TonemappingStage::Index TonemappingStage::findTonemapping(const TonemappingPointer& tonemapping) const { + auto found = _tonemappingMap.find(tonemapping); + if (found != _tonemappingMap.end()) { + return INVALID_INDEX; + } else { + return (*found).second; + } +} + +TonemappingStage::Index TonemappingStage::addTonemapping(const TonemappingPointer& tonemapping) { + auto found = _tonemappingMap.find(tonemapping); + if (found == _tonemappingMap.end()) { + auto tonemappingId = _tonemappings.newElement(tonemapping); + // Avoid failing to allocate a tonemapping, just pass + if (tonemappingId != INVALID_INDEX) { + // Insert the tonemapping and its index in the reverse map + _tonemappingMap.insert(TonemappingMap::value_type(tonemapping, tonemappingId)); + } + return tonemappingId; + } else { + return (*found).second; + } +} + +TonemappingStage::TonemappingPointer TonemappingStage::removeTonemapping(Index index) { + TonemappingPointer removed = _tonemappings.freeElement(index); + if (removed) { + _tonemappingMap.erase(removed); + } + return removed; +} + +TonemappingStageSetup::TonemappingStageSetup() {} + +void TonemappingStageSetup::run(const render::RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(TonemappingStage::getName()); + if (!stage) { + renderContext->_scene->resetStage(TonemappingStage::getName(), std::make_shared()); + } +} diff --git a/libraries/render-utils/src/TonemappingStage.h b/libraries/render-utils/src/TonemappingStage.h index ee05c0ace4..15161094a2 100644 --- a/libraries/render-utils/src/TonemappingStage.h +++ b/libraries/render-utils/src/TonemappingStage.h @@ -11,17 +11,74 @@ #ifndef hifi_render_utils_TonemappingStage_h #define hifi_render_utils_TonemappingStage_h -#include +#include +#include +#include +#include #include -#include + +#include +#include +#include // Tonemapping stage to set up tonemapping-related rendering tasks -class TonemappingStage : public render::PointerStage {}; +class TonemappingStage : public render::Stage { +public: + static std::string _stageName; + static const std::string& getName() { return _stageName; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + using TonemappingPointer = graphics::TonemappingPointer; + using Tonemappings = render::indexed_container::IndexedPointerVector; + using TonemappingMap = std::unordered_map; + + using TonemappingIndices = std::vector; + + Index findTonemapping(const TonemappingPointer& tonemapping) const; + Index addTonemapping(const TonemappingPointer& tonemapping); + + TonemappingPointer removeTonemapping(Index index); + + bool checkTonemappingId(Index index) const { return _tonemappings.checkIndex(index); } + + Index getNumTonemappings() const { return _tonemappings.getNumElements(); } + Index getNumFreeTonemappings() const { return _tonemappings.getNumFreeIndices(); } + Index getNumAllocatedTonemappings() const { return _tonemappings.getNumAllocatedIndices(); } + + TonemappingPointer getTonemapping(Index tonemappingId) const { + return _tonemappings.get(tonemappingId); + } + + Tonemappings _tonemappings; + TonemappingMap _tonemappingMap; + + class Frame { + public: + Frame() {} + + void clear() { _tonemappings.clear(); } + + void pushTonemapping(TonemappingStage::Index index) { _tonemappings.emplace_back(index); } + + TonemappingStage::TonemappingIndices _tonemappings; + }; + using FramePointer = std::shared_ptr; + + Frame _currentFrame; +}; using TonemappingStagePointer = std::shared_ptr; -class TonemappingStageSetup : public render::StageSetup { +class TonemappingStageSetup { public: using JobModel = render::Job::Model; + + TonemappingStageSetup(); + void run(const render::RenderContextPointer& renderContext); + +protected: }; #endif diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp index 5d958d8a84..a8fb349e4d 100644 --- a/libraries/render-utils/src/ZoneRenderer.cpp +++ b/libraries/render-utils/src/ZoneRenderer.cpp @@ -4,7 +4,6 @@ // // Created by Sam Gateau on 4/4/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -87,11 +86,11 @@ void SetupZones::run(const RenderContextPointer& context, const Input& input) { // Finally add the default lights and background: lightStage->_currentFrame.pushSunLight(lightStage->getDefaultLight()); lightStage->_currentFrame.pushAmbientLight(lightStage->getDefaultLight()); - backgroundStage->_currentFrame.pushElement(0); - hazeStage->_currentFrame.pushElement(0); - bloomStage->_currentFrame.pushElement(INVALID_INDEX); - tonemappingStage->_currentFrame.pushElement(0); - ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX); + backgroundStage->_currentFrame.pushBackground(0); + hazeStage->_currentFrame.pushHaze(0); + bloomStage->_currentFrame.pushBloom(INVALID_INDEX); + tonemappingStage->_currentFrame.pushTonemapping(0); + ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX); } gpu::PipelinePointer DebugZoneLighting::_keyLightPipeline; @@ -145,22 +144,22 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I std::vector keyLightStack; if (lightStage && lightFrame->_sunLights.size()) { for (auto index : lightFrame->_sunLights) { - keyLightStack.push_back(lightStage->getElement(index)); + keyLightStack.push_back(lightStage->getLight(index)); } } std::vector ambientLightStack; if (lightStage && lightFrame->_ambientLights.size()) { for (auto index : lightFrame->_ambientLights) { - ambientLightStack.push_back(lightStage->getElement(index)); + ambientLightStack.push_back(lightStage->getLight(index)); } } auto backgroundStage = context->_scene->getStage(BackgroundStage::getName()); std::vector skyboxStack; - if (backgroundStage && backgroundFrame->_elements.size()) { - for (auto index : backgroundFrame->_elements) { - auto background = backgroundStage->getElement(index); + if (backgroundStage && backgroundFrame->_backgrounds.size()) { + for (auto index : backgroundFrame->_backgrounds) { + auto background = backgroundStage->getBackground(index); if (background) { skyboxStack.push_back(background->getSkybox()); } diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh index 5471415e1c..c927070a4d 100644 --- a/libraries/render-utils/src/sdf_text3D.slh +++ b/libraries/render-utils/src/sdf_text3D.slh @@ -95,8 +95,8 @@ vec4 evalSDFColor(vec2 texCoord, vec4 glyphBounds) { vec4 evalSDFSuperSampled(vec2 texCoord, vec2 positionMS, vec4 glyphBounds) { // Clip to edges. Note: We don't need to check the top edge. - if ((params.bounds.z > 0.0 && (positionMS.x < params.bounds.x || positionMS.x > (params.bounds.x + params.bounds.z))) || - (params.bounds.w > 0.0 && (positionMS.y < params.bounds.y - params.bounds.w))) { + if (positionMS.x < params.bounds.x || positionMS.x > (params.bounds.x + params.bounds.z) || + positionMS.y < params.bounds.y - params.bounds.w) { return vec4(0.0); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 23699de69a..81badb8440 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -438,7 +438,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm // Draw the token for (const QChar& c : token) { - if (bounds.x != -1 && advance.x > rightEdge) { + if (advance.x > rightEdge) { break; } const Glyph& glyph = _glyphs[c]; diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 730f6db758..8112e279e5 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -71,8 +71,8 @@ class Font : public QObject { bool forward, bool mirror) : str(str), color(color), effectColor(effectColor), origin(origin), bounds(bounds), scale(scale), effectThickness(effectThickness), effect(effect), alignment(alignment), verticalAlignment(verticalAlignment), unlit(unlit), forward(forward), mirror(mirror) {} - DrawProps(const QString& str, const glm::vec4& color, const glm::vec2& origin, const glm::vec2& bounds, TextAlignment alignment, bool forward) : - str(str), color(color), origin(origin), bounds(bounds), alignment(alignment), forward(forward) {} + DrawProps(const QString& str, const glm::vec4& color, const glm::vec2& origin, const glm::vec2& bounds, bool forward) : + str(str), color(color), origin(origin), bounds(bounds), forward(forward) {} const QString& str; const glm::vec4& color; diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index d722197205..925cffdae1 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -4,7 +4,6 @@ // // Created by Niraj Venkat on 6/29/15. // Copyright 2015 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -164,7 +163,7 @@ void DrawStatus::run(const RenderContextPointer& renderContext, const Input& inp status.setColor(Item::Status::Value::RED); } // Set icon based on transition type - auto& transition = transitionStage->getElement(transitionID); + auto& transition = transitionStage->getTransition(transitionID); switch (transition.eventType) { case Transition::Type::USER_ENTER_DOMAIN: status.setIcon((unsigned char)Item::Status::Icon::USER_TRANSITION_IN); diff --git a/libraries/render/src/render/HighlightStage.cpp b/libraries/render/src/render/HighlightStage.cpp index 07fb5b5635..c9f097b387 100644 --- a/libraries/render/src/render/HighlightStage.cpp +++ b/libraries/render/src/render/HighlightStage.cpp @@ -1,31 +1,33 @@ -// -// HighlightStage.cpp -// -// Created by Olivier Prat on 07/07/2017. -// Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - #include "HighlightStage.h" -#include "Engine.h" - using namespace render; -template <> -std::string TypedStage::_name { "HIGHLIGHT_STAGE" }; +std::string HighlightStage::_name("Highlight"); +const HighlightStage::Index HighlightStage::INVALID_INDEX{ render::indexed_container::INVALID_INDEX }; HighlightStage::Index HighlightStage::addHighlight(const std::string& selectionName, const HighlightStyle& style) { - Highlight outline { selectionName, style }; - return addElement(outline); + Highlight outline{ selectionName, style }; + Index id; + + id = _highlights.newElement(outline); + _activeHighlightIds.push_back(id); + + return id; } -HighlightStage::Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const { - for (auto outlineId : _activeElementIDs) { - const auto& outline = _elements.get(outlineId); +void HighlightStage::removeHighlight(Index index) { + HighlightIdList::iterator idIterator = std::find(_activeHighlightIds.begin(), _activeHighlightIds.end(), index); + if (idIterator != _activeHighlightIds.end()) { + _activeHighlightIds.erase(idIterator); + } + if (!_highlights.isElementFreed(index)) { + _highlights.freeElement(index); + } +} + +Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const { + for (auto outlineId : _activeHighlightIds) { + const auto& outline = _highlights.get(outlineId); if (outline._selectionName == selectionName) { return outlineId; } @@ -98,6 +100,9 @@ void HighlightStageConfig::setOccludedFillOpacity(float value) { emit dirty(); } +HighlightStageSetup::HighlightStageSetup() { +} + void HighlightStageSetup::configure(const Config& config) { // Copy the styles here but update the stage with the new styles in run to be sure everything is // thread safe... @@ -107,7 +112,8 @@ void HighlightStageSetup::configure(const Config& config) { void HighlightStageSetup::run(const render::RenderContextPointer& renderContext) { auto stage = renderContext->_scene->getStage(HighlightStage::getName()); if (!stage) { - renderContext->_scene->resetStage(HighlightStage::getName(), std::make_shared()); + stage = std::make_shared(); + renderContext->_scene->resetStage(HighlightStage::getName(), stage); } if (!_styles.empty()) { @@ -121,3 +127,4 @@ void HighlightStageSetup::run(const render::RenderContextPointer& renderContext) _styles.clear(); } } + diff --git a/libraries/render/src/render/HighlightStage.h b/libraries/render/src/render/HighlightStage.h index d675ed9f6b..91d8cc3f81 100644 --- a/libraries/render/src/render/HighlightStage.h +++ b/libraries/render/src/render/HighlightStage.h @@ -3,7 +3,6 @@ // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,27 +11,56 @@ #ifndef hifi_render_utils_HighlightStage_h #define hifi_render_utils_HighlightStage_h +#include "Stage.h" #include "Engine.h" +#include "IndexedContainer.h" #include "HighlightStyle.h" -#include "Stage.h" namespace render { - class Highlight { + // Highlight stage to set up HighlightStyle-related effects + class HighlightStage : public Stage { public: - Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { } - std::string _selectionName; - HighlightStyle _style; - }; + class Highlight { + public: + + Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { } + + std::string _selectionName; + HighlightStyle _style; + + }; + + static const std::string& getName() { return _name; } + + using Index = render::indexed_container::Index; + static const Index INVALID_INDEX; + using HighlightIdList = render::indexed_container::Indices; + + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + bool checkHighlightId(Index index) const { return _highlights.checkIndex(index); } + + const Highlight& getHighlight(Index index) const { return _highlights.get(index); } + Highlight& editHighlight(Index index) { return _highlights.edit(index); } - // Highlight stage to set up HighlightStyle-related effects - class HighlightStage : public TypedStage { - public: Index addHighlight(const std::string& selectionName, const HighlightStyle& style = HighlightStyle()); Index getHighlightIdBySelection(const std::string& selectionName) const; + void removeHighlight(Index index); + + HighlightIdList::iterator begin() { return _activeHighlightIds.begin(); } + HighlightIdList::iterator end() { return _activeHighlightIds.end(); } + const HighlightIdList& getActiveHighlightIds() const { return _activeHighlightIds; } + + private: + + using Highlights = render::indexed_container::IndexedVector; + + static std::string _name; - const IDList& getActiveHighlightIds() const { return _activeElementIDs; } + Highlights _highlights; + HighlightIdList _activeHighlightIds; }; using HighlightStagePointer = std::shared_ptr; @@ -94,7 +122,7 @@ namespace render { using Config = HighlightStageConfig; using JobModel = render::Job::Model; - HighlightStageSetup() {} + HighlightStageSetup(); void configure(const Config& config); void run(const RenderContextPointer& renderContext); diff --git a/libraries/render/src/render/HighlightStyle.h b/libraries/render/src/render/HighlightStyle.h index 266ce71262..138674ffbb 100644 --- a/libraries/render/src/render/HighlightStyle.h +++ b/libraries/render/src/render/HighlightStyle.h @@ -17,8 +17,6 @@ #include -#include - namespace render { // This holds the configuration for a particular outline style diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 3776caef0d..a9bb2a5856 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -440,7 +440,7 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti auto transitionId = item.getTransitionId(); if (!TransitionStage::isIndexInvalid(transitionId)) { - auto& transition = transitionStage->getElement(transitionId); + auto& transition = transitionStage->getTransition(transitionId); func(itemId, &transition); } else { func(itemId, nullptr); @@ -477,7 +477,7 @@ void Scene::resetHighlights(const Transaction::HighlightResets& transactions) { if (HighlightStage::isIndexInvalid(outlineId)) { outlineStage->addHighlight(selectionName, newStyle); } else { - outlineStage->editElement(outlineId)._style = newStyle; + outlineStage->editHighlight(outlineId)._style = newStyle; } } } @@ -490,7 +490,7 @@ void Scene::removeHighlights(const Transaction::HighlightRemoves& transactions) auto outlineId = outlineStage->getHighlightIdBySelection(selectionName); if (!HighlightStage::isIndexInvalid(outlineId)) { - outlineStage->removeElement(outlineId); + outlineStage->removeHighlight(outlineId); } } } @@ -505,7 +505,7 @@ void Scene::queryHighlights(const Transaction::HighlightQueries& transactions) { auto outlineId = outlineStage->getHighlightIdBySelection(selectionName); if (!HighlightStage::isIndexInvalid(outlineId)) { - func(&outlineStage->editElement(outlineId)._style); + func(&outlineStage->editHighlight(outlineId)._style); } else { func(nullptr); } @@ -559,7 +559,7 @@ void Scene::removeItemTransition(ItemID itemId) { auto& item = _items[itemId]; TransitionStage::Index transitionId = item.getTransitionId(); if (!render::TransitionStage::isIndexInvalid(transitionId)) { - const auto& transition = transitionStage->getElement(transitionId); + const auto& transition = transitionStage->getTransition(transitionId); const auto transitionOwner = transition.itemId; if (transitionOwner == itemId) { // No more items will be using this transition. Clean it up. @@ -570,7 +570,7 @@ void Scene::removeItemTransition(ItemID itemId) { } } _transitionFinishedOperatorMap.erase(transitionId); - transitionStage->removeElement(transitionId); + transitionStage->removeTransition(transitionId); } setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); diff --git a/libraries/render/src/render/Stage.cpp b/libraries/render/src/render/Stage.cpp index d5335f07ed..1ee9b1d6ff 100644 --- a/libraries/render/src/render/Stage.cpp +++ b/libraries/render/src/render/Stage.cpp @@ -4,13 +4,23 @@ // // Created by Sam Gateau on 6/14/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "Stage.h" + using namespace render; -const Stage::Index Stage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; + + +Stage::~Stage() { + +} + +Stage::Stage() : + _name() +{ +} + diff --git a/libraries/render/src/render/Stage.h b/libraries/render/src/render/Stage.h index 2bb81771e5..5145810671 100644 --- a/libraries/render/src/render/Stage.h +++ b/libraries/render/src/render/Stage.h @@ -4,7 +4,6 @@ // // Created by Sam Gateau on 6/14/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,141 +12,27 @@ #ifndef hifi_render_Stage_h #define hifi_render_Stage_h -#include #include -#include +#include #include -#include "IndexedContainer.h" - namespace render { - using ElementIndices = std::vector; - class Stage { public: - Stage() {} - virtual ~Stage() {} - using Name = std::string; - using Index = indexed_container::Index; - static const Index INVALID_INDEX; - using IDList = indexed_container::Indices; - - static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } - }; - - using StagePointer = std::shared_ptr; - using StageMap = std::map; - - template - class TypedStage : public Stage { - public: - TypedStage() {} - virtual ~TypedStage() {} - - static const Name& getName() { return _name; } - - bool checkId(Index index) const { return _elements.checkIndex(index); } - const T& getElement(Index id) const { return _elements.get(id); } - T& editElement(Index id) { return _elements.edit(id); } - Index addElement(const T& element) { - Index id = _elements.newElement(element); - _activeElementIDs.push_back(id); - return id; - } + Stage(); + virtual ~Stage(); - void removeElement(Index index) { - IDList::iterator idIterator = std::find(_activeElementIDs.begin(), _activeElementIDs.end(), index); - if (idIterator != _activeElementIDs.end()) { - _activeElementIDs.erase(idIterator); - } - if (!_elements.isElementFreed(index)) { - _elements.freeElement(index); - } - } - - IDList::iterator begin() { return _activeElementIDs.begin(); } - IDList::iterator end() { return _activeElementIDs.end(); } protected: - static Name _name; - - indexed_container::IndexedVector _elements; - IDList _activeElementIDs; - }; - - class Frame { - public: - Frame() {} - - using Index = indexed_container::Index; - - void clear() { _elements.clear(); } - void pushElement(Index index) { _elements.emplace_back(index); } - - ElementIndices _elements; + Name _name; }; - template - class PointerStage : public Stage { - public: - PointerStage() {} - virtual ~PointerStage() {} - - static const Name& getName() { return _name; } - - bool checkId(Index index) const { return _elements.checkIndex(index); } - - Index getNumElements() const { return _elements.getNumElements(); } - Index getNumFreeElements() const { return _elements.getNumFreeIndices(); } - Index getNumAllocatedElements() const { return _elements.getNumAllocatedIndices(); } - - P getElement(Index id) const { return _elements.get(id); } - - Index findElement(const P& element) const { - auto found = _elementMap.find(element); - if (found != _elementMap.end()) { - return INVALID_INDEX; - } else { - return (*found).second; - } - } - - virtual Index addElement(const P& element) { - auto found = _elementMap.find(element); - if (found == _elementMap.end()) { - auto id = _elements.newElement(element); - // Avoid failing to allocate an element, just pass - if (id != INVALID_INDEX) { - // Insert the element and its index in the reverse map - _elementMap[element] = id; - } - return id; - } else { - return (*found).second; - } - } - - virtual P removeElement(Index index) { - P removed = _elements.freeElement(index); - - if (removed) { - _elementMap.erase(removed); - } - return removed; - } - - using Frame = F; - using FramePointer = std::shared_ptr; - F _currentFrame; + using StagePointer = std::shared_ptr; - protected: - static Name _name; + using StageMap = std::map; - indexed_container::IndexedPointerVector _elements; - std::unordered_map _elementMap; - }; } #endif // hifi_render_Stage_h diff --git a/libraries/render/src/render/StageSetup.h b/libraries/render/src/render/StageSetup.h deleted file mode 100644 index 5691bca207..0000000000 --- a/libraries/render/src/render/StageSetup.h +++ /dev/null @@ -1,35 +0,0 @@ -// -// StageSetup.h -// render/src/render -// -// Created by HifiExperiments on 10/16/24 -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_render_StageSetup_h -#define hifi_render_StageSetup_h - -#include "Engine.h" - -namespace render { - - template - class StageSetup { - public: - StageSetup() {} - - void run(const RenderContextPointer& renderContext) { - if (renderContext->_scene) { - auto stage = renderContext->_scene->getStage(T::getName()); - if (!stage) { - renderContext->_scene->resetStage(T::getName(), std::make_shared()); - } - } - } - }; -} - -#endif // hifi_render_StageSetup_h diff --git a/libraries/render/src/render/TransitionStage.cpp b/libraries/render/src/render/TransitionStage.cpp index 72637cc3d9..9ddc72f4db 100644 --- a/libraries/render/src/render/TransitionStage.cpp +++ b/libraries/render/src/render/TransitionStage.cpp @@ -1,25 +1,43 @@ -// -// TransitionStage.cpp -// -// Created by Olivier Prat on 07/07/2017. -// Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - #include "TransitionStage.h" +#include + using namespace render; -template <> -std::string TypedStage::_name { "TRANSITION_STAGE" }; +std::string TransitionStage::_name("Transition"); +const TransitionStage::Index TransitionStage::INVALID_INDEX{ indexed_container::INVALID_INDEX }; TransitionStage::Index TransitionStage::addTransition(ItemID itemId, Transition::Type type, ItemID boundId) { Transition transition; + Index id; + transition.eventType = type; transition.itemId = itemId; transition.boundItemId = boundId; - return addElement(transition); + id = _transitions.newElement(transition); + _activeTransitionIds.push_back(id); + + return id; +} + +void TransitionStage::removeTransition(Index index) { + TransitionIdList::iterator idIterator = std::find(_activeTransitionIds.begin(), _activeTransitionIds.end(), index); + if (idIterator != _activeTransitionIds.end()) { + _activeTransitionIds.erase(idIterator); + } + if (!_transitions.isElementFreed(index)) { + _transitions.freeElement(index); + } } + +TransitionStageSetup::TransitionStageSetup() { +} + +void TransitionStageSetup::run(const RenderContextPointer& renderContext) { + auto stage = renderContext->_scene->getStage(TransitionStage::getName()); + if (!stage) { + stage = std::make_shared(); + renderContext->_scene->resetStage(TransitionStage::getName(), stage); + } +} + diff --git a/libraries/render/src/render/TransitionStage.h b/libraries/render/src/render/TransitionStage.h index 11256fb346..abfdca9a06 100644 --- a/libraries/render/src/render/TransitionStage.h +++ b/libraries/render/src/render/TransitionStage.h @@ -1,9 +1,8 @@ // // TransitionStage.h -// + // Created by Olivier Prat on 07/07/2017. // Copyright 2017 High Fidelity, Inc. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,22 +12,55 @@ #define hifi_render_TransitionStage_h #include "Stage.h" -#include "StageSetup.h" +#include "IndexedContainer.h" +#include "Engine.h" #include "Transition.h" namespace render { // Transition stage to set up Transition-related effects - class TransitionStage : public TypedStage { + class TransitionStage : public render::Stage { public: - bool isTransitionUsed(Index index) const { return _elements.checkIndex(index) && !_elements.isElementFreed(index); } + + static const std::string& getName() { return _name; } + + using Index = indexed_container::Index; + static const Index INVALID_INDEX; + using TransitionIdList = indexed_container::Indices; + + static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; } + + bool isTransitionUsed(Index index) const { return _transitions.checkIndex(index) && !_transitions.isElementFreed(index); } + + const Transition& getTransition(Index TransitionId) const { return _transitions.get(TransitionId); } + + Transition& editTransition(Index TransitionId) { return _transitions.edit(TransitionId); } + Index addTransition(ItemID itemId, Transition::Type type, ItemID boundId); + void removeTransition(Index index); + + TransitionIdList::iterator begin() { return _activeTransitionIds.begin(); } + TransitionIdList::iterator end() { return _activeTransitionIds.end(); } + + private: + + using Transitions = indexed_container::IndexedVector; + + static std::string _name; + + Transitions _transitions; + TransitionIdList _activeTransitionIds; }; using TransitionStagePointer = std::shared_ptr; - class TransitionStageSetup : public StageSetup { + class TransitionStageSetup { public: - using JobModel = Job::Model; + using JobModel = render::Job::Model; + + TransitionStageSetup(); + void run(const RenderContextPointer& renderContext); + + protected: }; } diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index bc54bcc034..3c08c9a1bc 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -1,11 +1,12 @@ # Copyright 2013-2020, High Fidelity, Inc. -# Copyright 2021-2025 Overte e.V. +# Copyright 2021-2023 Overte e.V. # SPDX-License-Identifier: Apache-2.0 set(TARGET_NAME shared) include_directories("${QT_DIR}/include/QtCore/${QT_VERSION}/QtCore" "${QT_DIR}/include/QtCore/${QT_VERSION}") +# TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network) if (WIN32) diff --git a/libraries/shared/src/AACube.h b/libraries/shared/src/AACube.h index 27c424cadb..66b29e3185 100644 --- a/libraries/shared/src/AACube.h +++ b/libraries/shared/src/AACube.h @@ -20,7 +20,6 @@ #include #include "BoxBase.h" -#include "SerDes.h" class AABox; class Extents; @@ -81,10 +80,6 @@ class AACube { glm::vec3 _corner; float _scale; - - friend DataSerializer& operator<<(DataSerializer &ser, const AACube &cube); - friend DataDeserializer& operator>>(DataDeserializer &des, AACube &cube); - }; inline bool operator==(const AACube& a, const AACube& b) { @@ -104,16 +99,5 @@ inline QDebug operator<<(QDebug debug, const AACube& cube) { return debug; } -inline DataSerializer& operator<<(DataSerializer &ser, const AACube &cube) { - ser << cube._corner; - ser << cube._scale; - return ser; -} - -inline DataDeserializer& operator>>(DataDeserializer &des, AACube &cube) { - des >> cube._corner; - des >> cube._scale; - return des; -} #endif // hifi_AACube_h diff --git a/libraries/shared/src/BlendshapeConstants.h b/libraries/shared/src/BlendshapeConstants.h index b741059146..596e7df4ee 100644 --- a/libraries/shared/src/BlendshapeConstants.h +++ b/libraries/shared/src/BlendshapeConstants.h @@ -122,25 +122,6 @@ struct BlendshapeOffsetUnpacked { float positionOffsetX, positionOffsetY, positionOffsetZ; float normalOffsetX, normalOffsetY, normalOffsetZ; float tangentOffsetX, tangentOffsetY, tangentOffsetZ; - - /** - * @brief Set all components of all the offsets to zero - * - * @note glm::vec3 is not trivially copyable, so it's not correct to clear it with memset. - */ - void clear() { - positionOffsetX = 0.0f; - positionOffsetY = 0.0f; - positionOffsetZ = 0.0f; - - normalOffsetX = 0.0f; - normalOffsetY = 0.0f; - normalOffsetZ = 0.0f; - - tangentOffsetX = 0.0f; - tangentOffsetY = 0.0f; - tangentOffsetZ = 0.0f; - } }; using BlendshapeOffset = BlendshapeOffsetPacked; diff --git a/libraries/shared/src/PickFilter.h b/libraries/shared/src/PickFilter.h index acf0c70eab..1cc1a8b0b5 100644 --- a/libraries/shared/src/PickFilter.h +++ b/libraries/shared/src/PickFilter.h @@ -1,7 +1,6 @@ // -// Created by Sam Gondelman on December 7th, 2018. +// Created by Sam Gondelman on 12/7/18. // Copyright 2018 High Fidelity, Inc. -// Copyright 2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -27,7 +26,7 @@ class PickFilter { * PICK_DOMAIN_ENTITIES1Include domain entities when intersecting. * PICK_AVATAR_ENTITIES2Include avatar entities when intersecting. * PICK_LOCAL_ENTITIES4Include local entities when intersecting. - * PICK_AVATARS8Include avatars when intersecting. + * PICK_AVATATRS8Include avatars when intersecting. * PICK_HUD16Include the HUD surface when intersecting in HMD mode. * PICK_INCLUDE_VISIBLE32Include visible objects when intersecting. * PICK_INCLUDE_INVISIBLE64Include invisible objects when intersecting. diff --git a/libraries/shared/src/SerDes.cpp b/libraries/shared/src/SerDes.cpp deleted file mode 100644 index ad32d7014f..0000000000 --- a/libraries/shared/src/SerDes.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// -// SerDes.h -// -// -// Created by Dale Glass on 5/6/2022 -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include "SerDes.h" -const int DataSerializer::DEFAULT_SIZE; -const char DataSerializer::PADDING_CHAR; - - -static void dumpHex(QDebug &debug, const char*buf, size_t len) { - QString literal; - QString hex; - - for(size_t i=0;i(c), 16 ); - if ( hnum.length() == 1 ) { - hnum.prepend("0"); - } - - hex.append(hnum + " "); - - if ( literal.length() == 16 || (i+1 == len) ) { - while( literal.length() < 16 ) { - literal.append(" "); - hex.append(" "); - } - - debug << literal << " " << hex << "\n"; - literal.clear(); - hex.clear(); - } - } -} - - -QDebug operator<<(QDebug debug, const DataSerializer &ser) { - debug << "{ capacity =" << ser.capacity() << "; length = " << ser.length() << "; pos = " << ser.pos() << "}"; - debug << "\n"; - - dumpHex(debug, ser.buffer(), ser.length()); - return debug; -} - - -QDebug operator<<(QDebug debug, const DataDeserializer &des) { - debug << "{ length = " << des.length() << "; pos = " << des.pos() << "}"; - debug << "\n"; - - - dumpHex(debug, des.buffer(), des.length()); - return debug; -} - - -void DataSerializer::changeAllocation(size_t new_size) { - while ( _capacity < new_size) { - _capacity *= 2; - } - - char *new_buf = new char[_capacity]; - assert( *new_buf ); - - memcpy(new_buf, _store, _length); - char *prev_buf = _store; - _store = new_buf; - - delete []prev_buf; -} diff --git a/libraries/shared/src/SerDes.h b/libraries/shared/src/SerDes.h deleted file mode 100644 index f80d09a60a..0000000000 --- a/libraries/shared/src/SerDes.h +++ /dev/null @@ -1,953 +0,0 @@ -// -// SerDes.h -// -// -// Created by Dale Glass on 5/6/2022 -// Copyright 2024 Overte e.V. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#pragma once -#include -#include -#include -#include -#include - -/** - * @brief Data serializer - * - * When encoding, this class takes in data and encodes it into a buffer. No attempt is made to store version numbers, lengths, - * or any other metadata. It's entirely up to the user to use the class in such a way that the process can be - * correctly reversed if variable-length or optional fields are used. - * - * It can operate both on an internal, dynamically-allocated buffer, or an externally provided, fixed-size one. - * If an external store is used, the class will refuse to add data once capacity is reached and set the overflow flag. - * When decoding, this class operates on a fixed size buffer. If an attempt to read past the end is made, the read fails, - * and the overflow flag is set. - * - * The class was written for the maximum simplicity possible and inline friendliness. - * - * Example of encoding: - * - * @code {.cpp} - * uint8_t version = 1; - * uint16_t count = 1; - * glm::vec3 pos{1.5, 2.0, 9.0}; - * - * Serializer ser; - * ser << version; - * ser << count; - * ser << pos; - * - * // Serialized data is in ser.buffer(), ser.length() long. - * @endcode - * - * This object should be modified directly to add support for any primitive and common datatypes in the code. To support serializing/deserializing - * classes and structures, implement a `operator<<` and `operator>>` functions for that object, eg: - * - * @code {.cpp} - * DataSerializer &operator<<(DataSerializer &ser, const Object &o) { - * ser << o._borderColor; - * ser << o._maxAnisotropy; - * ser << o._filter; - * return ser; - * } - * @endcode - * - */ -class DataSerializer { - public: - - /** - * @brief RAII tracker of advance position - * - * When a custom operator<< is implemented for DataSserializer, - * this class allows to easily keep track of how much data has been added and - * adjust the parent's lastAdvance() count on this class' destruction. - * - * @code {.cpp} - * DataSerializer &operator<<(DataSerializer &ser, const Object &o) { - * DataSerializer::SizeTracker tracker(ser); - * - * ser << o._borderColor; - * ser << o._maxAnisotropy; - * ser << o._filter; - * return ser; - * } - * @endcode - */ - class SizeTracker { - public: - SizeTracker(DataSerializer &parent) : _parent(parent) { - _start_pos = _parent.pos(); - } - - ~SizeTracker() { - size_t cur_pos = _parent.pos(); - - if ( cur_pos >= _start_pos ) { - _parent._lastAdvance = cur_pos - _start_pos; - } else { - _parent._lastAdvance = 0; - } - } - - private: - DataSerializer &_parent; - size_t _start_pos = 0; - }; - - /** - * @brief Default size for a dynamically allocated buffer. - * - * Since this is mostly intended to be used for networking, we default to the largest probable MTU here. - */ - static const int DEFAULT_SIZE = 1500; - - /** - * @brief Character to use for padding. - * - * Padding should be ignored, so it doesn't matter what we go with here, but it can be useful to set it - * to something that would be distinctive in a dump. - */ - static const char PADDING_CHAR = (char)0xAA; - - /** - * @brief Construct a dynamically allocated serializer - * - * If constructed this way, an internal buffer will be dynamically allocated and grown as needed. - * - * The buffer is SerDes::DEFAULT_SIZE bytes by default, and doubles in size every time the limit is reached. - */ - DataSerializer() { - _capacity = DEFAULT_SIZE; - _pos = 0; - _length = 0; - _store = new char[_capacity]; - } - - /** - * @brief Construct a statically allocated serializer - * - * If constructed this way, the external buffer will be used to store data. The class will refuse to - * keep adding data if the maximum length is reached, write a critical message to the log, and set - * the overflow flag. - * - * The flag can be read with isOverflow() - * - * @param externalStore External data store - * @param storeLength Length of the data store - */ - DataSerializer(char *externalStore, size_t storeLength) { - _capacity = storeLength; - _length = 0; - _pos = 0; - _storeIsExternal = true; - _store = externalStore; - } - - /** - * @brief Construct a statically allocated serializer - * - * If constructed this way, the external buffer will be used to store data. The class will refuse to - * keep adding data if the maximum length is reached, and set the overflow flag. - * - * The flag can be read with isOverflow() - * - * @param externalStore External data store - * @param storeLength Length of the data store - */ - DataSerializer(uint8_t *externalStore, size_t storeLength) : DataSerializer((char*)externalStore, storeLength) { - - } - - DataSerializer(const DataSerializer &) = delete; - DataSerializer &operator=(const DataSerializer &) = delete; - - - - ~DataSerializer() { - if (!_storeIsExternal) { - delete[] _store; - } - } - - /** - * @brief Adds padding to the output - * - * The bytes will be set to SerDes::PADDING_CHAR, which is a constant in the source code. - * Since padding isn't supposed to be read, it can be any value and is intended to - * be set to something that can be easily recognized in a dump. - * - * @param bytes Number of bytes to add - */ - void addPadding(size_t bytes) { - if (!extendBy(bytes, "padding")) { - return; - } - - // Fill padding with something recognizable. Will keep valgrind happier. - memset(&_store[_pos], PADDING_CHAR, bytes); - _pos += bytes; - } - - /** - * @brief Add an uint8_t to the output - * - * @param c Character to add - * @return SerDes& This object - */ - DataSerializer &operator<<(uint8_t c) { - return *this << int8_t(c); - } - - /** - * @brief Add an int8_t to the output - * - * @param c Character to add - * @return SerDes& This object - */ - DataSerializer &operator<<(int8_t c) { - if (!extendBy(1, "int8_t")) { - return *this; - } - - _store[_pos++] = c; - return *this; - } - - - /////////////////////////////////////////////////////////// - - /** - * @brief Add an uint16_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(uint16_t val) { - return *this << int16_t(val); - } - - /** - * @brief Add an int16_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(int16_t val) { - if (!extendBy(sizeof(val), "int16_t")) { - return *this; - } - - memcpy(&_store[_pos], (char*)&val, sizeof(val)); - _pos += sizeof(val); - return *this; - } - - - - /////////////////////////////////////////////////////////// - - /** - * @brief Add an uint32_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(uint32_t val) { - return *this << int32_t(val); - } - - /** - * @brief Add an int32_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(int32_t val) { - if (!extendBy(sizeof(val), "int32_t")) { - return *this; - } - - memcpy(&_store[_pos], (char*)&val, sizeof(val)); - _pos += sizeof(val); - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Add an uint64_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(uint64_t val) { - return *this << int64_t(val); - } - - /** - * @brief Add an int64_t to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(int64_t val) { - if (!extendBy(sizeof(val), "int64_t")) { - return *this; - } - - memcpy(&_store[_pos], (char*)&val, sizeof(val)); - _pos += sizeof(val); - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Add an float to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(float val) { - if (extendBy(sizeof(val), "float")) { - memcpy(&_store[_pos], (char*)&val, sizeof(val)); - _pos += sizeof(val); - } - return *this; - } - - - /////////////////////////////////////////////////////////// - - - /** - * @brief Add an glm::vec3 to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(glm::vec3 val) { - size_t sz = sizeof(val.x); - if (extendBy(sz*3, "glm::vec3")) { - memcpy(&_store[_pos ], (char*)&val.x, sz); - memcpy(&_store[_pos + sz ], (char*)&val.y, sz); - memcpy(&_store[_pos + sz*2], (char*)&val.z, sz); - - _pos += sz*3; - } - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Add a glm::vec4 to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(glm::vec4 val) { - size_t sz = sizeof(val.x); - if (extendBy(sz*4, "glm::vec4")) { - memcpy(&_store[_pos ], (char*)&val.x, sz); - memcpy(&_store[_pos + sz ], (char*)&val.y, sz); - memcpy(&_store[_pos + sz*2], (char*)&val.z, sz); - memcpy(&_store[_pos + sz*3], (char*)&val.w, sz); - - _pos += sz*4; - } - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Add a glm::ivec2 to the output - * - * @param val Value to add - * @return SerDes& This object - */ - DataSerializer &operator<<(glm::ivec2 val) { - size_t sz = sizeof(val.x); - if (extendBy(sz*2, "glm::ivec2")) { - memcpy(&_store[_pos ], (char*)&val.x, sz); - memcpy(&_store[_pos + sz ], (char*)&val.y, sz); - - _pos += sz*2; - } - return *this; - } - - - /////////////////////////////////////////////////////////// - - /** - * @brief Write a null-terminated string into the buffer - * - * The `\0` at the end of the string is also written. - * - * @param val Value to write - * @return SerDes& This object - */ - DataSerializer &operator<<(const char *val) { - size_t len = strlen(val)+1; - if (extendBy(len, "string")) { - memcpy(&_store[_pos], val, len); - _pos += len; - } - return *this; - } - - /** - * @brief Write a QString into the buffer - * - * The string is encoded in UTF-8 and the `\0` at the end of the string is also written. - * - * @param val Value to write - * @return SerDes& This object - */ - DataSerializer &operator<<(const QString &val) { - return *this << val.toUtf8().constData(); - } - - - /////////////////////////////////////////////////////////// - - /** - * @brief Pointer to the start of the internal buffer. - * - * The allocated amount can be found with capacity(). - * - * The end of the stored data can be found with length(). - * - * @return Pointer to buffer - */ - char *buffer() const { return _store; } - - /** - * @brief Current position in the buffer. Starts at 0. - * - * @return size_t - */ - size_t pos() const { return _pos; } - - /** - * @brief Last position that was written to in the buffer. Starts at 0. - * - * @return size_t - */ - size_t length() const { return _length; } - - /** - * @brief Current capacity of the buffer. - * - * If the buffer is dynamically allocated, it can grow. - * - * If the buffer is static, this is a fixed limit. - * - * @return size_t - */ - size_t capacity() const { return _capacity; } - - /** - * @brief Whether there's any data in the buffer - * - * @return true Something has been written - * @return false The buffer is empty - */ - bool isEmpty() const { return _length == 0; } - - /** - * @brief The buffer size limit has been reached - * - * This can only return true for a statically allocated buffer. - * - * @return true Limit reached - * @return false There is still room - */ - bool isOverflow() const { return _overflow; } - - /** - * @brief Reset the serializer to the start, clear overflow bit. - * - */ - void rewind() { _pos = 0; _overflow = false; _lastAdvance = 0; } - - - /** - * @brief Size of the last advance - * - * This can be used to get how many bytes were added in the last operation. - * It is reset on rewind() - * - * @return size_t - */ - size_t lastAdvance() const { return _lastAdvance; } - - /** - * @brief Dump the contents of this object into QDebug - * - * This produces a dump of the internal state, and an ASCII/hex dump of - * the contents, for debugging. - * - * @param debug Qt QDebug stream - * @param ds This object - * @return QDebug - */ - friend QDebug operator<<(QDebug debug, const DataSerializer &ds); - - private: - bool extendBy(size_t bytes, const QString &type_name) { - //qDebug() << "Extend by" << bytes; - - if ( _capacity < _length + bytes) { - if ( _storeIsExternal ) { - qCritical() << "Serializer trying to write past end of output buffer of" << _capacity << "bytes. Error writing" << bytes << "bytes for" << type_name << " from position " << _pos << ", length " << _length; - _overflow = true; - return false; - } - - changeAllocation(_length + bytes); - } - - _length += bytes; - _lastAdvance = bytes; - return true; - } - - // This is split up here to try to make the class as inline-friendly as possible. - void changeAllocation(size_t new_size); - - char *_store; - bool _storeIsExternal = false; - bool _overflow = false; - size_t _capacity = 0; - size_t _length = 0; - size_t _pos = 0; - size_t _lastAdvance = 0; -}; - -/** - * @brief Data deserializer - * - * This class operates on a fixed size buffer. If an attempt to read past the end is made, the read fails, - * and the overflow flag is set. - * - * The class was written for the maximum simplicity possible and inline friendliness. - * - * Example of decoding: - * - * @code {.cpp} - * // Incoming data has been placed in: - * // char buffer[1024]; - * - * uint8_t version; - * uint16_t count; - * glm::vec3 pos; - * - * DataDeserializer des(buffer, sizeof(buffer)); - * des >> version; - * des >> count; - * des >> pos; - * @endcode - * - * This object should be modified directly to add support for any primitive and common datatypes in the code. To support deserializing - * classes and structures, implement an `operator>>` function for that object, eg: - * - * @code {.cpp} - * DataDeserializer &operator>>(DataDeserializer &des, Object &o) { - * des >> o._borderColor; - * des >> o._maxAnisotropy; - * des >> o._filter; - * return des; - * } - * @endcode - * - */ -class DataDeserializer { - public: - - /** - * @brief RAII tracker of advance position - * - * When a custom operator>> is implemented for DataDeserializer, - * this class allows to easily keep track of how much data has been added and - * adjust the parent's lastAdvance() count on this class' destruction. - * - * @code {.cpp} - * DataDeserializer &operator>>(Deserializer &des, Object &o) { - * DataDeserializer::SizeTracker tracker(des); - * - * des >> o._borderColor; - * des >> o._maxAnisotropy; - * des >> o._filter; - * return des; - * } - * @endcode - */ - class SizeTracker { - public: - SizeTracker(DataDeserializer &parent) : _parent(parent) { - _start_pos = _parent.pos(); - } - - ~SizeTracker() { - size_t cur_pos = _parent.pos(); - - if ( cur_pos >= _start_pos ) { - _parent._lastAdvance = cur_pos - _start_pos; - } else { - _parent._lastAdvance = 0; - } - } - - private: - DataDeserializer &_parent; - size_t _start_pos = 0; - }; - - /** - * @brief Construct a Deserializer - * * - * @param externalStore External data store - * @param storeLength Length of the data store - */ - DataDeserializer(const char *externalStore, size_t storeLength) { - _length = storeLength; - _pos = 0; - _store = externalStore; - _lastAdvance = 0; - } - - /** - * @brief Construct a Deserializer - * - * @param externalStore External data store - * @param storeLength Length of the data store - */ - DataDeserializer(const uint8_t *externalStore, size_t storeLength) : DataDeserializer((const char*)externalStore, storeLength) { - - } - - /** - * @brief Construct a new Deserializer reading data from a Serializer - * - * This is a convenience function for testing. - * - * @param serializer Serializer with data - */ - DataDeserializer(const DataSerializer &serializer) : DataDeserializer(serializer.buffer(), serializer.length()) { - - } - - /** - * @brief Skips padding in the input - * - * @param bytes Number of bytes to skip - */ - void skipPadding(size_t bytes) { - if (!canAdvanceBy(bytes, "padding")) { - return; - } - - _pos += bytes; - _lastAdvance = bytes; - } - - - /** - * @brief Read an uint8_t from the buffer - * - * @param c Character to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(uint8_t &c) { - return *this >> reinterpret_cast(c); - } - - /** - * @brief Read an int8_t from the buffer - * - * @param c Character to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(int8_t &c) { - if ( canAdvanceBy(1, "int8_t") ) { - c = _store[_pos++]; - _lastAdvance = sizeof(c); - } - - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Read an uint16_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(uint16_t &val) { - return *this >> reinterpret_cast(val); - } - - /** - * @brief Read an int16_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(int16_t &val) { - if ( canAdvanceBy(sizeof(val), "int16_t") ) { - memcpy((char*)&val, &_store[_pos], sizeof(val)); - _pos += sizeof(val); - _lastAdvance = sizeof(val); - } - - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Read an uint32_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(uint32_t &val) { - return *this >> reinterpret_cast(val); - } - - /** - * @brief Read an int32_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(int32_t &val) { - if ( canAdvanceBy(sizeof(val), "int32_t") ) { - memcpy((char*)&val, &_store[_pos], sizeof(val)); - _pos += sizeof(val); - _lastAdvance = sizeof(val); - } - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Read an uint64_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(uint64_t &val) { - return *this >> reinterpret_cast(val); - } - - /** - * @brief Read an int64_t from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(int64_t &val) { - if ( canAdvanceBy(sizeof(val), "int64_t") ) { - memcpy((char*)&val, &_store[_pos], sizeof(val)); - _pos += sizeof(val); - _lastAdvance = sizeof(val); - } - return *this; - } - - /////////////////////////////////////////////////////////// - - - /** - * @brief Read an float from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(float &val) { - if ( canAdvanceBy(sizeof(val), "float") ) { - memcpy((char*)&val, &_store[_pos], sizeof(val)); - _pos += sizeof(val); - _lastAdvance = sizeof(val); - } - return *this; - } - - /////////////////////////////////////////////////////////// - - - - - /** - * @brief Read a glm::vec3 from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(glm::vec3 &val) { - size_t sz = sizeof(val.x); - - if ( canAdvanceBy(sz*3, "glm::vec3") ) { - memcpy((char*)&val.x, &_store[_pos ], sz); - memcpy((char*)&val.y, &_store[_pos + sz ], sz); - memcpy((char*)&val.z, &_store[_pos + sz*2], sz); - - _pos += sz*3; - _lastAdvance = sz * 3; - } - - return *this; - } - - /////////////////////////////////////////////////////////// - - - /** - * @brief Read a glm::vec4 from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(glm::vec4 &val) { - size_t sz = sizeof(val.x); - - if ( canAdvanceBy(sz*4, "glm::vec4")) { - memcpy((char*)&val.x, &_store[_pos ], sz); - memcpy((char*)&val.y, &_store[_pos + sz ], sz); - memcpy((char*)&val.z, &_store[_pos + sz*2], sz); - memcpy((char*)&val.w, &_store[_pos + sz*3], sz); - - _pos += sz*4; - _lastAdvance = sz*4; - } - return *this; - } - - /////////////////////////////////////////////////////////// - - - /** - * @brief Read a glm::ivec2 from the buffer - * - * @param val Value to read - * @return SerDes& This object - */ - DataDeserializer &operator>>(glm::ivec2 &val) { - size_t sz = sizeof(val.x); - - if ( canAdvanceBy(sz*2, "glm::ivec2") ) { - memcpy((char*)&val.x, &_store[_pos ], sz); - memcpy((char*)&val.y, &_store[_pos + sz ], sz); - - _pos += sz*2; - _lastAdvance = sz * 2; - } - - return *this; - } - - /////////////////////////////////////////////////////////// - - /** - * @brief Pointer to the start of the internal buffer. - * - * The allocated amount can be found with capacity(). - * - * The end of the stored data can be found with length(). - * - * @return Pointer to buffer - */ - const char *buffer() const { return _store; } - - /** - * @brief Current position in the buffer. Starts at 0. - * - * @return size_t - */ - size_t pos() const { return _pos; } - - /** - * @brief Last position that was written to in the buffer. Starts at 0. - * - * @return size_t - */ - size_t length() const { return _length; } - - /** - * @brief Whether there's any data in the buffer - * - * @return true Something has been written - * @return false The buffer is empty - */ - bool isEmpty() const { return _length == 0; } - - /** - * @brief The buffer size limit has been reached - * - * This can only return true for a statically allocated buffer. - * - * @return true Limit reached - * @return false There is still room - */ - bool isOverflow() const { return _overflow; } - - /** - * @brief Reset the serializer to the start, clear overflow bit. - * - */ - void rewind() { _pos = 0; _overflow = false; _lastAdvance = 0; } - - /** - * @brief Size of the last advance - * - * This can be used to get how many bytes were added in the last operation. - * It is reset on rewind() - * - * @return size_t - */ - size_t lastAdvance() const { return _lastAdvance; } - - /** - * @brief Dump the contents of this object into QDebug - * - * This produces a dump of the internal state, and an ASCII/hex dump of - * the contents, for debugging. - * - * @param debug Qt QDebug stream - * @param ds This object - * @return QDebug - */ - friend QDebug operator<<(QDebug debug, const DataDeserializer &ds); - - private: - bool canAdvanceBy(size_t bytes, const QString &type_name) { - //qDebug() << "Checking advance by" << bytes; - - if ( _length < _pos + bytes) { - qCritical() << "Deserializer trying to read past end of input buffer of" << _length << "bytes, reading" << bytes << "bytes for" << type_name << "from position " << _pos; - _overflow = true; - return false; - } - - return true; - } - - const char *_store; - bool _overflow = false; - size_t _length = 0; - size_t _pos = 0; - size_t _lastAdvance = 0; -}; diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 24df1c9e0f..82cab1c76c 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -261,8 +261,6 @@ visible: false }); - var savedClippingEnabled = false; - function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { // Adjust the position such that the bounding box (registration, dimensions and orientation) lies behind the original // position in the given direction. @@ -1197,7 +1195,6 @@ selectionDisplay.disableTriggerMapping(); tablet.landscape = false; Controller.disableMapping(CONTROLLER_MAPPING_NAME); - Render.cameraClippingEnabled = savedClippingEnabled; } else { if (shouldUseEditTabletApp()) { tablet.loadQMLSource(Script.resolvePath("qml/Edit.qml"), true); @@ -1215,8 +1212,6 @@ print("starting tablet in landscape mode"); tablet.landscape = true; Controller.enableMapping(CONTROLLER_MAPPING_NAME); - savedClippingEnabled = Render.cameraClippingEnabled; - Render.cameraClippingEnabled = false; // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); diff --git a/scripts/system/domainChat/domainChat.js b/scripts/system/domainChat/domainChat.js index 1fa85ba37a..74de8d1b9b 100644 --- a/scripts/system/domainChat/domainChat.js +++ b/scripts/system/domainChat/domainChat.js @@ -1,7 +1,7 @@ // // domainChat.js // -// Created by Armored Dragon, May 17th, 2024. +// Created by Armored Dragon, 2024. // Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. @@ -51,7 +51,6 @@ activeIcon: Script.resolvePath("./img/icon_black.png"), sortOrder: 8, text: "CHAT", - sortOrder: 8, isActive: appIsVisible, }); diff --git a/scripts/system/graphicsSettings.js b/scripts/system/graphicsSettings.js index 748eafca24..df556a91b1 100644 --- a/scripts/system/graphicsSettings.js +++ b/scripts/system/graphicsSettings.js @@ -1,20 +1,15 @@ // // graphicsSettings.js // -// Created by Kalila L. on August 5th, 2020 +// Created by Kalila L. on 8/5/2020 // Copyright 2020 Vircadia contributors. -// Copyright 2024 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function() { // BEGIN LOCAL_SCOPE - var channelComm = "Overte-ShowGraphicsIconChanged"; - var appStatus = false; - var GRAPHICS_HIDE_AND_SHOW_SETTING_KEY = "showGraphicsIcon"; - var GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE = true; - + var AppUi = Script.require('appUi'); // cellphone-cog MDI @@ -54,49 +49,22 @@ } function startup() { - if (!appStatus) { - ui = new AppUi({ - buttonName: BUTTON_NAME, - sortOrder: 8, - normalButton: getIcon(), - activeButton: getIcon().replace('white', 'black'), - home: GRAPHICS_QML_SOURCE - }); - } - appStatus = true; + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 8, + normalButton: getIcon(), + activeButton: getIcon().replace('white', 'black'), + home: GRAPHICS_QML_SOURCE + }); } function shutdown() { - if (appStatus) { - ui.onScriptEnding(); - appStatus = false; - } - } - - function cleanup() { - Messages.messageReceived.disconnect(onMessageReceived); - Messages.unsubscribe(channelComm); - } - - function onMessageReceived(channel, message, sender, localOnly) { - if (channel === channelComm && localOnly) { - if (Settings.getValue(GRAPHICS_HIDE_AND_SHOW_SETTING_KEY, GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE)) { - startup(); - } else { - shutdown(); - } - } } // // Run the functions. // - if (Settings.getValue(GRAPHICS_HIDE_AND_SHOW_SETTING_KEY, GRAPHICS_HIDE_AND_SHOW_DEFAULT_VALUE)) { - startup(); - } - Messages.subscribe(channelComm); - Messages.messageReceived.connect(onMessageReceived); - - Script.scriptEnding.connect(cleanup); + startup(); + Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/places/icons/portalFX.png b/scripts/system/places/icons/portalFX.png deleted file mode 100644 index 6c781c824befad6e069574c51eecc309f06047d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79835 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

    r1j|Oqmj~@JCxYf~aRq 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 diff --git a/scripts/system/places/places.css b/scripts/system/places/places.css index 684139a5b8..37eac2d002 100644 --- a/scripts/system/places/places.css +++ b/scripts/system/places/places.css @@ -3,7 +3,7 @@ // places.css // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022-2025 Overte e.V. +// Copyright 2022 Overte e.V. // // css for the ui of the Places application. // @@ -750,20 +750,19 @@ font.domain-nbrUser_small { color: #cccccc; padding: 10px; text-align: justify; - text-justify: inter-word; + text-justify: inter-word; } #placeDetail-visitBtn { background: #0000ff; background-image: linear-gradient(to bottom, #0000ff, #000020); border: 0px; - border-radius: 6px; - font-weight: 700; + border-radius: 10px; + font-weight: 800; color: #ffffff; - font-size: 14px; - padding: 2px 22px 2px 22px; + font-size: 20px; + padding: 3px 22px 3px 22px; text-decoration: none; - width: 90%; } #placeDetail-visitBtn:hover { @@ -775,57 +774,7 @@ font.domain-nbrUser_small { #placeDetail-visitBtn-container { width: 100%; text-align: left; - margin-bottom: 8px; -} - -#placeDetail-rezPortalBtn { - background: #0000ff; - background-image: linear-gradient(to bottom, #0000ff, #000020); - border: 0px; - border-radius: 6px; - font-weight: 700; - color: #ffffff; - font-size: 14px; - padding: 2px 22px 2px 22px; - text-decoration: none; - width: 90%; -} - -#placeDetail-rezPortalBtn:hover { - background: #057eff; - background-image: linear-gradient(to bottom, #057eff, #00090f); - text-decoration: none; -} - -#placeDetail-rezPortalBtn-container { - width: 100%; - text-align: left; - margin-bottom: 8px; -} - -#placeDetail-copyPlaceURLBtn { - background: #0000ff; - background-image: linear-gradient(to bottom, #0000ff, #000020); - border: 0px; - border-radius: 6px; - font-weight: 700; - color: #ffffff; - font-size: 14px; - padding: 2px 22px 2px 22px; - text-decoration: none; - width: 90%; -} - -#placeDetail-copyPlaceURLBtn:hover { - background: #057eff; - background-image: linear-gradient(to bottom, #057eff, #00090f); - text-decoration: none; -} - -#placeDetail-copyPlaceURLBtn-container { - width: 100%; - text-align: left; - margin-bottom: 8px; + margin-bottom: 40px; } #placeDetail-placedata { @@ -855,7 +804,7 @@ font.domain-nbrUser_small { #placeDetail-users { font-size: 30px; - font-weight: 600; + font-weight: 600; } #placeDetail-capacity { diff --git a/scripts/system/places/places.html b/scripts/system/places/places.html index 6b7727c2d4..fda67f4066 100644 --- a/scripts/system/places/places.html +++ b/scripts/system/places/places.html @@ -4,7 +4,7 @@ // places.html // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022-2025 Overte e.V. +// Copyright 2022 Overte e.V. // // html for the ui of the Places application. // @@ -107,7 +107,7 @@
      ×
      -
      +
      @@ -118,8 +118,6 @@
      -
      -
      DOMAIN: @@ -504,28 +502,6 @@ } - function rezPortal(name, address, placeID) { - var portalOrder = { - "channel": channel, - "action": "REQUEST_PORTAL", - "name": name, - "address": address, - "placeID": placeID - }; - EventBridge.emitWebEvent(JSON.stringify(portalOrder)); - - } - - function copyPlaceURL(address) { - var portalOrder = { - "channel": channel, - "action": "COPY_URL", - "address": address - }; - EventBridge.emitWebEvent(JSON.stringify(portalOrder)); - - } - function goHome() { var message = { "channel": channel, @@ -775,17 +751,12 @@ } } - var pictureUrl = ""; + document.getElementById("placeDetail-image").src = ""; if (placeDetail.thumbnail === "") { - pictureUrl = "icons/placeholder_" + placeDetail.metaverseRegion + ".jpg"; + document.getElementById("placeDetail-image").src = "icons/placeholder_" + placeDetail.metaverseRegion + ".jpg"; } else { - pictureUrl = placeDetail.thumbnail; + document.getElementById("placeDetail-image").src = placeDetail.thumbnail; } - document.getElementById("placeDetail-image").style.backgroundImage = "url(" + pictureUrl + ")"; - document.getElementById("placeDetail-image").style.backgroundRepeat = "no-repeat"; - document.getElementById("placeDetail-image").style.backgroundPosition = "center center"; - document.getElementById("placeDetail-image").style.backgroundSize = "cover"; - document.getElementById("placeDetail-placeName").innerHTML = placeDetail.name; document.getElementById("placeDetail-managers").innerHTML = "By
          " + placeDetail.managers; document.getElementById("placeDetail-description").innerHTML = placeDetail.description; @@ -795,8 +766,6 @@ placeUrl = "hifi://" + placeDetail.address; } document.getElementById("placeDetail-visitBtn-container").innerHTML = ""; - document.getElementById("placeDetail-rezPortalBtn-container").innerHTML = ""; - document.getElementById("placeDetail-copyPlaceURLBtn-container").innerHTML = ""; document.getElementById("placeDetail-maturity").innerHTML = placeDetail.maturity.toUpperCase(); document.getElementById("placeDetail-maturity").className = placeDetail.maturity + "FilterOn placeMaturity"; document.getElementById("placeDetail-domain").innerHTML = placeDetail.domain.toUpperCase(); diff --git a/scripts/system/places/places.js b/scripts/system/places/places.js index 5aa8d282b1..fa22d536b7 100644 --- a/scripts/system/places/places.js +++ b/scripts/system/places/places.js @@ -3,7 +3,7 @@ // places.js // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022-2025 Overte e.V. +// Copyright 2022-2023 Overte e.V. // // Generate an explore app based on the differents source of placename data. // @@ -36,12 +36,6 @@ var APP_ICON_ACTIVE = ROOT + "icons/appicon_a.png"; var appStatus = false; var channel = "com.overte.places"; - - var portalChannelName = "com.overte.places.portalRezzer"; - var MAX_DISTANCE_TO_CONSIDER_PORTAL = 100.0; //in meters - var PORTAL_DURATION_MILLISEC = 45000; //45 sec - var rezzerPortalCount = 0; - var MAX_REZZED_PORTAL = 15; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -98,24 +92,6 @@ Window.location = messageObj.address; } - } else if (messageObj.action === "REQUEST_PORTAL" && (n - timestamp) > INTERCALL_DELAY) { - d = new Date(); - timestamp = d.getTime(); - var portalPosition = Vec3.sum(MyAvatar.feetPosition, Vec3.multiplyQbyV(MyAvatar.orientation, {"x": 0.0, "y": 0.0, "z": -2.0})); - var requestToSend = { - "action": "REZ_PORTAL", - "position": portalPosition, - "url": messageObj.address, - "name": messageObj.name, - "placeID": messageObj.placeID - }; - Messages.sendMessage(portalChannelName, JSON.stringify(requestToSend), false); - - } else if (messageObj.action === "COPY_URL" && (n - timestamp) > INTERCALL_DELAY) { - d = new Date(); - timestamp = d.getTime(); - Window.copyToClipboard(messageObj.address); - Window.displayAnnouncement("Place URL copied."); } else if (messageObj.action === "GO_HOME" && (n - timestamp) > INTERCALL_DELAY) { d = new Date(); timestamp = d.getTime(); @@ -308,8 +284,8 @@ region = "local"; order = "A"; fetch = true; - pinned = false; - currentFound = true; + pinned = false; + currentFound = true; } else { region = "federation"; order = "F"; @@ -579,57 +555,6 @@ } //####### END of seed random library ################ - function onMessageReceived(paramChannel, paramMessage, paramSender, paramLocalOnly) { - if (paramChannel === portalChannelName) { - var instruction = JSON.parse(paramMessage); - if (instruction.action === "REZ_PORTAL") { - generatePortal(instruction.position, instruction.url, instruction.name, instruction.placeID); - } - } - } - - function generatePortal(position, url, name, placeID) { - if (rezzerPortalCount <= MAX_REZZED_PORTAL) { - var TOLERANCE_FACTOR = 1.1; - if (Vec3.distance(MyAvatar.position, position) < MAX_DISTANCE_TO_CONSIDER_PORTAL) { - var height = MyAvatar.userHeight * MyAvatar.scale * TOLERANCE_FACTOR; - - var portalPosition = Vec3.sum(position, {"x": 0.0, "y": height/2, "z": 0.0}); - var dimensions = {"x": height * 0.618, "y": height, "z": height * 0.618}; - var userdata = { - "url": url, - "name": name, - "placeID": placeID - }; - - var portalID = Entities.addEntity({ - "position": portalPosition, - "dimensions": dimensions, - "type": "Shape", - "shape": "Sphere", - "name": "Portal to " + name, - "canCastShadow": false, - "collisionless": true, - "userData": JSON.stringify(userdata), - "script": ROOT + "portal.js", - "visible": "false", - "grab": { - "grabbable": false - } - }, "local"); - rezzerPortalCount = rezzerPortalCount + 1; - - Script.setTimeout(function () { - Entities.deleteEntity(portalID); - rezzerPortalCount = rezzerPortalCount - 1; - if (rezzerPortalCount < 0) { - rezzerPortalCount = 0; - } - }, PORTAL_DURATION_MILLISEC); - } - } - } - function cleanup() { if (appStatus) { @@ -637,15 +562,9 @@ tablet.webEventReceived.disconnect(onAppWebEventReceived); } - Messages.messageReceived.disconnect(onMessageReceived); - Messages.unsubscribe(portalChannelName); - tablet.screenChanged.disconnect(onScreenChanged); tablet.removeButton(button); } - Messages.subscribe(portalChannelName); - Messages.messageReceived.connect(onMessageReceived); - Script.scriptEnding.connect(cleanup); }()); diff --git a/scripts/system/places/portal.js b/scripts/system/places/portal.js deleted file mode 100644 index c77fbc648d..0000000000 --- a/scripts/system/places/portal.js +++ /dev/null @@ -1,201 +0,0 @@ -// -// portal.js -// -// Created by Alezia Kurdis, January 14th, 2025. -// Copyright 2025, Overte e.V. -// -// 3D portal for Places app. portal spawner. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -(function(){ - - var ROOT = Script.resolvePath('').split("portal.js")[0]; - var portalURL = ""; - var portalName = ""; - var TP_SOUND = SoundCache.getSound(ROOT + "sounds/teleportSound.mp3"); - - this.preload = function(entityID) { - - var properties = Entities.getEntityProperties(entityID, ["userData", "dimensions"]); - var userDataObj = JSON.parse(properties.userData); - portalURL = userDataObj.url; - portalName = userDataObj.name; - var portalColor = getColorFromPlaceID(userDataObj.placeID); - - var textLocalPosition = {"x": 0.0, "y": (properties.dimensions.y / 2) * 1.2, "z": 0.0}; - var scale = textLocalPosition.y/1.2; - var textID = Entities.addEntity({ - "type": "Text", - "parentID": entityID, - "localPosition": textLocalPosition, - "dimensions": { - "x": 1 * scale, - "y": 0.15 * scale, - "z": 0.01 - }, - "name": portalName, - "text": portalName, - "textColor": portalColor.light, - "lineHeight": 0.10 * scale, - "backgroundAlpha": 0.0, - "unlit": true, - "alignment": "center", - "verticalAlignment": "center", - "canCastShadow": false, - "billboardMode": "yaw", - "grab": { - "grabbable": false - } - },"local"); - - var fxID = Entities.addEntity({ - "type": "ParticleEffect", - "parentID": entityID, - "localPosition": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "name": "PORTAL_FX", - "dimensions": { - "x": 5.2 * scale, - "y": 5.2 * scale, - "z": 5.2 * scale - }, - "grab": { - "grabbable": false - }, - "shapeType": "ellipsoid", - "color": portalColor.light, - "alpha": 0.1, - "textures": ROOT + "icons/portalFX.png", - "maxParticles": 600, - "lifespan": 0.6, - "emitRate": 1000, - "emitSpeed": -1 * scale, - "speedSpread": 0 * scale, - "emitOrientation": { - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "emitDimensions": { - "x": 1.28 * scale, - "y": 2 * scale, - "z": 1.28 * scale - }, - "polarFinish": 3.1415927410125732, - "emitAcceleration": { - "x": 0, - "y": 0, - "z": 0 - }, - "particleRadius": 0.4000000059604645 * scale, - "radiusSpread": 0.30000001192092896 * scale, - "radiusStart": 1 * scale, - "radiusFinish": 0 * scale, - "colorStart": portalColor.saturated, - "colorFinish": { - "red": 255, - "green": 255, - "blue": 255 - }, - "alphaSpread": 0.019999999552965164, - "alphaStart": 0, - "alphaFinish": 0.20000000298023224, - "emitterShouldTrail": true, - "particleSpin": 1.5700000524520874, - "spinSpread": 2.9700000286102295, - "spinStart": 0, - "spinFinish": 0 - },"local"); - - var loopSoundID = Entities.addEntity({ - "type": "Sound", - "parentID": entityID, - "localPosition": {"x": 0.0, "y": 0.0, "z": 0.0}, - "name": "PORTAL SOUND", - "soundURL": ROOT + "sounds/portalSound.mp3", - "volume": 0.15, - "loop": true, - "positional": true, - "localOnly": true - },"local"); - - } - - this.enterEntity = function(entityID) { - var injectorOptions = { - "position": MyAvatar.position, - "volume": 0.3, - "loop": false, - "localOnly": true - }; - var injector = Audio.playSound(TP_SOUND, injectorOptions); - - var timer = Script.setTimeout(function () { - Window.location = portalURL; - Entities.deleteEntity(entityID); - }, 1000); - - }; - - function getColorFromPlaceID(placeID) { - var idIntegerConstant = getStringScore(placeID); - var hue = (idIntegerConstant%360)/360; - var color = hslToRgb(hue, 1, 0.5); - var colorLight = hslToRgb(hue, 1, 0.75); - return { - "saturated": {"red": color[0], "green": color[1], "blue": color[2]}, - "light": {"red": colorLight[0], "green": colorLight[1], "blue": colorLight[2]}, - }; - } - - function getStringScore(str) { - var score = 0; - for (var j = 0; j < str.length; j++){ - score += str.charCodeAt(j); - } - return score; - } - - /* - * Converts an HSL color value to RGB. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSL_color_space. - * Assumes h, s, and l are contained in the set [0, 1] and - * returns r, g, and b in the set [0, 255]. - * - * @param {number} h The hue - * @param {number} s The saturation - * @param {number} l The lightness - * @return {Array} The RGB representation - */ - function hslToRgb(h, s, l){ - var r, g, b; - - if(s == 0){ - r = g = b = l; // achromatic - }else{ - var hue2rgb = function hue2rgb(p, q, t){ - if(t < 0) t += 1; - if(t > 1) t -= 1; - if(t < 1/6) return p + (q - p) * 6 * t; - if(t < 1/2) return q; - if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; - return p; - } - - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hue2rgb(p, q, h + 1/3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1/3); - } - - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; - } - -}) diff --git a/scripts/system/places/sounds/portalSound.mp3 b/scripts/system/places/sounds/portalSound.mp3 deleted file mode 100644 index 5e7f5a9bd0f170da8994e137599daed2dcd7349b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83172 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

      _$9H7q?sv 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

    • DV!sKC)kcT+!@y3hgJ>X-B*5Z|@C7~~BhN@} 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 F{4aUD&$<8r diff --git a/scripts/system/places/sounds/teleportSound.mp3 b/scripts/system/places/sounds/teleportSound.mp3 deleted file mode 100644 index e5000e55b57821d16d313ca575411428a6b033c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53079 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 bCDHr0vKlEzMJf~$)y7cIfIx8jKePV #include #include -#include "SerDes.h" QTEST_GUILESS_MAIN(KtxTests) @@ -32,19 +31,6 @@ QString getRootPath() { return result; } -#if 0 -ktx::Byte* serializeSPH(ktx::Byte* data, const gpu::IrradianceKTXPayload &payload) const { - *(ktx::IrradianceKTXPayload::Version*)data = IrradianceKTXPayload::CURRENT_VERSION; - data += sizeof(ktx::IrradianceKTXPayload::Version); - - memcpy(data, &payload._irradianceSH, sizeof(ktx::SphericalHarmonics)); - data += sizeof(SphericalHarmonics); - - return data + PADDING; -} -#endif - - void KtxTests::initTestCase() { } @@ -161,14 +147,6 @@ void KtxTests::testKtxSerialization() { testTexture->setKtxBacking(TEST_IMAGE_KTX.fileName().toStdString()); } - -void KtxTests::testKtxNewSerializationSphericalHarmonics() { - DataSerializer ser; - - -} - - #if 0 static const QString TEST_FOLDER { "H:/ktx_cacheold" }; diff --git a/tests/ktx/src/KtxTests.h b/tests/ktx/src/KtxTests.h index c59fc17ccc..5627dc313d 100644 --- a/tests/ktx/src/KtxTests.h +++ b/tests/ktx/src/KtxTests.h @@ -16,7 +16,6 @@ private slots: void testKtxEvalFunctions(); void testKhronosCompressionFunctions(); void testKtxSerialization(); - void testKtxNewSerializationSphericalHarmonics(); }; diff --git a/tests/shared/src/SerializerTests.cpp b/tests/shared/src/SerializerTests.cpp deleted file mode 100644 index ae0198f573..0000000000 --- a/tests/shared/src/SerializerTests.cpp +++ /dev/null @@ -1,232 +0,0 @@ -// -// SerializerTests.cpp -// -// Copyright 2022 Dale Glass -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include "SerializerTests.h" -#include -#include -#include - -QTEST_GUILESS_MAIN(SerializerTests) - - -void SerializerTests::initTestCase() { -} - -void SerializerTests::testCreate() { - DataSerializer s; - QCOMPARE(s.length(), 0); - QCOMPARE(s.capacity(), DataSerializer::DEFAULT_SIZE); - QCOMPARE(s.isEmpty(), true); - - - DataDeserializer d(s); - QCOMPARE(d.length(), 0); -} - -void SerializerTests::testAdd() { - DataSerializer s; - s << (qint8)1; - QCOMPARE(s.length(), 1); - QCOMPARE(s.isEmpty(), false); - - s << (quint8)-1; - QCOMPARE(s.length(), 2); - - s << (qint16)0xaabb; - QCOMPARE(s.length(), 4); - - s << (quint16)-18000; - QCOMPARE(s.length(), 6); - - s << (qint32)0xCCDDEEFF; - QCOMPARE(s.length(), 10); - - s << (quint32)-1818000000; - QCOMPARE(s.length(), 14); - - s << "Hello, world!"; - QCOMPARE(s.length(), 28); - - glm::vec3 v{1.f,2.f,3.f}; - s << v; - QCOMPARE(s.length(), 40); - - s << 1.2345f; - QCOMPARE(s.length(), 44); - - - qDebug() << s; -} - -void SerializerTests::testAddAndRead() { - DataSerializer s; - glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; - glm::vec3 v3_b; - glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; - glm::vec4 v4_b; - glm::ivec2 iv2_a{10, 24}; - glm::ivec2 iv2_b; - float f_a = 1.2345f; - float f_b; - - s << (qint8)1; - s << (qint16)0xaabb; - s << (qint32)0xccddeeff; - s << v3_a; - s << v4_a; - s << iv2_a; - s << f_a; - - qint8 i8; - qint16 i16; - qint32 i32; - - DataDeserializer d(s); - - d >> i8; - d >> i16; - d >> i32; - d >> v3_b; - d >> v4_b; - d >> iv2_b; - d >> f_b; - - qDebug() << d; - - QCOMPARE(i8, (qint8)1); - QCOMPARE(i16, (qint16)0xaabb); - QCOMPARE(i32, (qint32)0xccddeeff); - QCOMPARE(v3_a, v3_b); - QCOMPARE(v4_a, v4_b); - QCOMPARE(iv2_a, iv2_b); - QCOMPARE(f_a, f_b); -} - -void SerializerTests::testReadPastEnd() { - DataSerializer s; - qint8 i8; - qint16 i16; - s << (qint8)1; - - DataDeserializer d(s); - d >> i8; - QCOMPARE(d.pos(), 1); - - d.rewind(); - d >> i16; - QCOMPARE(d.pos(), 0); -} - -void SerializerTests::testWritePastEnd() { - qint8 i8 = 255; - qint16 i16 = 65535; - - - char buf[16]; - - - // 1 byte buffer, we can write 1 byte - memset(buf, 0, sizeof(buf)); - DataSerializer s1(buf, 1); - s1 << i8; - QCOMPARE(s1.pos(), 1); - QCOMPARE(s1.isOverflow(), false); - QCOMPARE(buf[0], i8); - - // 1 byte buffer, we can't write 2 bytes - memset(buf, 0, sizeof(buf)); - DataSerializer s2(buf, 1); - s2 << i16; - QCOMPARE(s2.pos(), 0); - QCOMPARE(s2.isOverflow(), true); - QCOMPARE(buf[0], 0); // We didn't write - QCOMPARE(buf[1], 0); -} - - - - -void SerializerTests::benchmarkEncodingDynamicAlloc() { - QBENCHMARK { - DataSerializer s; - glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; - glm::vec3 v3_b; - glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; - glm::vec4 v4_b; - glm::ivec2 iv2_a{10, 24}; - glm::ivec2 iv2_b; - - s << (qint8)1; - s << (qint16)0xaabb; - s << (qint32)0xccddeeff; - s << v3_a; - s << v4_a; - s << iv2_a; - } -} - -void SerializerTests::benchmarkEncodingStaticAlloc() { - char buf[1024]; - - QBENCHMARK { - DataSerializer s(buf, sizeof(buf)); - glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; - glm::vec3 v3_b; - glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; - glm::vec4 v4_b; - glm::ivec2 iv2_a{10, 24}; - glm::ivec2 iv2_b; - - s << (qint8)1; - s << (qint16)0xaabb; - s << (qint32)0xccddeeff; - s << v3_a; - s << v4_a; - s << iv2_a; - } -} - - -void SerializerTests::benchmarkDecoding() { - DataSerializer s; - qint8 q8 = 1; - qint16 q16 = 0xaabb; - qint32 q32 = 0xccddeeff; - - glm::vec3 v3_a{1.f, 3.1415f, 2.71828f}; - glm::vec3 v3_b; - glm::vec4 v4_a{3.1415f, 2.71828f, 1.4142f, 1.6180f}; - glm::vec4 v4_b; - glm::ivec2 iv2_a{10, 24}; - glm::ivec2 iv2_b; - - s << q8; - s << q16; - s << q32; - s << v3_a; - s << v4_a; - s << iv2_a; - - - QBENCHMARK { - DataDeserializer d(s); - d >> q8; - d >> q16; - d >> q32; - d >> v3_a; - d >> v4_a; - d >> iv2_a; - } -} - - -void SerializerTests::cleanupTestCase() { -} - diff --git a/tests/shared/src/SerializerTests.h b/tests/shared/src/SerializerTests.h deleted file mode 100644 index 55da84c41a..0000000000 --- a/tests/shared/src/SerializerTests.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// ResourceTests.h -// -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef overte_SerializerTests_h -#define overte_SerializerTests_h - -#include -#include - -class SerializerTests : public QObject { - Q_OBJECT -private slots: - void initTestCase(); - void testCreate(); - void testAdd(); - void testAddAndRead(); - void testReadPastEnd(); - void testWritePastEnd(); - void benchmarkEncodingDynamicAlloc(); - void benchmarkEncodingStaticAlloc(); - void benchmarkDecoding(); - void cleanupTestCase(); -private: - -}; - -#endif // overte_SerializerTests_h diff --git a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 index eb26d1b426..12a56ced03 100644 --- a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 +++ b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-20.04 @@ -1,8 +1,8 @@ -# Copyright 2022-2024 Overte e.V. +# Copyright 2022-2023 Overte e.V. # SPDX-License-Identifier: Apache-2.0 # Docker file for building Overte -# Example build: docker build -t overte/overte-full-build:0.1.2-ubuntu-20.04 -f Dockerfile_build_ubuntu-20.04 . +# Example build: docker build -t overte/overte-full-build:0.1.1-ubuntu-20.04 -f Dockerfile_build_ubuntu-20.04 . FROM ubuntu:20.04 LABEL maintainer="Julian Groß (julian.gro@overte.org)" LABEL description="Development image for full Overte builds" @@ -18,8 +18,6 @@ RUN apt-get update && apt-get -y install tzdata RUN apt-get -y install curl ninja-build git cmake g++ libssl-dev python3-distutils python3-distro mesa-common-dev libgl1-mesa-dev libsystemd-dev # Install server-console build dependencies RUN apt-get -y install npm -# Install Interface dependencies -RUN apt-get -y install pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 vulkan-validationlayers libvulkan-dev libvulkan1 # Install tools for package creation RUN apt-get -y install sudo chrpath binutils dh-make diff --git a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 index a4da419e87..a80207471e 100644 --- a/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 +++ b/tools/ci-scripts/linux-ci/Dockerfile_build_ubuntu-22.04 @@ -1,8 +1,8 @@ -# Copyright 2022-2024 Overte e.V. +# Copyright 2022-2023 Overte e.V. # SPDX-License-Identifier: Apache-2.0 # Docker file for building Overte -# Example build: docker build -t overte/overte-full-build:0.1.2-ubuntu-22.04 -f Dockerfile_build_ubuntu-22.04 . +# Example build: docker build -t overte/overte-full-build:0.1.1-ubuntu-22.04 -f Dockerfile_build_ubuntu-22.04 . FROM ubuntu:22.04 LABEL maintainer="Julian Groß (julian.gro@overte.org)" LABEL description="Development image for full Overte builds" @@ -19,7 +19,7 @@ RUN apt-get -y install curl ninja-build git cmake g++ libssl-dev libqt5websocket # Install Overte tools build dependencies RUN apt-get -y install libqt5webchannel5-dev qtwebengine5-dev libqt5xmlpatterns5-dev # Install Overte Interface build dependencies -RUN apt-get -y install libqt5svg5-dev qttools5-dev vulkan-validationlayers libvulkan-dev libvulkan1 libqt5x11extras5-dev qtbase5-private-dev +RUN apt-get -y install libqt5svg5-dev qttools5-dev # Install server-console build dependencies RUN apt-get -y install npm diff --git a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 new file mode 100644 index 0000000000..ef5ef24819 --- /dev/null +++ b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39 @@ -0,0 +1,20 @@ +# Copyright 2022-2024 Overte e.V. +# SPDX-License-Identifier: Apache-2.0 + +# Docker file for building Overte Server +# Example build: docker build -t overte/overte-server-build:0.1.4-fedora-39 -f Dockerfile_build_fedora-39 . +FROM fedora:39 +LABEL maintainer="Julian Groß (julian.gro@overte.org)" +LABEL description="Development image for Overte Domain server and assignment clients." + +# Install Overte domain-server and assignment-client build dependencies +RUN dnf -y install curl ninja-build git cmake gcc gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel python3.11 + +# Install additional build tools +RUN dnf -y install zip unzip + +# Install tools for package creation +RUN dnf -y install chrpath rpmdevtools + +# Install tools needed for our Github Actions Workflow +Run dnf -y install python3-boto3 python3-pygithub diff --git a/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 b/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 index 5639d662b6..8c7e32f360 100644 --- a/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 +++ b/tools/qt-builder/Dockerfile_Ubuntu_20.04_Qt5 @@ -4,11 +4,11 @@ # - Check which commit you are building https://invent.kde.org/qt/qt/qt5/-/tree/kde/5.15 # - Adjust this file to include the commit hash you are building, the date, the number of threads you want to use (-j10), the platform, and the Qt and QtWebEngine versions. # Keep in mind that building Qt requires a lot of memory. You should have over 1.2GiB of system memory available per thread. -# - Run the build process with something like `PROGRESS_NO_TRUNC=1 DOCKER_BUILDKIT=1 BUILDKIT_STEP_LOG_MAX_SIZE=-1 docker build --progress plain -t overte-qt5:5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3 -f Dockerfile_Ubuntu_20.04_Qt5 .` +# - Run the build process with something like `PROGRESS_NO_TRUNC=1 DOCKER_BUILDKIT=1 BUILDKIT_STEP_LOG_MAX_SIZE=-1 docker build --progress plain -t overte-qt5:5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371 -f Dockerfile_Ubuntu_20.04_Qt5 .` # Buildkit is used to cache intermittent steps in case you need to modify something afterwards. # - Once the build has completed, create a container from the image and export the created Qt package. -# `docker create --name extract overte-qt5:5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3` -# `docker cp extract:qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz /path/on/host` +# `docker create --name extract overte-qt5:5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371` +# `docker cp extract:qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz /path/on/host` # `docker rm extract` FROM ubuntu:20.04 @@ -47,7 +47,7 @@ RUN apt-get -y install git python gperf flex bison pkg-config mesa-utils libgl1- RUN mkdir qt5-install && mkdir qt5-build WORKDIR qt5-build -RUN ../qt5/configure -force-debug-info -release -opensource -confirm-license -platform linux-g++ -recheck-all -nomake tests -nomake examples -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtcharts -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -skip qtlottie -skip qtquick3d -skip qtpim -skip qtdocgallery -no-warnings-are-errors -no-pch -no-icu -prefix ../qt5-install +RUN ../qt5/configure -force-debug-info -release -opensource -confirm-license -platform linux-g++ -recheck-all -nomake tests -nomake examples -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtcharts -skip qtx11extras -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -skip qtlottie -skip qtquick3d -skip qtpim -skip qtdocgallery -no-warnings-are-errors -no-pch -no-icu -prefix ../qt5-install RUN NINJAFLAGS='-j6' make -j6 @@ -58,12 +58,12 @@ WORKDIR ../../qt5-install RUN find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \; # Overwrite QtWebengine version to work around version conflicts -RUN find . -name \Qt5WebEngine*Config.cmake -exec sed -i '' -e 's/5\.15\.19/5\.15\.16/g' {} \; -RUN cp lib/libQt5WebEngine.so.5.15.19 lib/libQt5WebEngine.so.5.15.16 -RUN cp lib/libQt5WebEngineCore.so.5.15.19 lib/libQt5WebEngineCore.so.5.15.16 -RUN cp lib/libQt5WebEngineWidgets.so.5.15.19 lib/libQt5WebEngineWidgets.so.5.15.16 -RUN cp lib/libQt5Pdf.so.5.15.19 lib/libQt5Pdf.so.5.15.16 -RUN cp lib/libQt5PdfWidgets.so.5.15.19 lib/libQt5PdfWidgets.so.5.15.16 +RUN find . -name \Qt5WebEngine*Config.cmake -exec sed -i '' -e 's/5\.15\.17/5\.15\.14/g' {} \; +RUN cp lib/libQt5WebEngine.so.5.15.17 lib/libQt5WebEngine.so.5.15.14 +RUN cp lib/libQt5WebEngineCore.so.5.15.17 lib/libQt5WebEngineCore.so.5.15.14 +RUN cp lib/libQt5WebEngineWidgets.so.5.15.17 lib/libQt5WebEngineWidgets.so.5.15.14 +RUN cp lib/libQt5Pdf.so.5.15.17 lib/libQt5Pdf.so.5.15.14 +RUN cp lib/libQt5PdfWidgets.so.5.15.17 lib/libQt5PdfWidgets.so.5.15.14 COPY ./qt.conf ./bin/ @@ -71,4 +71,4 @@ COPY ./qt.conf ./bin/ RUN cp ../qt5-build/config.summary ./ WORKDIR .. -RUN XZ_OPT='-T0' tar -Jcvf qt5-install-5.15.16-2024.12.14-kde_32be154325bfba3ad2ba8bf75dad702f3588e8d3-ubuntu-20.04-amd64.tar.xz qt5-install +RUN XZ_OPT='-T0' tar -Jcvf qt5-install-5.15.14-2024.06.17-kde_570f5b2105df1ea052bec0d6dbf8a00137274371-ubuntu-20.04-amd64.tar.xz qt5-install