From b33a3eb5ae3b3913f22693f2f6d3e3a0bcc5e235 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Fri, 24 Nov 2023 00:19:50 -0500 Subject: [PATCH 01/49] Scaffolding to add a planets page to the app. Currently using the NavView .ui file for the moment. --- starfab/app.py | 24 ++++++++-- starfab/gui/widgets/pages/__init__.py | 1 + starfab/gui/widgets/pages/page_PlanetView.py | 46 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 starfab/gui/widgets/pages/page_PlanetView.py diff --git a/starfab/app.py b/starfab/app.py index cddbe78..afe19a2 100644 --- a/starfab/app.py +++ b/starfab/app.py @@ -121,6 +121,7 @@ class StarFab(QMainWindow): self.data_page_btn = None self.content_page_btn = None self.nav_page_btn = None + self.planets_page_btn = None # ------------- actions ----------------- # keyboard shortcuts, for QKeySequence see https://doc.qt.io/qtforpython-5/PySide2/QtGui/QKeySequence.html @@ -142,6 +143,8 @@ class StarFab(QMainWindow): self.actionContentView.triggered.connect(self._handle_workspace_action) self.actionNavView.setIcon(qta.icon("mdi6.map-marker-path")) self.actionNavView.triggered.connect(self._handle_workspace_action) + self.actionPlanetView.setIcon(qta.icon("ph.planet")) + self.actionPlanetView.triggered.connect(self._handle_workspace_action) self._open_settings = self.actionSettings self.actionSettings.setIcon(qta.icon("msc.settings-gear")) self._show_console = self.actionConsole @@ -184,6 +187,9 @@ class StarFab(QMainWindow): self.page_NavView = NavView(self) self.stackedWidgetWorkspace.addWidget(self.page_NavView) + self.page_PlanetView = PlanetView(self) + self.stackedWidgetWorkspace.addWidget(self.page_PlanetView) + self.dock_widgets = {} self.setup_dock_widgets() self._progress_tasks = {} @@ -243,6 +249,11 @@ class StarFab(QMainWindow): self.nav_page_btn.released.connect(self.handle_workspace) self.workspace_panel.add_ribbon_widget(self.nav_page_btn) + self.planets_page_btn = RibbonButton(self, self.actionPlanetView, True) + self.planets_page_btn.setAutoExclusive(True) + self.planets_page_btn.released.connect(self.handle_workspace) + self.workspace_panel.add_ribbon_widget(self.planets_page_btn) + self.options_panel = self.home_tab.add_ribbon_pane("Options") self.options_panel.add_ribbon_widget( RibbonButton(self, self._open_settings, True) @@ -378,7 +389,8 @@ Contributors: self.hide() self.close.emit() finally: - sys.exit(0) + print("closing!") + #sys.exit(0) def _refresh_recent(self, recent=None): if recent is None: @@ -749,7 +761,8 @@ Contributors: def handle_workspace(self, view=None, *args, **kwargs): self.workspace_btns = [self.data_page_btn, self.content_page_btn, - self.nav_page_btn] + self.nav_page_btn, + self.planets_page_btn] def _clear_checked(self): for btn in self.workspace_btns: @@ -763,6 +776,8 @@ Contributors: else: self.stackedWidgetWorkspace.setCurrentWidget(self.page_OpenView) + logger.info(f"Switching to workspace: {view}") + if self.sc is None: _clear_checked(self) elif view == "data": @@ -773,4 +788,7 @@ Contributors: self.stackedWidgetWorkspace.setCurrentWidget(self.page_ContentView) elif view == "nav": self.nav_page_btn.setChecked(True) - self.stackedWidgetWorkspace.setCurrentWidget(self.page_NavView) \ No newline at end of file + self.stackedWidgetWorkspace.setCurrentWidget(self.page_NavView) + elif view == "planets": + self.planets_page_btn.setChecked(True) + self.stackedWidgetWorkspace.setCurrentWidget(self.page_PlanetView) \ No newline at end of file diff --git a/starfab/gui/widgets/pages/__init__.py b/starfab/gui/widgets/pages/__init__.py index 3e20f04..87b398d 100644 --- a/starfab/gui/widgets/pages/__init__.py +++ b/starfab/gui/widgets/pages/__init__.py @@ -1,3 +1,4 @@ from .page_DataView import DataView from .page_NavView import NavView +from .page_PlanetView import PlanetView from .content import ContentView \ No newline at end of file diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py new file mode 100644 index 0000000..72797ff --- /dev/null +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -0,0 +1,46 @@ +from qtpy import uic +from scdatatools.sc.object_container import ObjectContainer +from scdatatools.sc.object_container.plotter import ObjectContainerPlotter +from starfab.gui import qtw +from starfab.gui.widgets.preview3d import Preview3D +from starfab.log import getLogger +from starfab.resources import RES_PATH +from pathlib import Path + + +logger = getLogger(__name__) + + +class PlanetView(qtw.QWidget): + def __init__(self, starfab): + super().__init__(parent=None) + self.starfab = starfab + uic.loadUi(str(RES_PATH / "ui" / "NavView.ui"), self) # Load the ui into self + + self.starfab.sc_manager.datacore_model.loaded.connect( + self._handle_datacore_loaded + ) + self.starfab.sc_manager.datacore_model.unloading.connect( + self._handle_datacore_unloading + ) + + self.starmap = None + + def _handle_datacore_unloading(self): + if self.starmap is not None: + self.preview_widget_layout.takeAt(0) + del self.starmap + self.starmap = None + + def _handle_datacore_loaded(self): + logger.info("DataCore loaded") + megamap_pu = self.starfab.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] + pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value + try: + path = Path(pu_socpak).as_posix().lower() + logger.info(path) + pu_oc = self.starfab.sc.oc_manager.load_socpak(pu_socpak) + except Exception as ex: + logger.exception(ex) + return + -- GitLab From 3ede6078a4032a09f70cf3ba16c6bbea04fd4a33 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Fri, 24 Nov 2023 01:43:35 -0500 Subject: [PATCH 02/49] Add new .ui file for planets page, and add simple enumeration of planets. --- starfab/gui/widgets/pages/page_PlanetView.py | 95 ++++++++++- starfab/resources/ui/PlanetView.ui | 166 +++++++++++++++++++ 2 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 starfab/resources/ui/PlanetView.ui diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 72797ff..cd22544 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,9 +1,15 @@ +from PySide6.QtCore import Qt +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import QComboBox, QPushButton from qtpy import uic -from scdatatools.sc.object_container import ObjectContainer +from scdatatools import StarCitizen +from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk +from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance from scdatatools.sc.object_container.plotter import ObjectContainerPlotter from starfab.gui import qtw from starfab.gui.widgets.preview3d import Preview3D from starfab.log import getLogger +from starfab.models.datacore import DCBModel from starfab.resources import RES_PATH from pathlib import Path @@ -15,7 +21,12 @@ class PlanetView(qtw.QWidget): def __init__(self, starfab): super().__init__(parent=None) self.starfab = starfab - uic.loadUi(str(RES_PATH / "ui" / "NavView.ui"), self) # Load the ui into self + + self.renderButton: QPushButton = None + self.planetComboBox: QComboBox = None + self.renderResolutionComboBox: QComboBox = None + self.coordinateSystemComboBox: QComboBox = None + uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self self.starfab.sc_manager.datacore_model.loaded.connect( self._handle_datacore_loaded @@ -26,9 +37,44 @@ class PlanetView(qtw.QWidget): self.starmap = None + self.renderResolutionComboBox.setModel(self.create_model([ + ("1px per tile", 1), + ("2px per tile", 2), + ("4px per tile", 4), + ("8px per tile", 8), + ("16px per tile", 16), + ("32px per tile", 32), + ("64px per tile", 64), + ("128px per tile", 128) + ])) + + self.coordinateSystemComboBox.setModel(self.create_model([ + ("NASA Format (0/360deg) - Community Standard", "NASA"), + ("Earth Format (-180/180deg) Shifted", "EarthShifted"), + ("Earth Format (-180/180deg) Unshifted", "EarthUnShifted") + ])) + + self.renderButton.clicked.connect(self.clicky) + + @staticmethod + def create_model(records): + # Create a model + model = QStandardItemModel() + + # Add items to the model with visible name and hidden ID + for item_text, item_id in records: + item = QStandardItem(item_text) + # Set the hidden ID in the user role of the item + item.setData(item_id, role=Qt.UserRole) + model.appendRow(item) + + return model + + def clicky(self): + print("Clicky!") + def _handle_datacore_unloading(self): if self.starmap is not None: - self.preview_widget_layout.takeAt(0) del self.starmap self.starmap = None @@ -37,10 +83,49 @@ class PlanetView(qtw.QWidget): megamap_pu = self.starfab.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value try: - path = Path(pu_socpak).as_posix().lower() - logger.info(path) pu_oc = self.starfab.sc.oc_manager.load_socpak(pu_socpak) + bodies: list[ObjectContainerInstance] = self._search_for_bodies(pu_oc) + + self.planetComboBox.setModel(self.create_model([ + (b.entity_name, b) for b in bodies + ])) + except Exception as ex: logger.exception(ex) return + @staticmethod + def _search_for_bodies(socpak: ObjectContainer, search_depth_after_first_body: int = 1): + results = [] + + def _inner_search(entry: ObjectContainerInstance, planet_depth: int, max_depth: int): + if planet_depth > max_depth: + return + for subchild in entry.children.values(): + if PlanetView.has_planet_data(subchild): + results.append(subchild) + _inner_search(subchild, planet_depth + 1, max_depth) + else: + # Only increment the next depth when we've hit a planet already + new_depth = planet_depth if planet_depth == 0 else planet_depth + 1 + _inner_search(subchild, new_depth, max_depth) + + child: ObjectContainerInstance + for child in socpak.children.values(): + _inner_search(child, 0, search_depth_after_first_body) + + return results + + @staticmethod + def has_planet_data(oc: ObjectContainerInstance): + if (not oc.container) or (not oc.container.has_additional): + return False + if oc.container.additional_data: + chunkfile: ChunkFile + for chunkfile in oc.container.additional_data: + chunk: Chunk + for c_id, chunk in chunkfile.chunks.items(): + if isinstance(chunk, JSONChunk): + return True + return False + diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui new file mode 100644 index 0000000..f35efe2 --- /dev/null +++ b/starfab/resources/ui/PlanetView.ui @@ -0,0 +1,166 @@ + + + page_ContentView + + + + 0 + 0 + 1860 + 918 + + + + Form + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 4 + 0 + + + + Qt::Horizontal + + + + true + + + + 0 + 0 + + + + + 400 + 0 + + + + + 500 + 16777215 + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QLayout::SetDefaultConstraint + + + + + + + Planet + + + + + + + + + + GPU Renderer + + + + + + + + + + Render Resolution + + + + + + + + + + Coordinate System + + + + + + + + + + + + PushButton + + + + + + + + + + + 3 + 0 + + + + + 0 + + + 0 + + + 0 + + + 3 + + + + + + + + + + -- GitLab From 25d97ba48c4a76fc7eaf76093b6577f7b8714442 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sat, 25 Nov 2023 20:14:10 -0500 Subject: [PATCH 03/49] Good proof-of-concept build that can render the super basic surface maps of planets on the GPU. --- starfab/gui/widgets/pages/page_PlanetView.py | 63 +-- starfab/models/planet.py | 402 +++++++++++++++++++ starfab/resources/ui/PlanetView.ui | 54 ++- 3 files changed, 488 insertions(+), 31 deletions(-) create mode 100644 starfab/models/planet.py diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index cd22544..e005607 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,15 +1,17 @@ from PySide6.QtCore import Qt from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QComboBox, QPushButton +from PySide6.QtWidgets import QComboBox, QPushButton, QPlainTextEdit from qtpy import uic from scdatatools import StarCitizen from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance from scdatatools.sc.object_container.plotter import ObjectContainerPlotter + from starfab.gui import qtw from starfab.gui.widgets.preview3d import Preview3D from starfab.log import getLogger from starfab.models.datacore import DCBModel +from starfab.models.planet import Planet, RenderSettings, EcoSystem from starfab.resources import RES_PATH from pathlib import Path @@ -18,23 +20,16 @@ logger = getLogger(__name__) class PlanetView(qtw.QWidget): - def __init__(self, starfab): + def __init__(self, sc): super().__init__(parent=None) - self.starfab = starfab self.renderButton: QPushButton = None self.planetComboBox: QComboBox = None self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None + self.hlslTextBox: QPlainTextEdit = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self - self.starfab.sc_manager.datacore_model.loaded.connect( - self._handle_datacore_loaded - ) - self.starfab.sc_manager.datacore_model.unloading.connect( - self._handle_datacore_unloading - ) - self.starmap = None self.renderResolutionComboBox.setModel(self.create_model([ @@ -54,8 +49,22 @@ class PlanetView(qtw.QWidget): ("Earth Format (-180/180deg) Unshifted", "EarthUnShifted") ])) + if isinstance(sc, StarCitizen): + self.sc = sc + self._handle_datacore_loaded() + else: + self.sc = sc.sc_manager + self.sc.datacore_model.loaded.connect(self._hack_before_load) + self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) + self.renderButton.clicked.connect(self.clicky) + def _hack_before_load(self): + # Hacky method to support faster dev testing and launching directly in-app + self.sc = self.sc.sc + EcoSystem.read_eco_headers(self.sc) + self._handle_datacore_loaded() + @staticmethod def create_model(records): # Create a model @@ -71,7 +80,12 @@ class PlanetView(qtw.QWidget): return model def clicky(self): - print("Clicky!") + selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) + shader = self.hlslTextBox.toPlainText() + print(selected_obj) + selected_obj.load_data() + print("Done loading planet data") + selected_obj.render(RenderSettings(True, 1, "NASA", shader)) def _handle_datacore_unloading(self): if self.starmap is not None: @@ -80,14 +94,14 @@ class PlanetView(qtw.QWidget): def _handle_datacore_loaded(self): logger.info("DataCore loaded") - megamap_pu = self.starfab.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] + megamap_pu = self.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value try: - pu_oc = self.starfab.sc.oc_manager.load_socpak(pu_socpak) - bodies: list[ObjectContainerInstance] = self._search_for_bodies(pu_oc) + pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) + bodies: list[Planet] = self._search_for_bodies(pu_oc) self.planetComboBox.setModel(self.create_model([ - (b.entity_name, b) for b in bodies + (b.oc.entity_name, b) for b in bodies ])) except Exception as ex: @@ -96,14 +110,15 @@ class PlanetView(qtw.QWidget): @staticmethod def _search_for_bodies(socpak: ObjectContainer, search_depth_after_first_body: int = 1): - results = [] + results: list[Planet] = [] def _inner_search(entry: ObjectContainerInstance, planet_depth: int, max_depth: int): if planet_depth > max_depth: return for subchild in entry.children.values(): - if PlanetView.has_planet_data(subchild): - results.append(subchild) + planet = Planet.try_create(subchild) + if planet: + results.append(planet) _inner_search(subchild, planet_depth + 1, max_depth) else: # Only increment the next depth when we've hit a planet already @@ -116,16 +131,4 @@ class PlanetView(qtw.QWidget): return results - @staticmethod - def has_planet_data(oc: ObjectContainerInstance): - if (not oc.container) or (not oc.container.has_additional): - return False - if oc.container.additional_data: - chunkfile: ChunkFile - for chunkfile in oc.container.additional_data: - chunk: Chunk - for c_id, chunk in chunkfile.chunks.items(): - if isinstance(chunk, JSONChunk): - return True - return False diff --git a/starfab/models/planet.py b/starfab/models/planet.py new file mode 100644 index 0000000..fbc995e --- /dev/null +++ b/starfab/models/planet.py @@ -0,0 +1,402 @@ +import io +import json +import math +import random +import struct +from os import listdir +from os.path import join, isfile +from pathlib import Path +from struct import unpack +from typing import Callable, Union + +import compushady +from PIL import Image +from compushady import Texture2D, Buffer, HEAP_UPLOAD, Compute, HEAP_READBACK, Resource, HEAP_DEFAULT +from compushady.backends.vulkan import Device +from compushady.formats import R8G8B8A8_UINT, R16_UINT, R8_UINT +from compushady.shaders import hlsl +from scdatatools import StarCitizen +from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk +from scdatatools.engine.textures import unsplit_dds +from scdatatools.p4k import P4KInfo +from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance + +from starfab.utils import image_converter + + +class RenderSettings: + def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str): + self.gpu = gpu + self.resolution = resolution + self.coordinate_mode = coordinate_mode + self.hlsl = hlsl + + +class LocalClimateData: + def __init__(self, x: float, y: float): + self.x: float = x + self.y: float = y + + self.temperature: int = 0 + self.humidity: int = 0 + self.eco_id: int = 0 + + self.elevation: float = 0 + self.random_offset: float = 0 + + self.normal_x: float = 0 + self.normal_y: float = 0 + self.normal_z: float = 0 + + self.surfaceTextureMap: int = 0 + self.oprBlendIndex: int = 0 + + @staticmethod + def create_packed_bytes(climate_records: list[list]): + pack_string = "2f2I" + pack_size = struct.calcsize(pack_string) + pack_index = 0 + pack_data = bytearray(pack_size * len(climate_records) * len(climate_records[0])) + # TODO: There has to be a better way to do this :^) + print("Packing") + for y in range(len(climate_records[0])): + print(f"{y}/{len(climate_records[0])}") + for x in range(len(climate_records)): + clim: LocalClimateData = climate_records[x][y] + struct.pack_into(pack_string, pack_data, pack_index, + clim.elevation, clim.random_offset, + clim.surfaceTextureMap, clim.oprBlendIndex) + pack_index += pack_size + print("Done Packing") + return pack_data + + +class Brush: + def __init__(self, record): + self.record = record + self.bedrockGradientColorA = self.record["bedrockBrush"]["colorGradient"]["gradientColorA"] + self.bedrockGradientColorB = self.record["bedrockBrush"]["colorGradient"]["gradientColorB"] + self.surfaceGradientColorA = self.record["surfaceBrush"]["colorGradient"]["gradientColorA"] + self.surfaceGradientColorB = self.record["surfaceBrush"]["colorGradient"]["gradientColorB"] + self.tMin = self.record["tMin"] + self.tMax = self.record["tMax"] + self.hMin = self.record["hMin"] + self.hMax = self.record["hMax"] + + +class LUTData: + def __init__(self): + self.bedrockGloss: list = [] + self.surfaceGloss: list = [] + self.bedrockColor: list = [] + self.surfaceColor: list = [] + self.brush_id: int = 0 + self.ground_texture_id: int = 0 + self.object_preset_id: int = 0 + self.bd_gradient_val_bedrock: int = 0 + self.bd_gradient_val_surface: int = 0 + self.bd_value_offset_bedrock: float = 0 + self.bd_value_offset_surface: float = 0 + self.bd_saturation_offset_bedrock: float = 0 + self.bd_saturation_offset_surface: float = 0 + self.bd_orp_blend_index: int = 0 + self.bd_texture_layer_index: int = 0 + self.brush_obj: Brush = None + + +class EcoSystem: + _cache = {} + _tex_root = Path("Data/Textures/planets/terrain") + _sc: Union[None, StarCitizen] = None + + @staticmethod + def find_in_cache_(guid: str): + return EcoSystem._cache[guid] if guid in EcoSystem._cache else None + + @staticmethod + def read_eco_headers(sc: StarCitizen): + if len(EcoSystem._cache) != 0: + return EcoSystem._cache + EcoSystem._sc = sc + p4k_results = EcoSystem._sc.p4k.search(".eco", mode="endswith") + for result in p4k_results: + eco = EcoSystem(json.loads(result.open().read())) + EcoSystem._cache[eco.id] = eco + return EcoSystem._cache + + def __init__(self, eco_data: dict): + self.id = eco_data["GUID"] + self.name = eco_data["name"] + self.offset = eco_data["fOffsetH"] + self.tex_path: str = eco_data["Textures"]["ecoSystemAlbedoTexture"].replace(".tif", ".dds") + self.norm_path: str = eco_data["Textures"]["ecoSystemNormalTexture"].replace(".tif", ".dds") + self.elevation_path: str = eco_data["Textures"]["elevationTexture"] + + self.climate_data: list[list[LocalClimateData]] = None + self.climate_texture: Image = None + self.normal_texture: Image = None + self.elevation_bytes: bytearray = None + self.elevation_size: int = 0 + + def read_full_data(self): + if self.climate_data: + return + + def _read_texture(subpath: str) -> bytes: + texture_path = (EcoSystem._tex_root / subpath).as_posix().lower() + + p4k_dds_files = EcoSystem._sc.p4k.search(texture_path, mode="startswith") + dds_files = {record.orig_filename: record + for record + in p4k_dds_files + if not record.orig_filename.endswith("a")} + res: bytes = unsplit_dds(dds_files) + return image_converter.convert_buffer(res, "dds", "png") + + climate_texture = Image.open(io.BytesIO(_read_texture(self.tex_path))) + normal_texture = Image.open(io.BytesIO(_read_texture(self.norm_path))) + + self.climate_texture = climate_texture + self.normal_texture = normal_texture + + # Addressed as [x][y] + self.climate_data = [[LocalClimateData(x, y) + for y in range(climate_texture.height)] + for x in range(climate_texture.width)] + + elevation_path = (EcoSystem._tex_root / self.elevation_path).as_posix().lower() + elevation_info: P4KInfo = EcoSystem._sc.p4k.NameToInfoLower[elevation_path] + with elevation_info.open() as o: + self.elevation_bytes = bytearray(o.read()) + self.elevation_size = int(math.sqrt(len(self.elevation_bytes) / 2)) + + print(f"Textures loaded for {self.name}") + + +class Planet: + def __init__(self, oc: ObjectContainerInstance, data: JSONChunk): + self.oc: ObjectContainerInstance = oc + self.data: JSONChunk = data + + self.planet_data = None + self.tile_count = None + self.radius_m = None + self.humidity_influence = None + self.temperature_influence = None + + self.climate_texture: Image = None + self.offset_data: bytearray = None + self.heightmap_data: bytearray = None + + self.brushes: list[Brush] = None + self.ecosystems: list[EcoSystem] = None + + self.lut: list[list[LUTData]] = None + + self.gpu_resources = {} + self.gpu_computer: Compute = None + + def load_data(self) -> object: + if self.planet_data: + return self.planet_data + + self.planet_data = self.data.dict() + + self.tile_count = self.planet_data["data"]["globalSplatWidth"] + self.radius_m = self.planet_data["data"]["General"]["tSphere"]["fPlanetTerrainRadius"] + self.humidity_influence = self.planet_data["data"]["General"]["textureLayers"]["localHumidityInfluence"] + self.temperature_influence = self.planet_data["data"]["General"]["textureLayers"]["localTemperatureInfluence"] + + self.brushes = [Brush(b) for b in self.planet_data["data"]["General"]["brushes"]] + + ecosystem_ids = self.planet_data["data"]["General"]["uniqueEcoSystemsGUIDs"] + self.ecosystems = [EcoSystem.find_in_cache_(e) + for e in ecosystem_ids[0:1] + if e != "00000000-0000-0000-0000-000000000000"] + + eco: EcoSystem + for eco in self.ecosystems: + eco.read_full_data() + + # R = Temp, G = Humidity, B = Biome ID, A = Unused + splat_raw = self.planet_data["data"]["splatMap"] + self.climate_texture = Image.frombuffer('RGBA', + (self.tile_count, self.tile_count), + bytearray(splat_raw)) + + offsets_raw = self.planet_data["data"]["randomOffset"] + self.offset_data = bytearray(offsets_raw) + + hm_path = self.planet_data["data"]["General"]["tSphere"]["sHMWorld"] + hm_path_posix = ("data" / Path(hm_path)).as_posix().lower() + hm_data: ObjectContainer = self.oc.container + hm_info: P4KInfo = hm_data.socpak.p4k.NameToInfoLower[hm_path_posix] + hm_raw = bytearray(hm_data.socpak.p4k.open(hm_info).read()) + + self.heightmap_data = hm_raw + + self._build_lut() + + print("Creating GPU Resources...") + self._construct_gpu_resources() + print("Created!") + + def _build_lut(self): + # Addressed as [x][y] + self.lut = [[LUTData() + for y in range(self.tile_count)] + for x in range(self.tile_count)] + + def _clamp(val, _min, _max): + return _min if val < _min else (_max if val > _max else val) + + def _lerp_int(_min, _max, val): + return int(((_max - _min) * val) + _min) + + def _lerp_color(a, b, val): + return [ + _clamp(_lerp_int(a[0], b[0], val), 0, 255), + _clamp(_lerp_int(a[1], b[1], val), 0, 255), + _clamp(_lerp_int(a[2], b[2], val), 0, 255), + _clamp(_lerp_int(a[3], b[3], val), 0, 255) + ] + + for y in range(128): + for x in range(128): + lut = self.lut[x][y] + lut.ground_texture_id = self.planet_data["data"]["groundTexIDLUT"][y][x] + lut.object_preset_id = self.planet_data["data"]["objectPresetLUT"][y][x] + lut.brush_id = self.planet_data["data"]["brushIDLUT"][y][x] + lut.brush_obj = self.brushes[lut.brush_id] + + brush_data = self.planet_data["data"]["brushDataLUT"][y][x] + + lut.bd_gradient_val_bedrock = brush_data["gradientPosBedRock"] + lut.bd_gradient_val_surface = brush_data["gradientPosSurface"] + lut.bd_value_offset_bedrock = brush_data["valOffsetBedRock"] + lut.bd_value_offset_surface = brush_data["valOffsetSurface"] + lut.bd_saturation_offset_bedrock = brush_data["satOffsetBedRock"] + lut.bd_saturation_offset_surface = brush_data["satOffsetSurface"] + lut.bd_orp_blend_index = brush_data["oprBlendIndex"] + lut.bd_texture_layer_index = brush_data["texturLayerIndex"] + + lut.bedrockColor = _lerp_color(lut.brush_obj.bedrockGradientColorA, + lut.brush_obj.bedrockGradientColorB, + lut.bd_gradient_val_bedrock / 127) + + lut.surfaceColor = _lerp_color(lut.brush_obj.surfaceGradientColorA, + lut.brush_obj.surfaceGradientColorB, + lut.bd_gradient_val_surface / 127) + + def _construct_gpu_resources(self): + def _buffer_from_lut_color(fn_color: Callable[[LUTData], list[int]]) -> Resource: + texture = Texture2D(128, 128, R8G8B8A8_UINT) + buffer = Buffer(texture.size, HEAP_UPLOAD) + data = [0 for _ in range(128 * 128 * 4)] + for y in range(128): + for x in range(128): + color = fn_color(self.lut[x][y]) + index = 4 * (y * 128 + x) + data[index + 0] = color[0] + data[index + 1] = color[1] + data[index + 2] = color[2] + data[index + 3] = color[3] + # TODO: Can we write directly into the buffer as we generate? + buffer.upload(bytes(data)) + buffer.copy_to(texture) + return texture + + def _buffer_for_climate(climate: list[list[LocalClimateData]]) -> Resource: + buffer_bytes = LocalClimateData.create_packed_bytes(climate) + print(f"Allocating climate: {len(buffer_bytes)}") + gpu_buffer = Buffer(len(buffer_bytes), HEAP_DEFAULT, stride=16) + local_buffer = Buffer(len(buffer_bytes), HEAP_UPLOAD) + local_buffer.upload(buffer_bytes) + local_buffer.copy_to(gpu_buffer) + return gpu_buffer + + def _buffer_for_image(image: Image) -> Resource: + data = image.tobytes('raw', 'RGBA') + texture = Texture2D(image.width, image.height, R8G8B8A8_UINT) + buffer = Buffer(len(data), HEAP_UPLOAD) + buffer.upload(data) + buffer.copy_to(texture) + return texture + + def _buffer_for_bytes(data: bytearray, bytes_per_pixel: int, format: int) -> Resource: + dim = int(math.sqrt(len(data) / bytes_per_pixel)) + img = Texture2D(dim, dim, format) + buffer = Buffer(len(data), HEAP_UPLOAD) + buffer.upload(data) + buffer.copy_to(img) + return img + + self.gpu_resources['bedrock'] = _buffer_from_lut_color(lambda x: x.bedrockColor) + self.gpu_resources['surface'] = _buffer_from_lut_color(lambda x: x.surfaceColor) + self.gpu_resources['planet_climate'] = _buffer_for_image(self.climate_texture) + self.gpu_resources['planet_offsets'] = _buffer_for_bytes(self.offset_data, 1, R8_UINT) + self.gpu_resources['planet_heightmap'] = _buffer_for_bytes(self.heightmap_data, 2, R16_UINT) + # self.gpu_resources['ecosystems'] = _buffer_for_climate(self.ecosystems[0].climate_data) + # self.gpu_resources['splat'] = _buffer_for_image(self.climate_texture) + + eco_size = len(self.ecosystems[0].climate_data) + destination_texture = Texture2D(eco_size * 2, eco_size, R8G8B8A8_UINT) + + self.gpu_resources['destination'] = destination_texture + self.gpu_resources['readback'] = Buffer(destination_texture.size, HEAP_READBACK) + + print(f"Dest: ({destination_texture.width}, {destination_texture.height})") + + def render(self, settings: RenderSettings): + r = self.gpu_resources + samplers = [ + r['bedrock'], r['surface'], r['planet_climate'], r['planet_offsets'], r['planet_heightmap'] + ] + + destination_texture = self.gpu_resources['destination'] + readback_buffer = self.gpu_resources['readback'] + + compute = Compute(hlsl.compile(settings.hlsl), + srv=samplers, + uav=[destination_texture]) + + compute.dispatch(destination_texture.width // 8, destination_texture.height // 8, 1) + destination_texture.copy_to(readback_buffer) + + image = Image.frombuffer('RGBA', (destination_texture.width, + destination_texture.height), readback_buffer.readback()) + image.show() + return + + def readback(self): + destination: Texture2D = self.gpu_resources['destination'] + readback: Buffer = self.gpu_resources['readback'] + destination.copy_to(readback) + + print(f"{destination.width}, {destination.height}") + + image = Image.frombuffer('RGBA', (destination.width, + destination.height), readback.readback()) + image.show() + + @staticmethod + def try_create(oc: ObjectContainerInstance): + json_chunk = Planet.find_planet_data(oc) + if json_chunk: + return Planet(oc, json_chunk) + else: + return None + + @staticmethod + def find_planet_data(oc: ObjectContainerInstance) -> [None, JSONChunk]: + if (not oc.container) or (not oc.container.has_additional): + return None + if oc.container.additional_data: + chunkfile: ChunkFile + for chunkfile in oc.container.additional_data: + chunk: Chunk + for c_id, chunk in chunkfile.chunks.items(): + if isinstance(chunk, JSONChunk): + return chunk + return None + diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index f35efe2..f07651c 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -122,6 +122,58 @@ + + + + HLSL + + + + + + + Texture2D<uint4> bedrock : register(t0); +Texture2D<uint4> surface : register(t1); + +Texture2D<uint4> planet_climate : register(t2); +Texture2D<uint> planet_offsets : register(t3); +Texture2D<min16uint> planet_heightmap : register(t4); + +//StructuredBuffer<ClimateData> planetClimate : register(t2); +//ConstantBuffer<ClimateData> ecosystemClimate : register(t3); + +RWTexture2D<uint4> destination : register(u0); + +[numthreads(8,8,1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + uint width; + uint height; + destination.GetDimensions(width, height); + + uint clim_w, clim_h; + planet_climate.GetDimensions(clim_w, clim_h); + uint2 clim_sz = uint2(clim_w, clim_h); + + float2 normalized_position = tid.xy / float2(width, height); + + uint4 local_climate = planet_climate[normalized_position * clim_sz]; + local_climate = uint4(local_climate.xy / 2, local_climate.z / 16, 0); + + uint4 surface_color = surface[local_climate.yx]; + + + uint4 read = uint4(0, 0, 0, 255); + //read.xy = planetClimate[index].climate_th; + //read.z = 0; + + read.xyz = surface_color.xyz; + + destination[tid.xy] = read; +} + + + @@ -142,7 +194,7 @@ 0 - + 0 -- GitLab From 89e9ccaeee44261309601e611c8eaf472410befa Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sat, 25 Nov 2023 23:23:06 -0500 Subject: [PATCH 04/49] Added inline image viewer. Cleaned up imports a bit more. --- starfab/gui/widgets/pages/page_PlanetView.py | 17 ++++++----- starfab/models/planet.py | 14 +++------ starfab/resources/ui/PlanetView.ui | 30 ++++++++++---------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index e005607..f064600 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,16 +1,14 @@ +from PIL import ImageQt from PySide6.QtCore import Qt -from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QComboBox, QPushButton, QPlainTextEdit +from PySide6.QtGui import QStandardItem, QStandardItemModel, QPixmap +from PySide6.QtWidgets import QComboBox, QPushButton, QPlainTextEdit, QGraphicsView, QGraphicsScene from qtpy import uic from scdatatools import StarCitizen -from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance -from scdatatools.sc.object_container.plotter import ObjectContainerPlotter from starfab.gui import qtw -from starfab.gui.widgets.preview3d import Preview3D +from starfab.gui.widgets.image_viewer import QImageViewer from starfab.log import getLogger -from starfab.models.datacore import DCBModel from starfab.models.planet import Planet, RenderSettings, EcoSystem from starfab.resources import RES_PATH from pathlib import Path @@ -28,6 +26,7 @@ class PlanetView(qtw.QWidget): self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None self.hlslTextBox: QPlainTextEdit = None + self.renderOutput: QImageViewer = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self self.starmap = None @@ -85,7 +84,11 @@ class PlanetView(qtw.QWidget): print(selected_obj) selected_obj.load_data() print("Done loading planet data") - selected_obj.render(RenderSettings(True, 1, "NASA", shader)) + + # TODO: Deal with buffer directly + img = selected_obj.render(RenderSettings(True, 1, "NASA", shader)) + qimg = ImageQt.ImageQt(img) + self.renderOutput.setImage(qimg) def _handle_datacore_unloading(self): if self.starmap is not None: diff --git a/starfab/models/planet.py b/starfab/models/planet.py index fbc995e..34fd1c3 100644 --- a/starfab/models/planet.py +++ b/starfab/models/planet.py @@ -362,22 +362,16 @@ class Planet: compute.dispatch(destination_texture.width // 8, destination_texture.height // 8, 1) destination_texture.copy_to(readback_buffer) - - image = Image.frombuffer('RGBA', (destination_texture.width, - destination_texture.height), readback_buffer.readback()) - image.show() - return + return self.readback() def readback(self): destination: Texture2D = self.gpu_resources['destination'] readback: Buffer = self.gpu_resources['readback'] destination.copy_to(readback) - print(f"{destination.width}, {destination.height}") - - image = Image.frombuffer('RGBA', (destination.width, - destination.height), readback.readback()) - image.show() + return Image.frombuffer('RGBA', + (destination.width, destination.height), + readback.readback()) @staticmethod def try_create(oc: ObjectContainerInstance): diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index f07651c..7d2288a 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -93,43 +93,33 @@ - - - GPU Renderer - - - - - - - Render Resolution - + - + Coordinate System - + - + HLSL - + Texture2D<uint4> bedrock : register(t0); @@ -207,12 +197,22 @@ void main(uint3 tid : SV_DispatchThreadID) 3 + + + + + + QImageViewer + QGraphicsView +
starfab.gui.widgets.image_viewer
+
+
-- GitLab From 325ab62f8e3ddd2c355b4419aad503b6e6fc51b0 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sat, 25 Nov 2023 23:59:29 -0500 Subject: [PATCH 05/49] Read/write shader from file instead of storing in .ui file. --- starfab/gui/widgets/pages/page_PlanetView.py | 23 +++++++- starfab/planets/shader.hlsl | 34 ++++++++++++ starfab/resources/ui/PlanetView.ui | 56 ++++++-------------- 3 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 starfab/planets/shader.hlsl diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index f064600..2778028 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,3 +1,5 @@ +import io + from PIL import ImageQt from PySide6.QtCore import Qt from PySide6.QtGui import QStandardItem, QStandardItemModel, QPixmap @@ -21,6 +23,8 @@ class PlanetView(qtw.QWidget): def __init__(self, sc): super().__init__(parent=None) + self.loadButton: QPushButton = None + self.saveButton: QPushButton = None self.renderButton: QPushButton = None self.planetComboBox: QComboBox = None self.renderResolutionComboBox: QComboBox = None @@ -56,7 +60,11 @@ class PlanetView(qtw.QWidget): self.sc.datacore_model.loaded.connect(self._hack_before_load) self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) - self.renderButton.clicked.connect(self.clicky) + self.loadButton.clicked.connect(self._load_shader) + self.saveButton.clicked.connect(self._save_shader) + self.renderButton.clicked.connect(self._do_render) + + self._load_shader() def _hack_before_load(self): # Hacky method to support faster dev testing and launching directly in-app @@ -78,7 +86,18 @@ class PlanetView(qtw.QWidget): return model - def clicky(self): + def shader_path(self) -> Path: + return Path(__file__).parent / '../../../planets/shader.hlsl' + + def _load_shader(self): + with io.open(self.shader_path(), "r") as shader: + self.hlslTextBox.setPlainText(shader.read()) + + def _save_shader(self): + with io.open(self.shader_path(), "w") as shader: + shader.write(self.hlslTextBox.toPlainText()) + + def _do_render(self): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) shader = self.hlslTextBox.toPlainText() print(selected_obj) diff --git a/starfab/planets/shader.hlsl b/starfab/planets/shader.hlsl new file mode 100644 index 0000000..24861a0 --- /dev/null +++ b/starfab/planets/shader.hlsl @@ -0,0 +1,34 @@ +Texture2D bedrock : register(t0); +Texture2D surface : register(t1); + +Texture2D planet_climate : register(t2); +Texture2D planet_offsets : register(t3); +Texture2D planet_heightmap : register(t4); + +RWTexture2D destination : register(u0); + +[numthreads(8,8,1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + uint width; + uint height; + destination.GetDimensions(width, height); + + uint clim_w, clim_h; + planet_climate.GetDimensions(clim_w, clim_h); + uint2 clim_sz = uint2(clim_w, clim_h); + + float2 normalized_position = tid.xy / float2(width, height); + + uint4 local_climate = planet_climate[normalized_position * clim_sz]; + local_climate = uint4(local_climate.xy / 2, local_climate.z / 16, 0); + + uint4 surface_color = surface[local_climate.yx]; + uint4 read = uint4(0, 0, 0, 255); + //read.xy = planetClimate[index].climate_th; + //read.z = 0; + + read.xyz = surface_color.xyz; + + destination[tid.xy] = read; +} \ No newline at end of file diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 7d2288a..aadb886 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -122,54 +122,30 @@ - Texture2D<uint4> bedrock : register(t0); -Texture2D<uint4> surface : register(t1); - -Texture2D<uint4> planet_climate : register(t2); -Texture2D<uint> planet_offsets : register(t3); -Texture2D<min16uint> planet_heightmap : register(t4); - -//StructuredBuffer<ClimateData> planetClimate : register(t2); -//ConstantBuffer<ClimateData> ecosystemClimate : register(t3); - -RWTexture2D<uint4> destination : register(u0); - -[numthreads(8,8,1)] -void main(uint3 tid : SV_DispatchThreadID) -{ - uint width; - uint height; - destination.GetDimensions(width, height); - - uint clim_w, clim_h; - planet_climate.GetDimensions(clim_w, clim_h); - uint2 clim_sz = uint2(clim_w, clim_h); - - float2 normalized_position = tid.xy / float2(width, height); - - uint4 local_climate = planet_climate[normalized_position * clim_sz]; - local_climate = uint4(local_climate.xy / 2, local_climate.z / 16, 0); - - uint4 surface_color = surface[local_climate.yx]; - - - uint4 read = uint4(0, 0, 0, 255); - //read.xy = planetClimate[index].climate_th; - //read.z = 0; - - read.xyz = surface_color.xyz; - - destination[tid.xy] = read; -} +
+ + + + Load + + + + + + + Save + + + - PushButton + Render -- GitLab From dbb9246ab52aa6ad3a513f2000bb87d4d568a239 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sun, 26 Nov 2023 21:19:40 -0500 Subject: [PATCH 06/49] Add all the ecosystem terrain projection functionality from SCExplorer, looking pretty good at the moment. --- starfab/gui/widgets/image_viewer.py | 6 +- starfab/gui/widgets/pages/page_PlanetView.py | 19 +- starfab/models/planet.py | 138 ++++--- starfab/planets/shader.hlsl | 367 ++++++++++++++++++- starfab/resources/ui/PlanetView.ui | 20 +- 5 files changed, 491 insertions(+), 59 deletions(-) diff --git a/starfab/gui/widgets/image_viewer.py b/starfab/gui/widgets/image_viewer.py index 4f8d7fe..82bb3d5 100644 --- a/starfab/gui/widgets/image_viewer.py +++ b/starfab/gui/widgets/image_viewer.py @@ -113,10 +113,11 @@ class QImageViewer(qtw.QGraphicsView): self.scale(factor, factor) self._zoom = 0 - def setImage(self, image): + def setImage(self, image, fit: bool = True): """Set the scene's current image pixmap to the input QImage or QPixmap. Raises a RuntimeError if the input image has type other than QImage or QPixmap. :type image: QImage | QPixmap + :type fit: bool """ self._zoom = 0 if isinstance(image, qtg.QPixmap): @@ -135,7 +136,8 @@ class QImageViewer(qtw.QGraphicsView): self._empty = True self.setDragMode(qtw.QGraphicsView.NoDrag) self.image.setPixmap(qtg.QPixmap()) - self.fitInView() + if fit: + self.fitInView() def wheelEvent(self, event): if self.hasImage(): diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 2778028..3d0c82f 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -29,6 +29,7 @@ class PlanetView(qtw.QWidget): self.planetComboBox: QComboBox = None self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None + self.sampleModeComboBox: QComboBox = None self.hlslTextBox: QPlainTextEdit = None self.renderOutput: QImageViewer = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self @@ -52,6 +53,12 @@ class PlanetView(qtw.QWidget): ("Earth Format (-180/180deg) Unshifted", "EarthUnShifted") ])) + self.sampleModeComboBox.setModel(self.create_model([ + ("Nearest Neighbor", 0), + ("Bi-Linear", 1), + ("Bi-Cubic", 2), + ])) + if isinstance(sc, StarCitizen): self.sc = sc self._handle_datacore_loaded() @@ -97,17 +104,23 @@ class PlanetView(qtw.QWidget): with io.open(self.shader_path(), "w") as shader: shader.write(self.hlslTextBox.toPlainText()) + def get_settings(self): + scale = self.renderResolutionComboBox.currentData(role=Qt.UserRole) + coordinates = self.coordinateSystemComboBox.currentData(role=Qt.UserRole) + interpolation = self.sampleModeComboBox.currentData(role=Qt.UserRole) + shader = self.hlslTextBox.toPlainText() + return RenderSettings(True, scale, coordinates, shader, interpolation) + def _do_render(self): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) - shader = self.hlslTextBox.toPlainText() print(selected_obj) selected_obj.load_data() print("Done loading planet data") # TODO: Deal with buffer directly - img = selected_obj.render(RenderSettings(True, 1, "NASA", shader)) + img = selected_obj.render(self.get_settings()) qimg = ImageQt.ImageQt(img) - self.renderOutput.setImage(qimg) + self.renderOutput.setImage(qimg, fit=False) def _handle_datacore_unloading(self): if self.starmap is not None: diff --git a/starfab/models/planet.py b/starfab/models/planet.py index 34fd1c3..649fdf3 100644 --- a/starfab/models/planet.py +++ b/starfab/models/planet.py @@ -11,9 +11,9 @@ from typing import Callable, Union import compushady from PIL import Image -from compushady import Texture2D, Buffer, HEAP_UPLOAD, Compute, HEAP_READBACK, Resource, HEAP_DEFAULT +from compushady import Texture2D, Texture3D, Buffer, HEAP_UPLOAD, Compute, HEAP_READBACK, Resource, HEAP_DEFAULT from compushady.backends.vulkan import Device -from compushady.formats import R8G8B8A8_UINT, R16_UINT, R8_UINT +from compushady.formats import R8G8B8A8_UINT, R16_UINT, R8_UINT, R16_FLOAT, R8G8B8A8_UNORM_SRGB, R8G8B8A8_SINT from compushady.shaders import hlsl from scdatatools import StarCitizen from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk @@ -25,11 +25,13 @@ from starfab.utils import image_converter class RenderSettings: - def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str): + def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str, + interpolation: int): self.gpu = gpu self.resolution = resolution self.coordinate_mode = coordinate_mode self.hlsl = hlsl + self.interpolation = interpolation class LocalClimateData: @@ -104,6 +106,39 @@ class LUTData: self.brush_obj: Brush = None +class RenderJobSettings: + PACK_STRING: str = "5f3i2f" + PACK_LENGTH: int = struct.calcsize(PACK_STRING) + + def __init__(self): + self.offset_x: float = 0 + self.offset_y: float = 0 + + self.size_x: float = 1 + self.size_y: float = 1 + + self.planet_radius: float = 0 + self.interpolation: int = 0 + self.render_scale_x: int = 0 + self.render_scale_y: int = 0 + + self.local_humidity_influence: float = 0 + self.local_temperature_influence: float = 0 + + def pack(self) -> bytes: + return struct.pack(RenderJobSettings.PACK_STRING, + self.offset_x, self.offset_y, self.size_x, self.size_y, + self.planet_radius, self.interpolation, + self.render_scale_x, self.render_scale_y, + self.local_humidity_influence, self.local_temperature_influence) + + def update_buffer(self, buffer_gpu: Buffer): + data = self.pack() + buffer = Buffer(RenderJobSettings.PACK_LENGTH, HEAP_UPLOAD) + buffer.upload(data) + buffer.copy_to(buffer_gpu) + + class EcoSystem: _cache = {} _tex_root = Path("Data/Textures/planets/terrain") @@ -133,7 +168,7 @@ class EcoSystem: self.elevation_path: str = eco_data["Textures"]["elevationTexture"] self.climate_data: list[list[LocalClimateData]] = None - self.climate_texture: Image = None + self.climate_image: Image = None self.normal_texture: Image = None self.elevation_bytes: bytearray = None self.elevation_size: int = 0 @@ -156,14 +191,9 @@ class EcoSystem: climate_texture = Image.open(io.BytesIO(_read_texture(self.tex_path))) normal_texture = Image.open(io.BytesIO(_read_texture(self.norm_path))) - self.climate_texture = climate_texture + self.climate_image = climate_texture self.normal_texture = normal_texture - # Addressed as [x][y] - self.climate_data = [[LocalClimateData(x, y) - for y in range(climate_texture.height)] - for x in range(climate_texture.width)] - elevation_path = (EcoSystem._tex_root / self.elevation_path).as_posix().lower() elevation_info: P4KInfo = EcoSystem._sc.p4k.NameToInfoLower[elevation_path] with elevation_info.open() as o: @@ -184,7 +214,7 @@ class Planet: self.humidity_influence = None self.temperature_influence = None - self.climate_texture: Image = None + self.climate_data: bytearray = None self.offset_data: bytearray = None self.heightmap_data: bytearray = None @@ -211,7 +241,7 @@ class Planet: ecosystem_ids = self.planet_data["data"]["General"]["uniqueEcoSystemsGUIDs"] self.ecosystems = [EcoSystem.find_in_cache_(e) - for e in ecosystem_ids[0:1] + for e in ecosystem_ids if e != "00000000-0000-0000-0000-000000000000"] eco: EcoSystem @@ -220,12 +250,11 @@ class Planet: # R = Temp, G = Humidity, B = Biome ID, A = Unused splat_raw = self.planet_data["data"]["splatMap"] - self.climate_texture = Image.frombuffer('RGBA', - (self.tile_count, self.tile_count), - bytearray(splat_raw)) + self.climate_data = bytearray(splat_raw) offsets_raw = self.planet_data["data"]["randomOffset"] self.offset_data = bytearray(offsets_raw) + print(f"Offset Length: {len(self.offset_data)}") hm_path = self.planet_data["data"]["General"]["tSphere"]["sHMWorld"] hm_path_posix = ("data" / Path(hm_path)).as_posix().lower() @@ -291,7 +320,7 @@ class Planet: def _construct_gpu_resources(self): def _buffer_from_lut_color(fn_color: Callable[[LUTData], list[int]]) -> Resource: texture = Texture2D(128, 128, R8G8B8A8_UINT) - buffer = Buffer(texture.size, HEAP_UPLOAD) + staging = Buffer(texture.size, HEAP_UPLOAD) data = [0 for _ in range(128 * 128 * 4)] for y in range(128): for x in range(128): @@ -302,63 +331,90 @@ class Planet: data[index + 2] = color[2] data[index + 3] = color[3] # TODO: Can we write directly into the buffer as we generate? - buffer.upload(bytes(data)) - buffer.copy_to(texture) + staging.upload(bytes(data)) + staging.copy_to(texture) return texture - def _buffer_for_climate(climate: list[list[LocalClimateData]]) -> Resource: - buffer_bytes = LocalClimateData.create_packed_bytes(climate) - print(f"Allocating climate: {len(buffer_bytes)}") - gpu_buffer = Buffer(len(buffer_bytes), HEAP_DEFAULT, stride=16) - local_buffer = Buffer(len(buffer_bytes), HEAP_UPLOAD) - local_buffer.upload(buffer_bytes) - local_buffer.copy_to(gpu_buffer) - return gpu_buffer - def _buffer_for_image(image: Image) -> Resource: data = image.tobytes('raw', 'RGBA') texture = Texture2D(image.width, image.height, R8G8B8A8_UINT) - buffer = Buffer(len(data), HEAP_UPLOAD) - buffer.upload(data) - buffer.copy_to(texture) + staging = Buffer(len(data), HEAP_UPLOAD) + staging.upload(data) + staging.copy_to(texture) return texture def _buffer_for_bytes(data: bytearray, bytes_per_pixel: int, format: int) -> Resource: dim = int(math.sqrt(len(data) / bytes_per_pixel)) img = Texture2D(dim, dim, format) - buffer = Buffer(len(data), HEAP_UPLOAD) - buffer.upload(data) - buffer.copy_to(img) + staging = Buffer(len(data), HEAP_UPLOAD) + staging.upload(data) + staging.copy_to(img) return img + def _ecosystem_texture(fn_texture: Callable[[EcoSystem], Image.Image], format: int) -> Texture3D: + textures: list[Image] = [fn_texture(e) for e in self.ecosystems] + result_tex = Texture3D(textures[0].width, textures[0].height, len(textures), format) + staging = Buffer(result_tex.size, HEAP_UPLOAD) + framesize = int(result_tex.size / len(textures)) + for i, eco_texture in enumerate(textures): + eco_bytes = eco_texture.tobytes('raw', 'RGBA') + staging.upload(eco_bytes, i * framesize) + staging.copy_to(result_tex) + return result_tex + self.gpu_resources['bedrock'] = _buffer_from_lut_color(lambda x: x.bedrockColor) self.gpu_resources['surface'] = _buffer_from_lut_color(lambda x: x.surfaceColor) - self.gpu_resources['planet_climate'] = _buffer_for_image(self.climate_texture) + self.gpu_resources['planet_climate'] = _buffer_for_bytes(self.climate_data, 4, R8G8B8A8_UINT) self.gpu_resources['planet_offsets'] = _buffer_for_bytes(self.offset_data, 1, R8_UINT) self.gpu_resources['planet_heightmap'] = _buffer_for_bytes(self.heightmap_data, 2, R16_UINT) - # self.gpu_resources['ecosystems'] = _buffer_for_climate(self.ecosystems[0].climate_data) + + self.gpu_resources['ecosystems'] = _ecosystem_texture(lambda x: x.climate_image, R8G8B8A8_UINT) # self.gpu_resources['splat'] = _buffer_for_image(self.climate_texture) - eco_size = len(self.ecosystems[0].climate_data) - destination_texture = Texture2D(eco_size * 2, eco_size, R8G8B8A8_UINT) + self.gpu_resources['settings'] = Buffer(RenderJobSettings.PACK_LENGTH) + + width = self.gpu_resources['planet_climate'].width + height = self.gpu_resources['planet_climate'].height + + destination_texture = Texture2D(width * 2, height, R8G8B8A8_UINT) self.gpu_resources['destination'] = destination_texture self.gpu_resources['readback'] = Buffer(destination_texture.size, HEAP_READBACK) - print(f"Dest: ({destination_texture.width}, {destination_texture.height})") - def render(self, settings: RenderSettings): r = self.gpu_resources samplers = [ - r['bedrock'], r['surface'], r['planet_climate'], r['planet_offsets'], r['planet_heightmap'] + r['bedrock'], r['surface'], + r['planet_climate'], r['planet_offsets'], r['planet_heightmap'], + r['ecosystems'] ] + job_s = RenderJobSettings() + + if settings.coordinate_mode == "NASA": + job_s.offset_x = 0.5 + elif settings.coordinate_mode == "EarthShifted": + job_s.offset_x = 0.5 + elif settings.coordinate_mode == "EarchUnshifted": + job_s.offset_x = 0 + + job_s.interpolation = settings.interpolation + job_s.render_scale_x = settings.resolution * 2 + job_s.render_scale_y = settings.resolution + job_s.planet_radius = self.radius_m + job_s.local_humidity_influence = self.humidity_influence + job_s.local_temperature_influence = self.temperature_influence + destination_texture = self.gpu_resources['destination'] readback_buffer = self.gpu_resources['readback'] + settings_buffer = self.gpu_resources['settings'] + + job_s.update_buffer(settings_buffer) compute = Compute(hlsl.compile(settings.hlsl), srv=samplers, - uav=[destination_texture]) + uav=[destination_texture], + cbv=[settings_buffer]) compute.dispatch(destination_texture.width // 8, destination_texture.height // 8, 1) destination_texture.copy_to(readback_buffer) diff --git a/starfab/planets/shader.hlsl b/starfab/planets/shader.hlsl index 24861a0..04da445 100644 --- a/starfab/planets/shader.hlsl +++ b/starfab/planets/shader.hlsl @@ -1,34 +1,385 @@ +#define PI radians(180) + +struct RenderJobSettings +{ + float2 offset; + float2 size; + float planet_radius; + int interpolation; + int2 render_scale; + float local_humidity_influence; + float local_temperature_influence; +}; + +struct LocalizedWarping +{ + float2 center; + float vertical_delta; + float upper_width; + float lower_width; +}; + +struct ProjectedTerrainInfluence +{ + float2 temp_humidity; + float elevation; + float mask_total; + int num_influences; + + bool is_override; + uint3 override; +}; + Texture2D bedrock : register(t0); Texture2D surface : register(t1); - Texture2D planet_climate : register(t2); Texture2D planet_offsets : register(t3); Texture2D planet_heightmap : register(t4); +Texture3D ecosystem_climates: register(t5); RWTexture2D destination : register(u0); +ConstantBuffer jobSettings : register(b0); + +uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) +{ + float4 topRow = lerp(ul, ur, value.x); + float4 bottomRow = lerp(bl, br, value.x); + + return lerp(topRow, bottomRow, value.y); +} + +uint4 interpolate_cubic(float4 v0, float4 v1, float4 v2, float4 v3, float fraction) +{ + float4 p = (v3 - v2) - (v0 - v1); + float4 q = (v0 - v1) - p; + float4 r = v2 - v0; + + return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; +} + +uint4 take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +{ + return texture[position % dimensions]; +} + +uint take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +{ + return texture[position % dimensions]; +} + +uint4 take_sample_bilinear(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint4 tl = take_sample_nn(texture, position, dimensions); + uint4 tr = take_sample_nn(texture, position + int2(1, 0), dimensions); + uint4 bl = take_sample_nn(texture, position + int2(0, 1), dimensions); + uint4 br = take_sample_nn(texture, position + int2(1, 1), dimensions); + return lerp2d(tl, tr, bl, br, offset); +} + +uint4 take_sample_bicubic(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint4 samples[4]; + + for (int i = 0; i < 4; ++i) + { + float4 ll = take_sample_nn(texture, position + int2(-1, i - 1), dimensions); + float4 ml = take_sample_nn(texture, position + int2( 0, i - 1), dimensions); + float4 mr = take_sample_nn(texture, position + int2( 1, i - 1), dimensions); + float4 rr = take_sample_nn(texture, position + int2( 2, i - 1), dimensions); + samples[i] = interpolate_cubic(ll, ml, mr, rr, offset.x); + } + + return interpolate_cubic(samples[0], samples[1], samples[2], samples[3], offset.y); +} + +uint4 take_sample_nn_3d(Texture3D texture, float2 position, int2 dimensions, int layer) +{ + uint3 read_pos; + read_pos.xy = uint2(position % dimensions); + read_pos.z = layer; + return texture[read_pos]; +} + +int4 take_sample_bicubic_3d(Texture3D texture, float2 position, int2 dimensions, int layer) +{ + float2 offset = position - floor(position); + int4 samples[4]; + + for (int i = 0; i < 4; ++i) + { + float4 ll = take_sample_nn_3d(texture, position + int2(-1, i - 1), dimensions, layer); + float4 ml = take_sample_nn_3d(texture, position + int2( 0, i - 1), dimensions, layer); + float4 mr = take_sample_nn_3d(texture, position + int2( 1, i - 1), dimensions, layer); + float4 rr = take_sample_nn_3d(texture, position + int2( 2, i - 1), dimensions, layer); + samples[i] = interpolate_cubic(ll, ml, mr, rr, offset.x); + } + + return interpolate_cubic(samples[0], samples[1], samples[2], samples[3], offset.y); +} + +uint4 take_sample_uint(Texture2D texture, float2 position, int2 dimensions, int mode) +{ + if(mode == 0) { + return take_sample_nn(texture, position, dimensions); + } else if (mode == 1) { + return take_sample_bilinear(texture, position, dimensions); + } else if (mode == 2) { + return take_sample_bicubic(texture, position, dimensions); + } else { + return uint4(0, 0, 0, 0); + } +} + +float circumference_at_distance_from_equator(float vertical_distance_meters) +{ + float half_circumference_km = PI * jobSettings.planet_radius; + //Normalize to +/-0.5, then * pi to get +/- half_pi + float angle = (vertical_distance_meters / half_circumference_km) * PI; + return (float)(cos(angle) * jobSettings.planet_radius * PI * 2); +} + +float2 get_normalized_location(float2 position_meters) +{ + float half_circumference_m = (float)(PI * jobSettings.planet_radius); + float vert_normalized = ((position_meters.y / half_circumference_m) + 0.5f); + float vert_circ = circumference_at_distance_from_equator(position_meters.y); + float horiz_normalized = ((position_meters.x / vert_circ) + 0.5f); + + return float2(min(1.0f, max(0.0f, horiz_normalized)), + min(1.0f, max(0.0f, vert_normalized))); // 0.0 - 1.0 for x,y +} + +float2 pixels_to_meters(float2 position, int2 dimensions) +{ + if (position.x < 0) position.x += dimensions.x; + if (position.y < 0) position.y += dimensions.y; + if (position.x >= dimensions.x) position.x -= dimensions.x; + if (position.y >= dimensions.y) position.y -= dimensions.y; + + float half_circumference_km = (float)(PI * jobSettings.planet_radius); + + float vert_distance = ((position.y / dimensions.y) - 0.5f) * half_circumference_km; // +/- 1/4 circumference + float vert_circ = circumference_at_distance_from_equator(vert_distance); + float horiz_distance = ((position.x / dimensions.x) - 0.5f) * vert_circ; // +/- 1/2 circumference + + return float2(horiz_distance, vert_distance); +} + +LocalizedWarping get_local_image_warping(float2 position_meters, float2 patch_size_meters) +{ + LocalizedWarping result; + + float upper_circ = circumference_at_distance_from_equator(position_meters.y + (patch_size_meters.y / 2)); + float lower_circ = circumference_at_distance_from_equator(position_meters.y - (patch_size_meters.y / 2)); + + result.center = get_normalized_location(position_meters); + result.vertical_delta = (float)(patch_size_meters.y / 2.0f / (PI * jobSettings.planet_radius)); + result.upper_width = (patch_size_meters.x / 2.0f) / upper_circ; + result.lower_width = (patch_size_meters.x / 2.0f) / lower_circ; + + return result; +} + +ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimensions, float2 projected_size, float2 terrain_size) +{ + uint off_w, off_h; + planet_offsets.GetDimensions(off_w, off_h); + uint2 off_sz = uint2(off_w, off_h); + + ProjectedTerrainInfluence result = { + float2(0, 0), //float2 temp_humidity + 0.0f, //float elevation; + 0.0f, //float mask_total; + 0, //int num_influences; + false, //bool is_override; + uint3(0,0,0) //uint3 override; + }; + + float2 pos_meters = pixels_to_meters(position, dimensions); + + LocalizedWarping projection_warping = get_local_image_warping(pos_meters, projected_size); + LocalizedWarping physical_warping = get_local_image_warping(pos_meters, terrain_size); + + //upper_bound will be the lower number here because image is 0,0 top-left + float upper_bound = position.y - (projection_warping.vertical_delta * dimensions.y); + float lower_bound = position.y + (projection_warping.vertical_delta * dimensions.y); + + //No wrapping for Y-axis + float search_y_start = clamp(floor(upper_bound), 0, dimensions.y - 1); + float search_y_end = clamp(ceil(lower_bound), 0, dimensions.y - 1); + + int terrain_step = 1; + int pole_distance = dimensions.y / 16; + + //TODO Vary terrain step from 1 at pole_distance to TileCount at the pole + if (position.y < pole_distance / 2 || position.y >= dimensions.y - pole_distance / 2) { + terrain_step = 8; + }else if(position.y < pole_distance || position.y >= dimensions.y - pole_distance) { + terrain_step = 4; + } + + if (round(position.x - 0.5f) == 40 && round(position.y - 0.5f) == 40) { + result.is_override = true; + result.override = uint3(0, 255, 0); + return result; + } + + //Search vertically all cells that our projection overlaps with + for(float search_y_px = search_y_start; search_y_px <= search_y_end; search_y_px += 1.0f) + { + //Turn this cells position back into meters, and calculate local distortion size for this row specifically + float2 search_meters = pixels_to_meters(float2(0, search_y_px), dimensions); + float search_circumference = circumference_at_distance_from_equator(search_meters.y); + float half_projected_width_px = (projected_size.x / 2 / search_circumference) * dimensions.y; + + //Break if the circumference at this pixel is less than a single projection, ie: directly at poles + if(search_circumference < projected_size.x) + continue; + + float row_left_bound = position.x - half_projected_width_px; + float row_right_bound = position.x + half_projected_width_px; + float search_x_start = floor(row_left_bound); + float search_x_end = ceil(row_right_bound); + + //Now search horizontally all cells that out projection (at this vertical position) overlaps with + for (float search_x_px = search_x_start; search_x_px <= search_x_end; search_x_px += 1.0f) + { + if ((int)search_x_px % terrain_step != 0) continue; + + //We can use NN here since we are just looking for the ecosystem data + uint2 search_pos = uint2(search_x_px, search_y_px); + + uint4 global_climate = take_sample_uint(planet_climate, search_pos, dimensions, 0); + uint ecosystem_id = uint(global_climate.z / 16); + + // TODO: Use global random offset data + float offset = take_sample_nn(planet_offsets, float2(search_pos) / dimensions * off_sz, off_sz) / 256.0f; + float2 terrain_center = float2(search_x_px, search_y_px) + offset; + + //Now finally calculate the local distortion at the center of the terrain + float2 terrain_center_m = pixels_to_meters(terrain_center, dimensions.y); + float terrain_circumference = circumference_at_distance_from_equator(terrain_center_m.y); + float half_terrain_width_projected_px = (projected_size.x / 2 / terrain_circumference) * dimensions.y; + float half_terrain_width_physical_px = (terrain_size.x / 2 / terrain_circumference) * dimensions.y; + + float terrain_left_edge = terrain_center.x - half_terrain_width_projected_px; + float terrain_right_edge = terrain_center.x + half_terrain_width_projected_px; + float terrain_top_edge = terrain_center.y - (projection_warping.vertical_delta * dimensions.y); + float terrain_bottom_edge = terrain_center.y + (projection_warping.vertical_delta * dimensions.y); + + //Reject pixels outside of the terrains projected pixel borders + if (position.x < terrain_left_edge || position.x > terrain_right_edge) + continue; + if (position.y < terrain_top_edge || position.y > terrain_bottom_edge) + continue; + + //Finally calculate UV coordinates and return result + float terrain_u = ((position.x - terrain_center.x) / half_terrain_width_physical_px / 2) + 0.5f; + float terrain_v = ((position.y - terrain_center.y) / (physical_warping.vertical_delta * dimensions.y * 2)) + 0.5f; + float patch_u = ((position.x - terrain_left_edge) / (half_terrain_width_projected_px * 2)); + float patch_v = ((position.y - terrain_top_edge) / (projection_warping.vertical_delta * dimensions.y * 2)); + + if (terrain_u < 0) terrain_u += 1; + if (terrain_v < 0) terrain_v += 1; + if (terrain_u >= 1) terrain_u -= 1; + if (terrain_v >= 1) terrain_v -= 1; + + if (patch_u < 0) patch_u += 1; + if (patch_v < 0) patch_v += 1; + if (patch_u >= 1) patch_u -= 1; + if (patch_v >= 1) patch_v -= 1; + + float2 terrain_uv = float2(terrain_u, terrain_v); + float2 patch_uv = float2(patch_u, patch_v); + + float2 delta = patch_uv - float2(0.5f, 0.5f); + float center_distance = sqrt(delta.x * delta.x + delta.y * delta.y) * 1; + float local_mask_value = (float)(center_distance > 0.5f ? 0 : cos(center_distance * PI)); + + uint2 size = uint2(1024, 1024); + int4 local_eco_data = take_sample_nn_3d(ecosystem_climates, terrain_uv * size, size, ecosystem_id); + float4 local_eco_normalized = (local_eco_data - 127) / 127.0f; + // TODO: Heightmaps + + if (false && (round(search_x_px % 20) == 0 && round(search_y_px % 20) == 0)) { + result.is_override = true; + //result.override = uint3(255 * terrain_uv.x, 255 * terrain_uv.y, 0) * local_mask_value; + result.override = uint3(planet_offsets[search_pos], 0, 0); + return result; + } + + result.temp_humidity += local_eco_normalized * local_mask_value; + result.mask_total += local_mask_value; + result.num_influences += 1; + } + } + + return result; +} + +// TODO: Use the z-thread ID for doing sub-pixel searching [numthreads(8,8,1)] void main(uint3 tid : SV_DispatchThreadID) { uint width; uint height; destination.GetDimensions(width, height); + uint2 out_sz = uint2(width, height); uint clim_w, clim_h; planet_climate.GetDimensions(clim_w, clim_h); uint2 clim_sz = uint2(clim_w, clim_h); - float2 normalized_position = tid.xy / float2(width, height); + uint off_w, off_h; + planet_offsets.GetDimensions(off_w, off_h); + uint2 off_sz = uint2(off_w, off_h); - uint4 local_climate = planet_climate[normalized_position * clim_sz]; - local_climate = uint4(local_climate.xy / 2, local_climate.z / 16, 0); + float2 normalized_position = tid.xy / float2(out_sz) / jobSettings.render_scale; + normalized_position.xy += jobSettings.offset; + normalized_position.y += 0.5f; + normalized_position = normalized_position % 1; - uint4 surface_color = surface[local_climate.yx]; + uint4 local_nn = take_sample_uint(planet_climate, normalized_position * clim_sz, clim_sz, 0); + uint local_ecosystem = local_nn.z / 16; + uint global_offset = take_sample_nn(planet_offsets, normalized_position * off_sz, off_sz); + + uint4 local_climate = take_sample_uint(planet_climate, normalized_position * clim_sz, clim_sz, jobSettings.interpolation); uint4 read = uint4(0, 0, 0, 255); - //read.xy = planetClimate[index].climate_th; - //read.z = 0; + // uint4 local_climate = planet_climate[normalized_position * clim_sz]; + + local_climate = uint4(local_climate.xy, 0, 0); + float terrain_scaling = 1.5f; + float2 projected_size = float2(6000, 6000) * terrain_scaling; + float2 physical_size = float2(4000, 4000) * terrain_scaling; + float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); + + ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position * out_sz, out_sz, projected_size, physical_size); + + if (eco_influence.is_override) { + read.xyz = eco_influence.override; + } else { + if(eco_influence.mask_total > 0) { + eco_influence.temp_humidity /= eco_influence.mask_total; + local_climate.yx += eco_influence.temp_humidity * local_influence; + + } + + uint4 surface_color = take_sample_uint(surface, local_climate.yx / 2, int2(128, 128), jobSettings.interpolation); + + read.xyz = surface_color.xyz; + } - read.xyz = surface_color.xyz; + // Grid rendering + int2 cell_position = tid.xy % jobSettings.render_scale; + if(false && (cell_position.x == 0 || cell_position.y == 0)) + { + read.x = 255; + read.y = 0; + read.z = 0; + } destination[tid.xy] = read; } \ No newline at end of file diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index aadb886..6fe1dc5 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -55,7 +55,7 @@ - 500 + 800 16777215 @@ -112,20 +112,30 @@ - + HLSL - + - + + + + + Sample Mode + + + + + + @@ -185,7 +195,7 @@ QImageViewer - QGraphicsView + QWidget
starfab.gui.widgets.image_viewer
-- GitLab From 92a26a68e9bf836d123e636b7ee5f07d02a4b2f2 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Tue, 28 Nov 2023 03:48:52 -0500 Subject: [PATCH 07/49] Heightmaps, multiple layers, more classes and files. More settings as well. Now supports oceans! --- starfab/gui/widgets/pages/page_PlanetView.py | 87 +++- starfab/models/planet.py | 452 ------------------- starfab/planets/__init__.py | 0 starfab/planets/data.py | 127 ++++++ starfab/planets/ecosystem.py | 87 ++++ starfab/planets/planet.py | 159 +++++++ starfab/planets/planet_renderer.py | 210 +++++++++ starfab/planets/shader.hlsl | 284 ++++++++---- starfab/resources/ui/PlanetView.ui | 55 ++- 9 files changed, 868 insertions(+), 593 deletions(-) delete mode 100644 starfab/models/planet.py create mode 100644 starfab/planets/__init__.py create mode 100644 starfab/planets/data.py create mode 100644 starfab/planets/ecosystem.py create mode 100644 starfab/planets/planet.py create mode 100644 starfab/planets/planet_renderer.py diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 3d0c82f..8daa284 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,17 +1,21 @@ import io +from typing import Union -from PIL import ImageQt +from PIL import ImageQt, Image from PySide6.QtCore import Qt -from PySide6.QtGui import QStandardItem, QStandardItemModel, QPixmap -from PySide6.QtWidgets import QComboBox, QPushButton, QPlainTextEdit, QGraphicsView, QGraphicsScene +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import QComboBox, QPushButton from qtpy import uic from scdatatools import StarCitizen from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance -from starfab.gui import qtw +from starfab.gui import qtw, qtc from starfab.gui.widgets.image_viewer import QImageViewer from starfab.log import getLogger -from starfab.models.planet import Planet, RenderSettings, EcoSystem +from starfab.planets.planet import Planet +from starfab.planets.data import RenderSettings +from starfab.planets.ecosystem import EcoSystem +from starfab.planets.planet_renderer import PlanetRenderer, RenderResult from starfab.resources import RES_PATH from pathlib import Path @@ -30,12 +34,17 @@ class PlanetView(qtw.QWidget): self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None self.sampleModeComboBox: QComboBox = None - self.hlslTextBox: QPlainTextEdit = None + self.outputResolutionComboBox: QComboBox = None + self.displayModeComboBox: QComboBox = None + self.displayLayerComboBox: QComboBox = None self.renderOutput: QImageViewer = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self self.starmap = None + self.renderer = PlanetRenderer((4096, 2048)) + self.last_render: Union[None, RenderResult] = None + self.renderResolutionComboBox.setModel(self.create_model([ ("1px per tile", 1), ("2px per tile", 2), @@ -58,6 +67,27 @@ class PlanetView(qtw.QWidget): ("Bi-Linear", 1), ("Bi-Cubic", 2), ])) + self.sampleModeComboBox.setCurrentIndex(1) + + self.outputResolutionComboBox.setModel(self.create_model([ + ("2MP - 2,048 x 1,024", (2048, 1024)), + ("8MP - 4,096 x 2,048", (4096, 2048)), + ("32MP - 8,192 x 4,096", (8192, 4096)), + ("128MP - 16,384 x 8,192", (16384, 8192)) + ])) + self.outputResolutionComboBox.currentIndexChanged.connect(self._display_resolution_changed) + + self.displayModeComboBox.setModel(self.create_model([ + ("Pixel-Perfect", qtc.Qt.FastTransformation), + ("Smooth", qtc.Qt.SmoothTransformation) + ])) + self.displayModeComboBox.currentIndexChanged.connect(self._display_mode_changed) + + self.displayLayerComboBox.setModel(self.create_model([ + ("Surface", "surface"), + ("Heightmap", "heightmap") + ])) + self.displayLayerComboBox.currentIndexChanged.connect(self._display_layer_changed) if isinstance(sc, StarCitizen): self.sc = sc @@ -67,11 +97,25 @@ class PlanetView(qtw.QWidget): self.sc.datacore_model.loaded.connect(self._hack_before_load) self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) - self.loadButton.clicked.connect(self._load_shader) - self.saveButton.clicked.connect(self._save_shader) self.renderButton.clicked.connect(self._do_render) - self._load_shader() + def _display_resolution_changed(self): + new_resolution = self.outputResolutionComboBox.currentData(role=Qt.UserRole) + self.renderer.set_resolution(new_resolution) + + def _display_mode_changed(self): + new_transform = self.displayModeComboBox.currentData(role=Qt.UserRole) + self.renderOutput.image.setTransformationMode(new_transform) + + def _display_layer_changed(self): + layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) + if layer == "surface": + self._update_image(self.last_render.tex_color) + elif layer == "heightmap": + self._update_image(self.last_render.tex_heightmap) + + def _update_image(self, image: Image): + self.renderOutput.setImage(ImageQt.ImageQt(image), fit=False) def _hack_before_load(self): # Hacky method to support faster dev testing and launching directly in-app @@ -96,20 +140,17 @@ class PlanetView(qtw.QWidget): def shader_path(self) -> Path: return Path(__file__).parent / '../../../planets/shader.hlsl' - def _load_shader(self): + def _get_shader(self): with io.open(self.shader_path(), "r") as shader: - self.hlslTextBox.setPlainText(shader.read()) - - def _save_shader(self): - with io.open(self.shader_path(), "w") as shader: - shader.write(self.hlslTextBox.toPlainText()) + return shader.read() def get_settings(self): scale = self.renderResolutionComboBox.currentData(role=Qt.UserRole) coordinates = self.coordinateSystemComboBox.currentData(role=Qt.UserRole) interpolation = self.sampleModeComboBox.currentData(role=Qt.UserRole) - shader = self.hlslTextBox.toPlainText() - return RenderSettings(True, scale, coordinates, shader, interpolation) + resolution = self.outputResolutionComboBox.currentData(role=Qt.UserRole) + shader = self._get_shader() + return RenderSettings(True, scale, coordinates, shader, interpolation, resolution) def _do_render(self): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) @@ -117,10 +158,16 @@ class PlanetView(qtw.QWidget): selected_obj.load_data() print("Done loading planet data") + self.renderer.set_planet(selected_obj) + self.renderer.set_settings(self.get_settings()) + # TODO: Deal with buffer directly - img = selected_obj.render(self.get_settings()) - qimg = ImageQt.ImageQt(img) - self.renderOutput.setImage(qimg, fit=False) + try: + # img = selected_obj.render(self.get_settings()) + self.last_render = self.renderer.render() + self._display_layer_changed() + except Exception as ex: + logger.exception(ex) def _handle_datacore_unloading(self): if self.starmap is not None: diff --git a/starfab/models/planet.py b/starfab/models/planet.py deleted file mode 100644 index 649fdf3..0000000 --- a/starfab/models/planet.py +++ /dev/null @@ -1,452 +0,0 @@ -import io -import json -import math -import random -import struct -from os import listdir -from os.path import join, isfile -from pathlib import Path -from struct import unpack -from typing import Callable, Union - -import compushady -from PIL import Image -from compushady import Texture2D, Texture3D, Buffer, HEAP_UPLOAD, Compute, HEAP_READBACK, Resource, HEAP_DEFAULT -from compushady.backends.vulkan import Device -from compushady.formats import R8G8B8A8_UINT, R16_UINT, R8_UINT, R16_FLOAT, R8G8B8A8_UNORM_SRGB, R8G8B8A8_SINT -from compushady.shaders import hlsl -from scdatatools import StarCitizen -from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk -from scdatatools.engine.textures import unsplit_dds -from scdatatools.p4k import P4KInfo -from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance - -from starfab.utils import image_converter - - -class RenderSettings: - def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str, - interpolation: int): - self.gpu = gpu - self.resolution = resolution - self.coordinate_mode = coordinate_mode - self.hlsl = hlsl - self.interpolation = interpolation - - -class LocalClimateData: - def __init__(self, x: float, y: float): - self.x: float = x - self.y: float = y - - self.temperature: int = 0 - self.humidity: int = 0 - self.eco_id: int = 0 - - self.elevation: float = 0 - self.random_offset: float = 0 - - self.normal_x: float = 0 - self.normal_y: float = 0 - self.normal_z: float = 0 - - self.surfaceTextureMap: int = 0 - self.oprBlendIndex: int = 0 - - @staticmethod - def create_packed_bytes(climate_records: list[list]): - pack_string = "2f2I" - pack_size = struct.calcsize(pack_string) - pack_index = 0 - pack_data = bytearray(pack_size * len(climate_records) * len(climate_records[0])) - # TODO: There has to be a better way to do this :^) - print("Packing") - for y in range(len(climate_records[0])): - print(f"{y}/{len(climate_records[0])}") - for x in range(len(climate_records)): - clim: LocalClimateData = climate_records[x][y] - struct.pack_into(pack_string, pack_data, pack_index, - clim.elevation, clim.random_offset, - clim.surfaceTextureMap, clim.oprBlendIndex) - pack_index += pack_size - print("Done Packing") - return pack_data - - -class Brush: - def __init__(self, record): - self.record = record - self.bedrockGradientColorA = self.record["bedrockBrush"]["colorGradient"]["gradientColorA"] - self.bedrockGradientColorB = self.record["bedrockBrush"]["colorGradient"]["gradientColorB"] - self.surfaceGradientColorA = self.record["surfaceBrush"]["colorGradient"]["gradientColorA"] - self.surfaceGradientColorB = self.record["surfaceBrush"]["colorGradient"]["gradientColorB"] - self.tMin = self.record["tMin"] - self.tMax = self.record["tMax"] - self.hMin = self.record["hMin"] - self.hMax = self.record["hMax"] - - -class LUTData: - def __init__(self): - self.bedrockGloss: list = [] - self.surfaceGloss: list = [] - self.bedrockColor: list = [] - self.surfaceColor: list = [] - self.brush_id: int = 0 - self.ground_texture_id: int = 0 - self.object_preset_id: int = 0 - self.bd_gradient_val_bedrock: int = 0 - self.bd_gradient_val_surface: int = 0 - self.bd_value_offset_bedrock: float = 0 - self.bd_value_offset_surface: float = 0 - self.bd_saturation_offset_bedrock: float = 0 - self.bd_saturation_offset_surface: float = 0 - self.bd_orp_blend_index: int = 0 - self.bd_texture_layer_index: int = 0 - self.brush_obj: Brush = None - - -class RenderJobSettings: - PACK_STRING: str = "5f3i2f" - PACK_LENGTH: int = struct.calcsize(PACK_STRING) - - def __init__(self): - self.offset_x: float = 0 - self.offset_y: float = 0 - - self.size_x: float = 1 - self.size_y: float = 1 - - self.planet_radius: float = 0 - self.interpolation: int = 0 - self.render_scale_x: int = 0 - self.render_scale_y: int = 0 - - self.local_humidity_influence: float = 0 - self.local_temperature_influence: float = 0 - - def pack(self) -> bytes: - return struct.pack(RenderJobSettings.PACK_STRING, - self.offset_x, self.offset_y, self.size_x, self.size_y, - self.planet_radius, self.interpolation, - self.render_scale_x, self.render_scale_y, - self.local_humidity_influence, self.local_temperature_influence) - - def update_buffer(self, buffer_gpu: Buffer): - data = self.pack() - buffer = Buffer(RenderJobSettings.PACK_LENGTH, HEAP_UPLOAD) - buffer.upload(data) - buffer.copy_to(buffer_gpu) - - -class EcoSystem: - _cache = {} - _tex_root = Path("Data/Textures/planets/terrain") - _sc: Union[None, StarCitizen] = None - - @staticmethod - def find_in_cache_(guid: str): - return EcoSystem._cache[guid] if guid in EcoSystem._cache else None - - @staticmethod - def read_eco_headers(sc: StarCitizen): - if len(EcoSystem._cache) != 0: - return EcoSystem._cache - EcoSystem._sc = sc - p4k_results = EcoSystem._sc.p4k.search(".eco", mode="endswith") - for result in p4k_results: - eco = EcoSystem(json.loads(result.open().read())) - EcoSystem._cache[eco.id] = eco - return EcoSystem._cache - - def __init__(self, eco_data: dict): - self.id = eco_data["GUID"] - self.name = eco_data["name"] - self.offset = eco_data["fOffsetH"] - self.tex_path: str = eco_data["Textures"]["ecoSystemAlbedoTexture"].replace(".tif", ".dds") - self.norm_path: str = eco_data["Textures"]["ecoSystemNormalTexture"].replace(".tif", ".dds") - self.elevation_path: str = eco_data["Textures"]["elevationTexture"] - - self.climate_data: list[list[LocalClimateData]] = None - self.climate_image: Image = None - self.normal_texture: Image = None - self.elevation_bytes: bytearray = None - self.elevation_size: int = 0 - - def read_full_data(self): - if self.climate_data: - return - - def _read_texture(subpath: str) -> bytes: - texture_path = (EcoSystem._tex_root / subpath).as_posix().lower() - - p4k_dds_files = EcoSystem._sc.p4k.search(texture_path, mode="startswith") - dds_files = {record.orig_filename: record - for record - in p4k_dds_files - if not record.orig_filename.endswith("a")} - res: bytes = unsplit_dds(dds_files) - return image_converter.convert_buffer(res, "dds", "png") - - climate_texture = Image.open(io.BytesIO(_read_texture(self.tex_path))) - normal_texture = Image.open(io.BytesIO(_read_texture(self.norm_path))) - - self.climate_image = climate_texture - self.normal_texture = normal_texture - - elevation_path = (EcoSystem._tex_root / self.elevation_path).as_posix().lower() - elevation_info: P4KInfo = EcoSystem._sc.p4k.NameToInfoLower[elevation_path] - with elevation_info.open() as o: - self.elevation_bytes = bytearray(o.read()) - self.elevation_size = int(math.sqrt(len(self.elevation_bytes) / 2)) - - print(f"Textures loaded for {self.name}") - - -class Planet: - def __init__(self, oc: ObjectContainerInstance, data: JSONChunk): - self.oc: ObjectContainerInstance = oc - self.data: JSONChunk = data - - self.planet_data = None - self.tile_count = None - self.radius_m = None - self.humidity_influence = None - self.temperature_influence = None - - self.climate_data: bytearray = None - self.offset_data: bytearray = None - self.heightmap_data: bytearray = None - - self.brushes: list[Brush] = None - self.ecosystems: list[EcoSystem] = None - - self.lut: list[list[LUTData]] = None - - self.gpu_resources = {} - self.gpu_computer: Compute = None - - def load_data(self) -> object: - if self.planet_data: - return self.planet_data - - self.planet_data = self.data.dict() - - self.tile_count = self.planet_data["data"]["globalSplatWidth"] - self.radius_m = self.planet_data["data"]["General"]["tSphere"]["fPlanetTerrainRadius"] - self.humidity_influence = self.planet_data["data"]["General"]["textureLayers"]["localHumidityInfluence"] - self.temperature_influence = self.planet_data["data"]["General"]["textureLayers"]["localTemperatureInfluence"] - - self.brushes = [Brush(b) for b in self.planet_data["data"]["General"]["brushes"]] - - ecosystem_ids = self.planet_data["data"]["General"]["uniqueEcoSystemsGUIDs"] - self.ecosystems = [EcoSystem.find_in_cache_(e) - for e in ecosystem_ids - if e != "00000000-0000-0000-0000-000000000000"] - - eco: EcoSystem - for eco in self.ecosystems: - eco.read_full_data() - - # R = Temp, G = Humidity, B = Biome ID, A = Unused - splat_raw = self.planet_data["data"]["splatMap"] - self.climate_data = bytearray(splat_raw) - - offsets_raw = self.planet_data["data"]["randomOffset"] - self.offset_data = bytearray(offsets_raw) - print(f"Offset Length: {len(self.offset_data)}") - - hm_path = self.planet_data["data"]["General"]["tSphere"]["sHMWorld"] - hm_path_posix = ("data" / Path(hm_path)).as_posix().lower() - hm_data: ObjectContainer = self.oc.container - hm_info: P4KInfo = hm_data.socpak.p4k.NameToInfoLower[hm_path_posix] - hm_raw = bytearray(hm_data.socpak.p4k.open(hm_info).read()) - - self.heightmap_data = hm_raw - - self._build_lut() - - print("Creating GPU Resources...") - self._construct_gpu_resources() - print("Created!") - - def _build_lut(self): - # Addressed as [x][y] - self.lut = [[LUTData() - for y in range(self.tile_count)] - for x in range(self.tile_count)] - - def _clamp(val, _min, _max): - return _min if val < _min else (_max if val > _max else val) - - def _lerp_int(_min, _max, val): - return int(((_max - _min) * val) + _min) - - def _lerp_color(a, b, val): - return [ - _clamp(_lerp_int(a[0], b[0], val), 0, 255), - _clamp(_lerp_int(a[1], b[1], val), 0, 255), - _clamp(_lerp_int(a[2], b[2], val), 0, 255), - _clamp(_lerp_int(a[3], b[3], val), 0, 255) - ] - - for y in range(128): - for x in range(128): - lut = self.lut[x][y] - lut.ground_texture_id = self.planet_data["data"]["groundTexIDLUT"][y][x] - lut.object_preset_id = self.planet_data["data"]["objectPresetLUT"][y][x] - lut.brush_id = self.planet_data["data"]["brushIDLUT"][y][x] - lut.brush_obj = self.brushes[lut.brush_id] - - brush_data = self.planet_data["data"]["brushDataLUT"][y][x] - - lut.bd_gradient_val_bedrock = brush_data["gradientPosBedRock"] - lut.bd_gradient_val_surface = brush_data["gradientPosSurface"] - lut.bd_value_offset_bedrock = brush_data["valOffsetBedRock"] - lut.bd_value_offset_surface = brush_data["valOffsetSurface"] - lut.bd_saturation_offset_bedrock = brush_data["satOffsetBedRock"] - lut.bd_saturation_offset_surface = brush_data["satOffsetSurface"] - lut.bd_orp_blend_index = brush_data["oprBlendIndex"] - lut.bd_texture_layer_index = brush_data["texturLayerIndex"] - - lut.bedrockColor = _lerp_color(lut.brush_obj.bedrockGradientColorA, - lut.brush_obj.bedrockGradientColorB, - lut.bd_gradient_val_bedrock / 127) - - lut.surfaceColor = _lerp_color(lut.brush_obj.surfaceGradientColorA, - lut.brush_obj.surfaceGradientColorB, - lut.bd_gradient_val_surface / 127) - - def _construct_gpu_resources(self): - def _buffer_from_lut_color(fn_color: Callable[[LUTData], list[int]]) -> Resource: - texture = Texture2D(128, 128, R8G8B8A8_UINT) - staging = Buffer(texture.size, HEAP_UPLOAD) - data = [0 for _ in range(128 * 128 * 4)] - for y in range(128): - for x in range(128): - color = fn_color(self.lut[x][y]) - index = 4 * (y * 128 + x) - data[index + 0] = color[0] - data[index + 1] = color[1] - data[index + 2] = color[2] - data[index + 3] = color[3] - # TODO: Can we write directly into the buffer as we generate? - staging.upload(bytes(data)) - staging.copy_to(texture) - return texture - - def _buffer_for_image(image: Image) -> Resource: - data = image.tobytes('raw', 'RGBA') - texture = Texture2D(image.width, image.height, R8G8B8A8_UINT) - staging = Buffer(len(data), HEAP_UPLOAD) - staging.upload(data) - staging.copy_to(texture) - return texture - - def _buffer_for_bytes(data: bytearray, bytes_per_pixel: int, format: int) -> Resource: - dim = int(math.sqrt(len(data) / bytes_per_pixel)) - img = Texture2D(dim, dim, format) - staging = Buffer(len(data), HEAP_UPLOAD) - staging.upload(data) - staging.copy_to(img) - return img - - def _ecosystem_texture(fn_texture: Callable[[EcoSystem], Image.Image], format: int) -> Texture3D: - textures: list[Image] = [fn_texture(e) for e in self.ecosystems] - result_tex = Texture3D(textures[0].width, textures[0].height, len(textures), format) - staging = Buffer(result_tex.size, HEAP_UPLOAD) - framesize = int(result_tex.size / len(textures)) - for i, eco_texture in enumerate(textures): - eco_bytes = eco_texture.tobytes('raw', 'RGBA') - staging.upload(eco_bytes, i * framesize) - staging.copy_to(result_tex) - return result_tex - - self.gpu_resources['bedrock'] = _buffer_from_lut_color(lambda x: x.bedrockColor) - self.gpu_resources['surface'] = _buffer_from_lut_color(lambda x: x.surfaceColor) - self.gpu_resources['planet_climate'] = _buffer_for_bytes(self.climate_data, 4, R8G8B8A8_UINT) - self.gpu_resources['planet_offsets'] = _buffer_for_bytes(self.offset_data, 1, R8_UINT) - self.gpu_resources['planet_heightmap'] = _buffer_for_bytes(self.heightmap_data, 2, R16_UINT) - - self.gpu_resources['ecosystems'] = _ecosystem_texture(lambda x: x.climate_image, R8G8B8A8_UINT) - # self.gpu_resources['splat'] = _buffer_for_image(self.climate_texture) - - self.gpu_resources['settings'] = Buffer(RenderJobSettings.PACK_LENGTH) - - width = self.gpu_resources['planet_climate'].width - height = self.gpu_resources['planet_climate'].height - - destination_texture = Texture2D(width * 2, height, R8G8B8A8_UINT) - - self.gpu_resources['destination'] = destination_texture - self.gpu_resources['readback'] = Buffer(destination_texture.size, HEAP_READBACK) - - def render(self, settings: RenderSettings): - r = self.gpu_resources - samplers = [ - r['bedrock'], r['surface'], - r['planet_climate'], r['planet_offsets'], r['planet_heightmap'], - r['ecosystems'] - ] - - job_s = RenderJobSettings() - - if settings.coordinate_mode == "NASA": - job_s.offset_x = 0.5 - elif settings.coordinate_mode == "EarthShifted": - job_s.offset_x = 0.5 - elif settings.coordinate_mode == "EarchUnshifted": - job_s.offset_x = 0 - - job_s.interpolation = settings.interpolation - job_s.render_scale_x = settings.resolution * 2 - job_s.render_scale_y = settings.resolution - job_s.planet_radius = self.radius_m - job_s.local_humidity_influence = self.humidity_influence - job_s.local_temperature_influence = self.temperature_influence - - destination_texture = self.gpu_resources['destination'] - readback_buffer = self.gpu_resources['readback'] - settings_buffer = self.gpu_resources['settings'] - - job_s.update_buffer(settings_buffer) - - compute = Compute(hlsl.compile(settings.hlsl), - srv=samplers, - uav=[destination_texture], - cbv=[settings_buffer]) - - compute.dispatch(destination_texture.width // 8, destination_texture.height // 8, 1) - destination_texture.copy_to(readback_buffer) - return self.readback() - - def readback(self): - destination: Texture2D = self.gpu_resources['destination'] - readback: Buffer = self.gpu_resources['readback'] - destination.copy_to(readback) - - return Image.frombuffer('RGBA', - (destination.width, destination.height), - readback.readback()) - - @staticmethod - def try_create(oc: ObjectContainerInstance): - json_chunk = Planet.find_planet_data(oc) - if json_chunk: - return Planet(oc, json_chunk) - else: - return None - - @staticmethod - def find_planet_data(oc: ObjectContainerInstance) -> [None, JSONChunk]: - if (not oc.container) or (not oc.container.has_additional): - return None - if oc.container.additional_data: - chunkfile: ChunkFile - for chunkfile in oc.container.additional_data: - chunk: Chunk - for c_id, chunk in chunkfile.chunks.items(): - if isinstance(chunk, JSONChunk): - return chunk - return None - diff --git a/starfab/planets/__init__.py b/starfab/planets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/starfab/planets/data.py b/starfab/planets/data.py new file mode 100644 index 0000000..81b02e2 --- /dev/null +++ b/starfab/planets/data.py @@ -0,0 +1,127 @@ +import struct +from typing import Tuple + +from compushady import Buffer, HEAP_UPLOAD + + +class LUTData: + def __init__(self): + self.bedrockGloss: list = [] + self.surfaceGloss: list = [] + self.bedrockColor: list = [] + self.surfaceColor: list = [] + self.brush_id: int = 0 + self.ground_texture_id: int = 0 + self.object_preset_id: int = 0 + self.bd_gradient_val_bedrock: int = 0 + self.bd_gradient_val_surface: int = 0 + self.bd_value_offset_bedrock: float = 0 + self.bd_value_offset_surface: float = 0 + self.bd_saturation_offset_bedrock: float = 0 + self.bd_saturation_offset_surface: float = 0 + self.bd_orp_blend_index: int = 0 + self.bd_texture_layer_index: int = 0 + self.brush_obj: Brush = None + + +class RenderJobSettings: + PACK_STRING: str = "5f3i5f4i" + PACK_LENGTH: int = struct.calcsize(PACK_STRING) + + def __init__(self): + self.offset_x: float = 0 + self.offset_y: float = 0 + + self.size_x: float = 1 + self.size_y: float = 1 + + self.planet_radius: float = 0 + self.interpolation: int = 0 + self.render_scale_x: int = 0 + self.render_scale_y: int = 0 + + self.local_humidity_influence: float = 0 + self.local_temperature_influence: float = 0 + self.global_terrain_height_influence: float = 4000 + self.ecosystem_terrain_height_influence: float = 1000 + + self.ocean_depth: float = -2000 + self.ocean_color: list[int] = [0, 0, 0, 255] + + def pack(self) -> bytes: + return struct.pack(RenderJobSettings.PACK_STRING, + self.offset_x, self.offset_y, self.size_x, self.size_y, + self.planet_radius, self.interpolation, + self.render_scale_x, self.render_scale_y, + self.local_humidity_influence, self.local_temperature_influence, + self.global_terrain_height_influence, self.ecosystem_terrain_height_influence, + self.ocean_depth, *self.ocean_color) + + def update_buffer(self, buffer_gpu: Buffer): + data = self.pack() + buffer = Buffer(RenderJobSettings.PACK_LENGTH, HEAP_UPLOAD) + buffer.upload(data) + buffer.copy_to(buffer_gpu) + + +class Brush: + def __init__(self, record): + self.record = record + self.bedrockGradientColorA = self.record["bedrockBrush"]["colorGradient"]["gradientColorA"] + self.bedrockGradientColorB = self.record["bedrockBrush"]["colorGradient"]["gradientColorB"] + self.surfaceGradientColorA = self.record["surfaceBrush"]["colorGradient"]["gradientColorA"] + self.surfaceGradientColorB = self.record["surfaceBrush"]["colorGradient"]["gradientColorB"] + self.tMin = self.record["tMin"] + self.tMax = self.record["tMax"] + self.hMin = self.record["hMin"] + self.hMax = self.record["hMax"] + + +class LocalClimateData: + def __init__(self, x: float, y: float): + self.x: float = x + self.y: float = y + + self.temperature: int = 0 + self.humidity: int = 0 + self.eco_id: int = 0 + + self.elevation: float = 0 + self.random_offset: float = 0 + + self.normal_x: float = 0 + self.normal_y: float = 0 + self.normal_z: float = 0 + + self.surfaceTextureMap: int = 0 + self.oprBlendIndex: int = 0 + + @staticmethod + def create_packed_bytes(climate_records: list[list]): + pack_string = "2f2I" + pack_size = struct.calcsize(pack_string) + pack_index = 0 + pack_data = bytearray(pack_size * len(climate_records) * len(climate_records[0])) + # TODO: There has to be a better way to do this :^) + print("Packing") + for y in range(len(climate_records[0])): + print(f"{y}/{len(climate_records[0])}") + for x in range(len(climate_records)): + clim: LocalClimateData = climate_records[x][y] + struct.pack_into(pack_string, pack_data, pack_index, + clim.elevation, clim.random_offset, + clim.surfaceTextureMap, clim.oprBlendIndex) + pack_index += pack_size + print("Done Packing") + return pack_data + + +class RenderSettings: + def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str, + interpolation: int, output_resolution: Tuple[int, int]): + self.gpu = gpu + self.resolution = resolution + self.coordinate_mode = coordinate_mode + self.hlsl = hlsl + self.interpolation = interpolation + self.output_resolution = output_resolution diff --git a/starfab/planets/ecosystem.py b/starfab/planets/ecosystem.py new file mode 100644 index 0000000..1d39e82 --- /dev/null +++ b/starfab/planets/ecosystem.py @@ -0,0 +1,87 @@ +import io +import json +import math +import os +from pathlib import Path +from typing import Union + +from PIL import Image +from scdatatools import StarCitizen +from scdatatools.engine.textures import unsplit_dds +from scdatatools.p4k import P4KInfo + +from starfab.planets.data import LocalClimateData +from starfab.utils import image_converter + + +class EcoSystem: + _cache = {} + _tex_root = Path("Data/Textures/planets/terrain") + _sc: Union[None, StarCitizen] = None + + @staticmethod + def find_in_cache_(guid: str): + return EcoSystem._cache[guid] if guid in EcoSystem._cache else None + + @staticmethod + def read_eco_headers(sc: StarCitizen): + if len(EcoSystem._cache) != 0: + return EcoSystem._cache + EcoSystem._sc = sc + p4k_results = EcoSystem._sc.p4k.search(".eco", mode="endswith") + for result in p4k_results: + eco = EcoSystem(json.loads(result.open().read())) + EcoSystem._cache[eco.id] = eco + return EcoSystem._cache + + def __init__(self, eco_data: dict): + self.id = eco_data["GUID"] + self.name = eco_data["name"] + self.offset = eco_data["fOffsetH"] + self.tex_path: str = eco_data["Textures"]["ecoSystemAlbedoTexture"].replace(".tif", ".dds") + self.norm_path: str = eco_data["Textures"]["ecoSystemNormalTexture"].replace(".tif", ".dds") + self.elevation_path: str = eco_data["Textures"]["elevationTexture"] + + self.climate_data: list[list[LocalClimateData]] = None + self.climate_image: Image = None + self.normal_texture: Image = None + self.elevation_bytes: bytearray = None + self.elevation_size: int = 0 + + def read_full_data(self): + if self.climate_data: + return + + def _read_texture(subpath: str) -> bytes: + texture_path = (EcoSystem._tex_root / subpath).as_posix().lower() + + p4k_dds_files = EcoSystem._sc.p4k.search(texture_path, mode="startswith") + dds_files = {record.orig_filename: record + for record + in p4k_dds_files + if not record.orig_filename.endswith("a")} + res: bytes = unsplit_dds(dds_files) + return image_converter.convert_buffer(res, "dds", "png") + + # TODO: Use settings to define a cache directory to store these in + def _read_with_cache(subpath: str) -> Image: + check_path = Path(subpath).with_suffix(".png") + if not os.path.exists(check_path.parent): + os.makedirs(check_path.parent) + if check_path.exists(): + return Image.open(check_path) + else: + img = Image.open(io.BytesIO(_read_texture(subpath))) + img.save(check_path, "png") + return img + + self.climate_image = _read_with_cache(self.tex_path) + self.normal_texture = _read_with_cache(self.norm_path) + + elevation_path = (EcoSystem._tex_root / self.elevation_path).as_posix().lower() + elevation_info: P4KInfo = EcoSystem._sc.p4k.NameToInfoLower[elevation_path] + with elevation_info.open() as o: + self.elevation_bytes = bytearray(o.read()) + self.elevation_size = int(math.sqrt(len(self.elevation_bytes) / 2)) + + print(f"Textures loaded for {self.name}") diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py new file mode 100644 index 0000000..08c8e8f --- /dev/null +++ b/starfab/planets/planet.py @@ -0,0 +1,159 @@ +import struct +from pathlib import Path + +from compushady import Compute +from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk, CryXMLBChunk +from scdatatools.p4k import P4KInfo +from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance + +from starfab.planets.data import LUTData, Brush +from starfab.planets.ecosystem import EcoSystem + + +class Planet: + def __init__(self, oc: ObjectContainerInstance, data: JSONChunk): + self.oc: ObjectContainerInstance = oc + self.data: JSONChunk = data + + self.planet_data = None + self.tile_count = None + self.radius_m = None + self.humidity_influence = None + self.temperature_influence = None + + self.climate_data: bytearray = None + self.offset_data: bytearray = None + self.heightmap_data: bytearray = None + + self.brushes: list[Brush] = None + self.ecosystems: list[EcoSystem] = None + + self.lut: list[list[LUTData]] = None + + self.gpu_resources = {} + self.gpu_computer: Compute = None + + def load_data(self) -> object: + if self.planet_data: + return self.planet_data + + self.planet_data = self.data.dict() + + self.tile_count = self.planet_data["data"]["globalSplatWidth"] + self.radius_m = self.planet_data["data"]["General"]["tSphere"]["fPlanetTerrainRadius"] + self.humidity_influence = self.planet_data["data"]["General"]["textureLayers"]["localHumidityInfluence"] + self.temperature_influence = self.planet_data["data"]["General"]["textureLayers"]["localTemperatureInfluence"] + + ocean_material = self.planet_data["data"]["oceanParams"]["Geometry"]["MaterialOceanPlanet"] + ocean_path_posix = ("data" / Path(ocean_material)).as_posix().lower() + try: + if ocean_path_posix in self.oc.container.socpak.p4k.NameToInfoLower: + ocean_mat_info = self.oc.container.socpak.p4k.NameToInfoLower[ocean_path_posix] + ocean_mat_chunks = ChunkFile(ocean_mat_info) + for chunk in ocean_mat_chunks.chunks: + if isinstance(chunk, CryXMLBChunk): + print("XML Chunk!") + print(chunk.dict()) + diffuse_path = chunk.dict()["Material"] + print(diffuse_path) + except Exception as ex: + print(ex) + + self.brushes = [Brush(b) for b in self.planet_data["data"]["General"]["brushes"]] + + ecosystem_ids = self.planet_data["data"]["General"]["uniqueEcoSystemsGUIDs"] + self.ecosystems = [EcoSystem.find_in_cache_(e) + for e in ecosystem_ids + if e != "00000000-0000-0000-0000-000000000000"] + + eco: EcoSystem + for eco in self.ecosystems: + eco.read_full_data() + + # R = Temp, G = Humidity, B = Biome ID, A = Unused + splat_raw = self.planet_data["data"]["splatMap"] + self.climate_data = bytearray(splat_raw) + + offsets_raw = self.planet_data["data"]["randomOffset"] + self.offset_data = bytearray(offsets_raw) + + hm_path = self.planet_data["data"]["General"]["tSphere"]["sHMWorld"] + hm_path_posix = ("data" / Path(hm_path)).as_posix().lower() + hm_data: ObjectContainer = self.oc.container + hm_info: P4KInfo = hm_data.socpak.p4k.NameToInfoLower[hm_path_posix] + hm_raw: bytearray = bytearray(hm_data.socpak.p4k.open(hm_info).read()) + # Flip the endian-ness of the heightmap in-place, for easier interpolation + for offset in range(0, len(hm_raw), 2): + struct.pack_into("h", hm_raw, offset)[0]) + + self.heightmap_data = hm_raw + + self._build_lut() + + def _build_lut(self): + # Addressed as [x][y] + self.lut = [[LUTData() + for y in range(self.tile_count)] + for x in range(self.tile_count)] + + def _clamp(val, _min, _max): + return _min if val < _min else (_max if val > _max else val) + + def _lerp_int(_min, _max, val): + return int(((_max - _min) * val) + _min) + + def _lerp_color(a, b, val): + return [ + _clamp(_lerp_int(a[0], b[0], val), 0, 255), + _clamp(_lerp_int(a[1], b[1], val), 0, 255), + _clamp(_lerp_int(a[2], b[2], val), 0, 255), + _clamp(_lerp_int(a[3], b[3], val), 0, 255) + ] + + for y in range(128): + for x in range(128): + lut = self.lut[x][y] + lut.ground_texture_id = self.planet_data["data"]["groundTexIDLUT"][y][x] + lut.object_preset_id = self.planet_data["data"]["objectPresetLUT"][y][x] + lut.brush_id = self.planet_data["data"]["brushIDLUT"][y][x] + lut.brush_obj = self.brushes[lut.brush_id] + + brush_data = self.planet_data["data"]["brushDataLUT"][y][x] + + lut.bd_gradient_val_bedrock = brush_data["gradientPosBedRock"] + lut.bd_gradient_val_surface = brush_data["gradientPosSurface"] + lut.bd_value_offset_bedrock = brush_data["valOffsetBedRock"] + lut.bd_value_offset_surface = brush_data["valOffsetSurface"] + lut.bd_saturation_offset_bedrock = brush_data["satOffsetBedRock"] + lut.bd_saturation_offset_surface = brush_data["satOffsetSurface"] + lut.bd_orp_blend_index = brush_data["oprBlendIndex"] + lut.bd_texture_layer_index = brush_data["texturLayerIndex"] + + lut.bedrockColor = _lerp_color(lut.brush_obj.bedrockGradientColorA, + lut.brush_obj.bedrockGradientColorB, + lut.bd_gradient_val_bedrock / 127) + + lut.surfaceColor = _lerp_color(lut.brush_obj.surfaceGradientColorA, + lut.brush_obj.surfaceGradientColorB, + lut.bd_gradient_val_surface / 127) + + @staticmethod + def try_create(oc: ObjectContainerInstance): + json_chunk = Planet.find_planet_data(oc) + if json_chunk: + return Planet(oc, json_chunk) + else: + return None + + @staticmethod + def find_planet_data(oc: ObjectContainerInstance) -> [None, JSONChunk]: + if (not oc.container) or (not oc.container.has_additional): + return None + if oc.container.additional_data: + chunkfile: ChunkFile + for chunkfile in oc.container.additional_data: + chunk: Chunk + for c_id, chunk in chunkfile.chunks.items(): + if isinstance(chunk, JSONChunk): + return chunk + return None diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py new file mode 100644 index 0000000..df938e1 --- /dev/null +++ b/starfab/planets/planet_renderer.py @@ -0,0 +1,210 @@ +import gc +import math +from typing import Union, Callable, Tuple, cast + +from PIL import Image +from compushady import Texture2D, Compute, Resource, HEAP_UPLOAD, Buffer, Texture3D, HEAP_READBACK +from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT +from compushady.shaders import hlsl + +from starfab.planets.planet import Planet +from starfab.planets.data import LUTData, RenderJobSettings, RenderSettings +from starfab.planets.ecosystem import EcoSystem + + +class RenderResult: + def __init__(self, settings: RenderJobSettings, tex_color: Image, tex_heightmap: Image): + self.settings = settings + self.tex_color = tex_color + self.tex_heightmap = tex_heightmap + + +class PlanetRenderer: + + def __init__(self, buffer_resolution: Tuple[int, int]): + self.planet: Union[None, Planet] = None + self.settings: Union[None, RenderSettings] = None + self.gpu_resources: dict[str, Resource] = {} + self.render_resolution: Tuple[int, int] = buffer_resolution + + self._create_gpu_output_resources() + + def set_planet(self, planet: Planet): + if planet != self.planet: + self._cleanup_planet_gpu_resources() + self._cleanup_planet_gpu_resources() + self.planet = planet + self._create_planet_gpu_resources() + self._write_planet_input_resources() + + def set_settings(self, settings: RenderSettings): + self.settings = settings + + def set_resolution(self, new_dimensions: Tuple[int, int]): + self._cleanup_gpu_output_resources() + self.render_resolution = new_dimensions + self._create_gpu_output_resources() + + def render(self) -> RenderResult: + job_s = RenderJobSettings() + + if not self.planet: + raise Exception("Planet not set yet!") + + if self.settings.coordinate_mode == "NASA": + job_s.offset_x = 0.5 + elif self.settings.coordinate_mode == "EarthShifted": + job_s.offset_x = 0.5 + elif self.settings.coordinate_mode == "EarchUnshifted": + job_s.offset_x = 0 + + job_s.interpolation = self.settings.interpolation + job_s.render_scale_x = self.settings.resolution * 2 + job_s.render_scale_y = self.settings.resolution + job_s.planet_radius = self.planet.radius_m + job_s.local_humidity_influence = self.planet.humidity_influence + job_s.local_temperature_influence = self.planet.temperature_influence + + job_s.update_buffer(cast(Buffer, self.gpu_resources['settings'])) + + computer = self._get_computer() + + computer.dispatch(self.render_resolution[0] // 8, self.render_resolution[1] // 8, 1) + # TODO: Keep this around and render multiple tiles with the same Compute obj + + out_color: Image = self._read_frame("output_color") + out_heightmap: Image = self._read_frame("output_heightmap") + + del computer + + return RenderResult(job_s, out_color, out_heightmap) + + def _read_frame(self, resource_name: str) -> Image: + readback: Buffer = cast(Buffer, self.gpu_resources['readback']) + destination: Texture2D = cast(Texture2D, self.gpu_resources[resource_name]) + destination.copy_to(readback) + output_bytes = readback.readback() + del readback + + return Image.frombuffer('RGBA', + (destination.width, destination.height), + output_bytes) + + def _get_computer(self) -> Compute: + res = self.gpu_resources + samplers = [ + res['bedrock'], res['surface'], + res['planet_climate'], res['planet_offsets'], res['planet_heightmap'], + res['ecosystem_climates'], res['ecosystem_heightmaps'] + ] + + constant_buffers = [ + res['settings'] + ] + + output_buffers = [ + res['output_color'], res['output_heightmap'] + ] + + return Compute(hlsl.compile(self.settings.hlsl), + srv=samplers, + cbv=constant_buffers, + uav=output_buffers) + + def _do_cleanup(self, *resources): + for res_name in resources: + if res_name in self.gpu_resources: + del self.gpu_resources[res_name] + + gc.collect() # Force GC to clean up/dispose the buffers we just del'd + + def _cleanup_planet_gpu_resources(self): + self._do_cleanup('bedrock', 'surface', + 'planet_climate', 'planet_offsets', 'planet_heightmap', + 'ecosystem_climates', 'ecosystem_heightmaps', + 'settings') + + def _create_planet_gpu_resources(self): + climate_size = int(math.sqrt(len(self.planet.climate_data) / 4)) + offset_size = int(math.sqrt(len(self.planet.climate_data) / 4)) + heightmap_size = int(math.sqrt(len(self.planet.climate_data) / 4)) + ecosystem_climate_size = self.planet.ecosystems[0].climate_image.width + ecosystem_heightmap_size = self.planet.ecosystems[0].elevation_size + + self.gpu_resources['bedrock'] = Texture2D(128, 128, R8G8B8A8_UINT) + self.gpu_resources['surface'] = Texture2D(128, 128, R8G8B8A8_UINT) + self.gpu_resources['planet_climate'] = Texture2D(climate_size, climate_size, R8G8B8A8_UINT) + self.gpu_resources['planet_offsets'] = Texture2D(offset_size, offset_size, R8_UINT) + self.gpu_resources['planet_heightmap'] = Texture2D(heightmap_size, heightmap_size, R16_UINT) + + self.gpu_resources['ecosystem_climates'] = Texture3D(ecosystem_climate_size, ecosystem_climate_size, + len(self.planet.ecosystems), R8G8B8A8_UINT) + self.gpu_resources['ecosystem_heightmaps'] = Texture3D(ecosystem_heightmap_size, ecosystem_heightmap_size, + len(self.planet.ecosystems), R16_UINT) + + self.gpu_resources['settings'] = Buffer(RenderJobSettings.PACK_LENGTH) + + def _write_planet_input_resources(self): + def _update_from_lut(gpu_resource: Resource, fn_color: Callable[[LUTData], list[int]]): + staging = Buffer(gpu_resource.size, HEAP_UPLOAD) + data = [0 for _ in range(128 * 128 * 4)] + for y in range(128): + for x in range(128): + color = fn_color(self.planet.lut[x][y]) + index = 4 * (y * 128 + x) + data[index + 0] = color[0] + data[index + 1] = color[1] + data[index + 2] = color[2] + data[index + 3] = color[3] + # TODO: Can we write directly into the buffer as we generate? + staging.upload(bytes(data)) + staging.copy_to(gpu_resource) + del staging + + def _update_from_bytes(gpu_resource: Resource, data: bytearray): + staging = Buffer(len(data), HEAP_UPLOAD) + staging.upload(data) + staging.copy_to(gpu_resource) + del staging + return gpu_resource + + def _update_from_ecosystems(gpu_resource: Resource, fn_data: Callable[[EcoSystem], Union[bytes, Image.Image]]): + staging = Buffer(gpu_resource.size, HEAP_UPLOAD) + framesize = int(gpu_resource.size / len(self.planet.ecosystems)) + for i, eco in enumerate(self.planet.ecosystems): + eco_data = fn_data(eco) + if isinstance(eco_data, Image.Image): + eco_bytes = eco_data.tobytes('raw', 'RGBA') + else: + eco_bytes = eco_data + staging.upload(eco_bytes, i * framesize) + staging.copy_to(gpu_resource) + del staging + + _update_from_lut(self.gpu_resources['bedrock'], lambda x: x.bedrockColor) + _update_from_lut(self.gpu_resources['surface'], lambda x: x.surfaceColor) + _update_from_bytes(self.gpu_resources['planet_climate'], self.planet.climate_data) + _update_from_bytes(self.gpu_resources['planet_offsets'], self.planet.offset_data) + _update_from_bytes(self.gpu_resources['planet_heightmap'], self.planet.heightmap_data) + _update_from_ecosystems(self.gpu_resources['ecosystem_climates'], lambda x: x.climate_image) + _update_from_ecosystems(self.gpu_resources['ecosystem_heightmaps'], lambda x: x.elevation_bytes) + + def _cleanup_gpu_output_resources(self): + self._do_cleanup('output_color', 'output_heightmap', 'readback') + + def _create_gpu_output_resources(self): + if 'output_color' in self.gpu_resources: + return + + # TODO: Support variable size output buffers. For now just render to fixed size and stitch + # Also wasting a bit of space having the heightmap 32BPP when we only need 16 + # but this makes things a lot easier to work with elsewhere :^) + (out_w, out_h) = self.render_resolution + output_color_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) + output_heightmap_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) + + self.gpu_resources['output_color'] = output_color_texture + self.gpu_resources['output_heightmap'] = output_heightmap_texture + # NOTE: We will use the same readback buffer to read output_color and output_heightmap + # We take output_color's size because it will be 2x the size of the heightmap tex + self.gpu_resources['readback'] = Buffer(output_color_texture.size, HEAP_READBACK) diff --git a/starfab/planets/shader.hlsl b/starfab/planets/shader.hlsl index 04da445..8115733 100644 --- a/starfab/planets/shader.hlsl +++ b/starfab/planets/shader.hlsl @@ -1,4 +1,7 @@ #define PI radians(180) +#define MODE_NN 0 +#define MODE_BI_LINEAR 1 +#define MODE_BI_CUBIC 2 struct RenderJobSettings { @@ -7,8 +10,14 @@ struct RenderJobSettings float planet_radius; int interpolation; int2 render_scale; + float local_humidity_influence; float local_temperature_influence; + float global_terrain_height_influence; + float ecosystem_terrain_height_influence; + + float ocean_depth; + uint3 ocean_color; }; struct LocalizedWarping @@ -34,13 +43,15 @@ Texture2D bedrock : register(t0); Texture2D surface : register(t1); Texture2D planet_climate : register(t2); Texture2D planet_offsets : register(t3); -Texture2D planet_heightmap : register(t4); +Texture2D planet_heightmap : register(t4); Texture3D ecosystem_climates: register(t5); - -RWTexture2D destination : register(u0); +Texture3D ecosystem_heightmaps: register(t6); ConstantBuffer jobSettings : register(b0); +RWTexture2D output_color : register(u0); +RWTexture2D output_heightmap: register(u1); + uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) { float4 topRow = lerp(ul, ur, value.x); @@ -49,6 +60,14 @@ uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) return lerp(topRow, bottomRow, value.y); } +uint lerp2d(uint ul, uint ur, uint bl, uint br, float2 value) +{ + float topRow = lerp(ul, ur, value.x); + float bottomRow = lerp(bl, br, value.x); + + return lerp(topRow, bottomRow, value.y); +} + uint4 interpolate_cubic(float4 v0, float4 v1, float4 v2, float4 v3, float fraction) { float4 p = (v3 - v2) - (v0 - v1); @@ -58,12 +77,18 @@ uint4 interpolate_cubic(float4 v0, float4 v1, float4 v2, float4 v3, float fracti return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; } -uint4 take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +uint interpolate_cubic_uint(float v0, float v1, float v2, float v3, float fraction) { - return texture[position % dimensions]; + float p = (v3 - v2) - (v0 - v1); + float q = (v0 - v1) - p; + float r = v2 - v0; + + return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; } -uint take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +/* Texture2D implementations */ + +uint4 take_sample_nn(Texture2D texture, float2 position, int2 dimensions) { return texture[position % dimensions]; } @@ -95,6 +120,78 @@ uint4 take_sample_bicubic(Texture2D texture, float2 position, int2 dimens return interpolate_cubic(samples[0], samples[1], samples[2], samples[3], offset.y); } +uint4 take_sample(Texture2D texture, float2 position, int2 dimensions, int mode) +{ + if(mode == MODE_NN) { + return take_sample_nn(texture, position, dimensions); + } else if (mode == MODE_BI_LINEAR) { + return take_sample_bilinear(texture, position, dimensions); + } else if (mode == MODE_BI_CUBIC) { + return take_sample_bicubic(texture, position, dimensions); + } else { + return uint4(0, 0, 0, 0); + } +} + +/* Texture2D implementations */ + +uint take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +{ + return texture[position % dimensions]; +} + +uint take_sample_bilinear(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint tl = take_sample_nn(texture, position, dimensions); + uint tr = take_sample_nn(texture, position + int2(1, 0), dimensions); + uint bl = take_sample_nn(texture, position + int2(0, 1), dimensions); + uint br = take_sample_nn(texture, position + int2(1, 1), dimensions); + return lerp2d(tl, tr, bl, br, offset); +} + +uint take_sample_bicubic(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint samples[4]; + + for (int i = 0; i < 4; ++i) + { + float ll = take_sample_nn(texture, position + int2(-1, i - 1), dimensions); + float ml = take_sample_nn(texture, position + int2( 0, i - 1), dimensions); + float mr = take_sample_nn(texture, position + int2( 1, i - 1), dimensions); + float rr = take_sample_nn(texture, position + int2( 2, i - 1), dimensions); + samples[i] = interpolate_cubic_uint(ll, ml, mr, rr, offset.x); + } + + return interpolate_cubic_uint(samples[0], samples[1], samples[2], samples[3], offset.y); +} + +uint take_sample(Texture2D texture, float2 position, int2 dimensions, int mode) +{ + if(mode == 0) { + return take_sample_nn(texture, position, dimensions); + } else if (mode == 1) { + return take_sample_bilinear(texture, position, dimensions); + } else if (mode == 2) { + return take_sample_bicubic(texture, position, dimensions); + } else { + return uint(0); + } +} + +/* Texture3D implementations */ + +uint take_sample_nn_3d(Texture3D texture, float2 position, int2 dimensions, int layer) +{ + uint3 read_pos; + read_pos.xy = uint2(position % dimensions); + read_pos.z = layer; + return texture[read_pos]; +} + +/* Texture3D implementations */ + uint4 take_sample_nn_3d(Texture3D texture, float2 position, int2 dimensions, int layer) { uint3 read_pos; @@ -120,19 +217,6 @@ int4 take_sample_bicubic_3d(Texture3D texture, float2 position, int2 dime return interpolate_cubic(samples[0], samples[1], samples[2], samples[3], offset.y); } -uint4 take_sample_uint(Texture2D texture, float2 position, int2 dimensions, int mode) -{ - if(mode == 0) { - return take_sample_nn(texture, position, dimensions); - } else if (mode == 1) { - return take_sample_bilinear(texture, position, dimensions); - } else if (mode == 2) { - return take_sample_bicubic(texture, position, dimensions); - } else { - return uint4(0, 0, 0, 0); - } -} - float circumference_at_distance_from_equator(float vertical_distance_meters) { float half_circumference_km = PI * jobSettings.planet_radius; @@ -183,11 +267,13 @@ LocalizedWarping get_local_image_warping(float2 position_meters, float2 patch_si return result; } -ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimensions, float2 projected_size, float2 terrain_size) +ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, float2 projected_size, float2 terrain_size) { - uint off_w, off_h; - planet_offsets.GetDimensions(off_w, off_h); - uint2 off_sz = uint2(off_w, off_h); + uint2 out_sz; output_color.GetDimensions(out_sz.x, out_sz.y); + uint2 clim_sz; planet_climate.GetDimensions(clim_sz.x, clim_sz.y); + uint2 off_sz; planet_offsets.GetDimensions(off_sz.x, off_sz.y); + uint2 hm_sz; planet_heightmap.GetDimensions(hm_sz.x, hm_sz.y); + uint3 eco_sz; ecosystem_climates.GetDimensions(eco_sz.x, eco_sz.y, eco_sz.z); ProjectedTerrainInfluence result = { float2(0, 0), //float2 temp_humidity @@ -198,49 +284,45 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimens uint3(0,0,0) //uint3 override; }; - float2 pos_meters = pixels_to_meters(position, dimensions); + // NOTE: These pixel dimensions are relative to the climate image + float2 position_px = normal_position * clim_sz; + float2 position_m = pixels_to_meters(position_px, clim_sz); - LocalizedWarping projection_warping = get_local_image_warping(pos_meters, projected_size); - LocalizedWarping physical_warping = get_local_image_warping(pos_meters, terrain_size); + LocalizedWarping projection_warping = get_local_image_warping(position_m, projected_size); + LocalizedWarping physical_warping = get_local_image_warping(position_m, terrain_size); //upper_bound will be the lower number here because image is 0,0 top-left - float upper_bound = position.y - (projection_warping.vertical_delta * dimensions.y); - float lower_bound = position.y + (projection_warping.vertical_delta * dimensions.y); + float upper_bound = position_px.y - (projection_warping.vertical_delta * clim_sz.y); + float lower_bound = position_px.y + (projection_warping.vertical_delta * clim_sz.y); //No wrapping for Y-axis - float search_y_start = clamp(floor(upper_bound), 0, dimensions.y - 1); - float search_y_end = clamp(ceil(lower_bound), 0, dimensions.y - 1); + float search_y_start = clamp(floor(upper_bound), 0, clim_sz.y - 1); + float search_y_end = clamp(ceil(lower_bound), 0, clim_sz.y - 1); int terrain_step = 1; - int pole_distance = dimensions.y / 16; + int pole_distance = clim_sz.y / 16; //TODO Vary terrain step from 1 at pole_distance to TileCount at the pole - if (position.y < pole_distance / 2 || position.y >= dimensions.y - pole_distance / 2) { + if (position_px.y < pole_distance / 2 || position_px.y >= clim_sz.y - pole_distance / 2) { terrain_step = 8; - }else if(position.y < pole_distance || position.y >= dimensions.y - pole_distance) { + }else if(position_px.y < pole_distance || position_px.y >= clim_sz.y - pole_distance) { terrain_step = 4; } - if (round(position.x - 0.5f) == 40 && round(position.y - 0.5f) == 40) { - result.is_override = true; - result.override = uint3(0, 255, 0); - return result; - } - //Search vertically all cells that our projection overlaps with for(float search_y_px = search_y_start; search_y_px <= search_y_end; search_y_px += 1.0f) { //Turn this cells position back into meters, and calculate local distortion size for this row specifically - float2 search_meters = pixels_to_meters(float2(0, search_y_px), dimensions); + float2 search_meters = pixels_to_meters(float2(0, search_y_px), clim_sz); float search_circumference = circumference_at_distance_from_equator(search_meters.y); - float half_projected_width_px = (projected_size.x / 2 / search_circumference) * dimensions.y; + float half_projected_width_px = (projected_size.x / 2 / search_circumference) * clim_sz.y; //Break if the circumference at this pixel is less than a single projection, ie: directly at poles if(search_circumference < projected_size.x) continue; - float row_left_bound = position.x - half_projected_width_px; - float row_right_bound = position.x + half_projected_width_px; + float row_left_bound = position_px.x - half_projected_width_px; + float row_right_bound = position_px.x + half_projected_width_px; float search_x_start = floor(row_left_bound); float search_x_end = ceil(row_right_bound); @@ -251,36 +333,39 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimens //We can use NN here since we are just looking for the ecosystem data uint2 search_pos = uint2(search_x_px, search_y_px); + float2 search_pos_normal = search_pos / float2(clim_sz); - uint4 global_climate = take_sample_uint(planet_climate, search_pos, dimensions, 0); - uint ecosystem_id = uint(global_climate.z / 16); + // Only needed to extract the ecosystem ID at the location we are testing + // This lets us determine which texture we blend with the ground, projecting from this grid position + uint4 local_climate_data = take_sample(planet_climate, search_pos, clim_sz, 0); + uint ecosystem_id = uint(local_climate_data.z / 16); // TODO: Use global random offset data - float offset = take_sample_nn(planet_offsets, float2(search_pos) / dimensions * off_sz, off_sz) / 256.0f; + float offset = take_sample_nn(planet_offsets, search_pos_normal * off_sz, off_sz) / 256.0f; float2 terrain_center = float2(search_x_px, search_y_px) + offset; //Now finally calculate the local distortion at the center of the terrain - float2 terrain_center_m = pixels_to_meters(terrain_center, dimensions.y); + float2 terrain_center_m = pixels_to_meters(terrain_center, clim_sz.y); float terrain_circumference = circumference_at_distance_from_equator(terrain_center_m.y); - float half_terrain_width_projected_px = (projected_size.x / 2 / terrain_circumference) * dimensions.y; - float half_terrain_width_physical_px = (terrain_size.x / 2 / terrain_circumference) * dimensions.y; + float half_terrain_width_projected_px = (projected_size.x / 2 / terrain_circumference) * clim_sz.y; + float half_terrain_width_physical_px = (terrain_size.x / 2 / terrain_circumference) * clim_sz.y; float terrain_left_edge = terrain_center.x - half_terrain_width_projected_px; float terrain_right_edge = terrain_center.x + half_terrain_width_projected_px; - float terrain_top_edge = terrain_center.y - (projection_warping.vertical_delta * dimensions.y); - float terrain_bottom_edge = terrain_center.y + (projection_warping.vertical_delta * dimensions.y); + float terrain_top_edge = terrain_center.y - (projection_warping.vertical_delta * clim_sz.y); + float terrain_bottom_edge = terrain_center.y + (projection_warping.vertical_delta * clim_sz.y); //Reject pixels outside of the terrains projected pixel borders - if (position.x < terrain_left_edge || position.x > terrain_right_edge) + if (position_px.x < terrain_left_edge || position_px.x > terrain_right_edge) continue; - if (position.y < terrain_top_edge || position.y > terrain_bottom_edge) + if (position_px.y < terrain_top_edge || position_px.y > terrain_bottom_edge) continue; //Finally calculate UV coordinates and return result - float terrain_u = ((position.x - terrain_center.x) / half_terrain_width_physical_px / 2) + 0.5f; - float terrain_v = ((position.y - terrain_center.y) / (physical_warping.vertical_delta * dimensions.y * 2)) + 0.5f; - float patch_u = ((position.x - terrain_left_edge) / (half_terrain_width_projected_px * 2)); - float patch_v = ((position.y - terrain_top_edge) / (projection_warping.vertical_delta * dimensions.y * 2)); + float terrain_u = ((position_px.x - terrain_center.x) / half_terrain_width_physical_px / 2) + 0.5f; + float terrain_v = ((position_px.y - terrain_center.y) / (physical_warping.vertical_delta * clim_sz.y * 2)) + 0.5f; + float patch_u = ((position_px.x - terrain_left_edge) / (half_terrain_width_projected_px * 2)); + float patch_v = ((position_px.y - terrain_top_edge) / (projection_warping.vertical_delta * clim_sz.y * 2)); if (terrain_u < 0) terrain_u += 1; if (terrain_v < 0) terrain_v += 1; @@ -299,19 +384,21 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimens float center_distance = sqrt(delta.x * delta.x + delta.y * delta.y) * 1; float local_mask_value = (float)(center_distance > 0.5f ? 0 : cos(center_distance * PI)); - uint2 size = uint2(1024, 1024); - int4 local_eco_data = take_sample_nn_3d(ecosystem_climates, terrain_uv * size, size, ecosystem_id); + int4 local_eco_data = take_sample_nn_3d(ecosystem_climates, terrain_uv * eco_sz.xy, eco_sz.xy, ecosystem_id); float4 local_eco_normalized = (local_eco_data - 127) / 127.0f; // TODO: Heightmaps + float local_eco_height = take_sample_nn_3d(ecosystem_heightmaps, terrain_uv * eco_sz.xy, eco_sz.xy, ecosystem_id); + local_eco_height = (local_eco_height - 32767) / 32767.0f; if (false && (round(search_x_px % 20) == 0 && round(search_y_px % 20) == 0)) { result.is_override = true; //result.override = uint3(255 * terrain_uv.x, 255 * terrain_uv.y, 0) * local_mask_value; - result.override = uint3(planet_offsets[search_pos], 0, 0); + result.override = uint3(offset * 256, 0, 0); return result; } - result.temp_humidity += local_eco_normalized * local_mask_value; + result.temp_humidity += local_eco_normalized.xy * local_mask_value; + result.elevation += local_eco_height * local_mask_value; result.mask_total += local_mask_value; result.num_influences += 1; } @@ -324,62 +411,65 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 position, int2 dimens [numthreads(8,8,1)] void main(uint3 tid : SV_DispatchThreadID) { - uint width; - uint height; - destination.GetDimensions(width, height); - uint2 out_sz = uint2(width, height); + uint2 out_sz; output_color.GetDimensions(out_sz.x, out_sz.y); + uint2 clim_sz; planet_climate.GetDimensions(clim_sz.x, clim_sz.y); + uint2 off_sz; planet_offsets.GetDimensions(off_sz.x, off_sz.y); + uint2 hm_sz; planet_heightmap.GetDimensions(hm_sz.x, hm_sz.y); - uint clim_w, clim_h; - planet_climate.GetDimensions(clim_w, clim_h); - uint2 clim_sz = uint2(clim_w, clim_h); - - uint off_w, off_h; - planet_offsets.GetDimensions(off_w, off_h); - uint2 off_sz = uint2(off_w, off_h); + float terrain_scaling = 1; + float2 projected_size = float2(6000, 6000) * terrain_scaling; + float2 physical_size = float2(4000, 4000) * terrain_scaling; + float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); - float2 normalized_position = tid.xy / float2(out_sz) / jobSettings.render_scale; + // Calculate normalized position in the world (ie: 0,0 = top-left, 1,1 = bottom-right) + float2 normalized_position = tid.xy / float2(clim_sz) / jobSettings.render_scale; normalized_position.xy += jobSettings.offset; - normalized_position.y += 0.5f; + //normalized_position.y += 0.25f; normalized_position = normalized_position % 1; - uint4 local_nn = take_sample_uint(planet_climate, normalized_position * clim_sz, clim_sz, 0); - uint local_ecosystem = local_nn.z / 16; - uint global_offset = take_sample_nn(planet_offsets, normalized_position * off_sz, off_sz); + // Sample global data + uint4 global_climate = take_sample(planet_climate, normalized_position * clim_sz, clim_sz, jobSettings.interpolation); + float global_height = take_sample(planet_heightmap, normalized_position * hm_sz, hm_sz, jobSettings.interpolation); + uint global_offset = take_sample_nn(planet_offsets, normalized_position * off_sz, off_sz); - uint4 local_climate = take_sample_uint(planet_climate, normalized_position * clim_sz, clim_sz, jobSettings.interpolation); - uint4 read = uint4(0, 0, 0, 255); - // uint4 local_climate = planet_climate[normalized_position * clim_sz]; + global_height = (global_height - 32767) / 32767.0f; + global_climate = uint4(global_climate.xy / 2, 0, 0); - local_climate = uint4(local_climate.xy, 0, 0); - float terrain_scaling = 1.5f; - float2 projected_size = float2(6000, 6000) * terrain_scaling; - float2 physical_size = float2(4000, 4000) * terrain_scaling; - float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); + // Calculate influence of all neighboring terrain + ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position, projected_size, physical_size); - ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position * out_sz, out_sz, projected_size, physical_size); + uint4 out_color = uint4(0, 0, 0, 255); + float out_height = global_height * jobSettings.global_terrain_height_influence; // Value if (eco_influence.is_override) { - read.xyz = eco_influence.override; + out_color.xyz = eco_influence.override; } else { if(eco_influence.mask_total > 0) { eco_influence.temp_humidity /= eco_influence.mask_total; - local_climate.yx += eco_influence.temp_humidity * local_influence; - + global_climate.yx += eco_influence.temp_humidity * local_influence; + out_height += eco_influence.elevation * jobSettings.ecosystem_terrain_height_influence; } - uint4 surface_color = take_sample_uint(surface, local_climate.yx / 2, int2(128, 128), jobSettings.interpolation); + uint4 surface_color = take_sample(surface, global_climate.yx, int2(128, 128), jobSettings.interpolation); - read.xyz = surface_color.xyz; + out_color.xyz = surface_color.xyz; } - // Grid rendering - int2 cell_position = tid.xy % jobSettings.render_scale; + if (out_height < jobSettings.ocean_depth) { + out_color.xyz = jobSettings.ocean_color.xyz; + } + + // Squash out_height from meter range to normalized +/- 1.0 range + out_height /= (jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence); + + // DEBUG: Grid rendering + int2 cell_position = int2(normalized_position * out_sz * jobSettings.render_scale) % jobSettings.render_scale; if(false && (cell_position.x == 0 || cell_position.y == 0)) { - read.x = 255; - read.y = 0; - read.z = 0; + out_color.xyz = uint3(255, 0, 0); } - destination[tid.xy] = read; + output_color[tid.xy] = out_color; + //output_heightmap[tid.xy] = uint4(global_height & 0xFF, (global_height & 0xFF00) >> 8, 0, 255); + output_heightmap[tid.xy] = uint4(out_height * 127 + 127, 0, 0, 255); } \ No newline at end of file diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 6fe1dc5..c40f350 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -112,46 +112,53 @@ + + + + + Sample Mode + + + + + + + - + - HLSL + Output Resolution - - - + + + + + + + Display Mode - - + + + + + + - Sample Mode + Display Layer - - + + + - - - - Load - - - - - - - Save - - - -- GitLab From 0af3860796b6aea5bb23ba48b865a048ddc7828f Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 7 Dec 2023 20:12:52 -0500 Subject: [PATCH 08/49] Using custom planet_viewer.py class now for showing the planet. --- starfab/gui/widgets/image_viewer.py | 3 +- starfab/gui/widgets/pages/page_PlanetView.py | 8 +- starfab/gui/widgets/planet_viewer.py | 140 +++++++++++++++++++ starfab/resources/ui/PlanetView.ui | 73 ++++++++-- 4 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 starfab/gui/widgets/planet_viewer.py diff --git a/starfab/gui/widgets/image_viewer.py b/starfab/gui/widgets/image_viewer.py index 82bb3d5..cdb4fa1 100644 --- a/starfab/gui/widgets/image_viewer.py +++ b/starfab/gui/widgets/image_viewer.py @@ -17,7 +17,6 @@ SUPPORTED_IMG_FORMATS = set(Image.EXTENSION.keys()) SUPPORTED_IMG_FORMATS.update(['.' + bytes(_).decode('utf-8') for _ in qtg.QImageReader.supportedImageFormats()]) DDS_CONV_FORMAT = "png" - class QImageViewer(qtw.QGraphicsView): """PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning. Displays a QImage or QPixmap (QImage is internally converted to a QPixmap). @@ -59,7 +58,7 @@ class QImageViewer(qtw.QGraphicsView): self.act_save_as.triggered.connect(self._handle_save_as) # Store a local handle to the scene's current image pixmap. - self.image = qtw.QGraphicsPixmapItem() + self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() self.scene.addItem(self.image) # Scroll bar behaviour. diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 8daa284..fcd5f6b 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -11,6 +11,7 @@ from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInst from starfab.gui import qtw, qtc from starfab.gui.widgets.image_viewer import QImageViewer +from starfab.gui.widgets.planet_viewer import QPlanetViewer from starfab.log import getLogger from starfab.planets.planet import Planet from starfab.planets.data import RenderSettings @@ -37,8 +38,9 @@ class PlanetView(qtw.QWidget): self.outputResolutionComboBox: QComboBox = None self.displayModeComboBox: QComboBox = None self.displayLayerComboBox: QComboBox = None - self.renderOutput: QImageViewer = None + self.renderOutput: QPlanetViewer = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self + print(self.renderOutput) self.starmap = None @@ -115,7 +117,8 @@ class PlanetView(qtw.QWidget): self._update_image(self.last_render.tex_heightmap) def _update_image(self, image: Image): - self.renderOutput.setImage(ImageQt.ImageQt(image), fit=False) + # self.renderOutput.setImage(ImageQt.ImageQt(image), fit=False) + pass def _hack_before_load(self): # Hacky method to support faster dev testing and launching directly in-app @@ -166,6 +169,7 @@ class PlanetView(qtw.QWidget): # img = selected_obj.render(self.get_settings()) self.last_render = self.renderer.render() self._display_layer_changed() + self.renderOutput.update_render(self.last_render) except Exception as ex: logger.exception(ex) diff --git a/starfab/gui/widgets/planet_viewer.py b/starfab/gui/widgets/planet_viewer.py new file mode 100644 index 0000000..79674ad --- /dev/null +++ b/starfab/gui/widgets/planet_viewer.py @@ -0,0 +1,140 @@ +import PySide6 +from PIL import Image +from PIL.ImageQt import ImageQt +from PySide6.QtCore import QPointF, QRect, QRectF, QPoint +from PySide6.QtGui import QPainterPath, QColor, QTransform, QBrush, QPen, Qt, QPainter +from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsEffect + +from starfab.gui import qtc, qtg, qtw +from starfab.gui.widgets.image_viewer import QImageViewer +from starfab.planets.planet_renderer import RenderResult + + +class QPlanetViewer(qtw.QGraphicsView): + def __init__(self, *args, **kwargs): + # Image-space coordinates are always 0-360,000 (* 1000 to map closer to pixel sizes generally) + self._outer_perimeter: QRectF = QRectF(0, 0, 360 * 100, 180 * 100) + + super().__init__(*args, **kwargs) + self._empty = True + + self._zoom = 0 + self._zoom_factor = 1.25 + # we start at a 0 zoom level when the image is changed and we fill the view with it + self._min_zoom = -2 + self._max_zoom = 50 + + self._major_grid_pen = QPen(QColor(255, 255, 255, 255), 50) + self._minor_grid_pen = QPen(QColor(200, 200, 200, 255), 20) + + self.scene = qtw.QGraphicsScene() + self.scene.setBackgroundBrush(QColor(0, 0, 0, 255)) + self.setScene(self.scene) + + self.setTransformationAnchor(qtw.QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(qtw.QGraphicsView.AnchorUnderMouse) + self.setFrameShape(qtw.QFrame.NoFrame) + self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) + + self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() + self.scene.addItem(self.image) + + self.perimeter_path: QPainterPath = QPainterPath() + self._update_perimeter() + + self.perimeter_rect: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) + self.perimeter_rect.setZValue(1000) + self.perimeter_rect.setGraphicsEffect(GridEffect(self._major_grid_pen, self._minor_grid_pen)) + self.scene.addItem(self.perimeter_rect) + + image_padding = 10000 + # Add some extra padding around the planet bounds. + scene_area = QRectF(self._outer_perimeter.x() - image_padding, + self._outer_perimeter.y() - image_padding, + self._outer_perimeter.width() + 2 * image_padding, + self._outer_perimeter.height() + 2 * image_padding) + self.scene.setSceneRect(scene_area) + self.fitInView(self.image) + + def update_render(self, new_render: RenderResult): + image: Image = new_render.tex_color + self.setImage(ImageQt(image)) + + def _update_perimeter(self): + self.perimeter_path.clear() + self.perimeter_path.addRect(self._outer_perimeter) + + def setImage(self, image: Image.Image): + """Set the scene's current image pixmap to the input QImage or QPixmap. + Raises a RuntimeError if the input image has type other than QImage or QPixmap. + :type image: QImage | QPixmap + :type fit: bool + """ + self._zoom = 0 + if isinstance(image, qtg.QPixmap): + pixmap = image + elif isinstance(image, qtg.QImage): + pixmap = qtg.QPixmap.fromImage(image) + else: + raise ValueError( + "QImageViewer.setImage: Argument must be a QImage or QPixmap." + ) + if pixmap and not pixmap.isNull(): + self._empty = False + self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) + self.image.setPixmap(pixmap) + else: + self._empty = True + self.setDragMode(qtw.QGraphicsView.NoDrag) + self.image.setPixmap(qtg.QPixmap()) + width_scale = self._outer_perimeter.width() / image.width() + height_scale = self._outer_perimeter.height() / image.height() + image_transform: QTransform = QTransform\ + .fromScale(width_scale, height_scale) \ + .translate(self._outer_perimeter.x(), self._outer_perimeter.y()) + self.image.setTransform(image_transform) + + self.fitInView(self.perimeter_rect, Qt.KeepAspectRatio) + + def hasImage(self): + """Returns whether or not the scene contains an image pixmap.""" + return not self._empty + + def wheelEvent(self, event): + if self.hasImage(): + factor = 0 + if event.angleDelta().y() > 0: + if self._zoom < self._max_zoom: + factor = self._zoom_factor + self._zoom += 1 + else: + if self._zoom > self._min_zoom: + factor = 1 / self._zoom_factor + self._zoom -= 1 + if factor: + self.scale(factor, factor) + + +class GridEffect(QGraphicsEffect): + def __init__(self, primary_pen: QPen, secondary_pen: QPen, *args, **kwargs): + super().__init__(*args, **kwargs) + self.primary_pen = primary_pen + self.secondary_pen = secondary_pen + + def draw(self, painter: QPainter) -> None: + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference) + self._draw_lines(painter, self.primary_pen, True) + self._draw_lines(painter, self.secondary_pen, False) + + @staticmethod + def _draw_lines(painter: QPainter, pen: QPen, primary: bool): + painter.setPen(pen) + for lon in range(15, 360, 15): + is_primary = lon % 45 == 0 + if (is_primary and primary) or (not is_primary and not primary): + painter.drawLine(lon * 100, 0, lon * 100, 180 * 100) + + for lat in range(15, 180, 15): + is_primary = lat % 45 == 0 + if (is_primary and primary) or (not is_primary and not primary): + painter.drawLine(0, lat * 100, 360 * 100, lat * 100) diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index c40f350..9760bd7 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -112,7 +112,6 @@ - @@ -123,7 +122,6 @@ - @@ -134,7 +132,6 @@ - @@ -145,7 +142,6 @@ - @@ -156,9 +152,68 @@ - + + + + Enable Grid + + + + + + + true + + + + + + + Enable Crosshair + + + + + + + true + + + + + + + + 16777215 + 250 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + + + + + + + + @@ -191,7 +246,7 @@ 3 - + @@ -201,9 +256,9 @@ - QImageViewer - QWidget -
starfab.gui.widgets.image_viewer
+ QPlanetViewer + QGraphicsView +
starfab.gui.widgets.planet_viewer
-- GitLab From d43b0f4ae4cfa80e5e0b91bb6d6ceeee9909fde7 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 11 Dec 2023 02:01:00 -0500 Subject: [PATCH 09/49] A ton of change that add a ton of functionality, namely a draggable render window. --- starfab/gui/widgets/pages/page_PlanetView.py | 84 ++++- starfab/gui/widgets/planet_viewer.py | 318 ++++++++++++++----- starfab/planets/planet.py | 2 - starfab/planets/planet_renderer.py | 67 +++- starfab/resources/ui/PlanetView.ui | 7 + 5 files changed, 364 insertions(+), 114 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index fcd5f6b..dc3938d 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -2,9 +2,9 @@ import io from typing import Union from PIL import ImageQt, Image -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QPointF from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QComboBox, QPushButton +from PySide6.QtWidgets import QComboBox, QPushButton, QLabel, QCheckBox from qtpy import uic from scdatatools import StarCitizen from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance @@ -18,6 +18,7 @@ from starfab.planets.data import RenderSettings from starfab.planets.ecosystem import EcoSystem from starfab.planets.planet_renderer import PlanetRenderer, RenderResult from starfab.resources import RES_PATH +from starfab.settings import settings from pathlib import Path @@ -28,9 +29,8 @@ class PlanetView(qtw.QWidget): def __init__(self, sc): super().__init__(parent=None) - self.loadButton: QPushButton = None - self.saveButton: QPushButton = None self.renderButton: QPushButton = None + self.exportButton: QPushButton = None self.planetComboBox: QComboBox = None self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None @@ -39,12 +39,15 @@ class PlanetView(qtw.QWidget): self.displayModeComboBox: QComboBox = None self.displayLayerComboBox: QComboBox = None self.renderOutput: QPlanetViewer = None + self.enableGridCheckBox: QCheckBox = None + self.enableCrosshairCheckBox: QCheckBox = None + self.lbl_planetDetails: QLabel = None + self.lbl_currentStatus: QLabel = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self - print(self.renderOutput) self.starmap = None - self.renderer = PlanetRenderer((4096, 2048)) + self.renderer = PlanetRenderer((2048, 1024)) self.last_render: Union[None, RenderResult] = None self.renderResolutionComboBox.setModel(self.create_model([ @@ -57,12 +60,14 @@ class PlanetView(qtw.QWidget): ("64px per tile", 64), ("128px per tile", 128) ])) + self.renderResolutionComboBox.currentIndexChanged.connect(self._render_scale_changed) self.coordinateSystemComboBox.setModel(self.create_model([ ("NASA Format (0/360deg) - Community Standard", "NASA"), ("Earth Format (-180/180deg) Shifted", "EarthShifted"), ("Earth Format (-180/180deg) Unshifted", "EarthUnShifted") ])) + self.coordinateSystemComboBox.currentIndexChanged.connect(self._display_coordinate_system_changed) self.sampleModeComboBox.setModel(self.create_model([ ("Nearest Neighbor", 0), @@ -100,21 +105,42 @@ class PlanetView(qtw.QWidget): self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) self.renderButton.clicked.connect(self._do_render) + self.exportButton.clicked.connect(self._do_export) + self.exportButton.setEnabled(False) + self.renderOutput.mouse_moved.connect(self._do_mouse_moved) + self.enableGridCheckBox.stateChanged.connect(self.renderOutput.set_grid_enabled) + self.enableCrosshairCheckBox.stateChanged.connect(self.renderOutput.set_crosshair_enabled) + + self.renderer.set_settings(self.get_settings()) + + def _render_scale_changed(self): + new_scale = self.renderResolutionComboBox.currentData(role=Qt.UserRole) + self.renderer.settings.resolution = new_scale + self._update_planet_viewer() def _display_resolution_changed(self): new_resolution = self.outputResolutionComboBox.currentData(role=Qt.UserRole) self.renderer.set_resolution(new_resolution) + self._update_planet_viewer() def _display_mode_changed(self): new_transform = self.displayModeComboBox.currentData(role=Qt.UserRole) self.renderOutput.image.setTransformationMode(new_transform) + def _display_coordinate_system_changed(self): + new_coordinate_mode = self.coordinateSystemComboBox.currentData(role=Qt.UserRole) + self.renderer.settings.coordinate_mode = new_coordinate_mode + self._update_planet_viewer() + + def _update_planet_viewer(self): + planet_bounds = self.renderer.get_outer_bounds() + render_bounds = self.renderOutput.get_render_coords() + self.renderOutput.update_bounds(planet_bounds, + self.renderer.get_bounds_for_render(render_bounds.topLeft())) + def _display_layer_changed(self): layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) - if layer == "surface": - self._update_image(self.last_render.tex_color) - elif layer == "heightmap": - self._update_image(self.last_render.tex_heightmap) + self.renderOutput.update_visible_layer(layer) def _update_image(self, image: Image): # self.renderOutput.setImage(ImageQt.ImageQt(image), fit=False) @@ -141,7 +167,7 @@ class PlanetView(qtw.QWidget): return model def shader_path(self) -> Path: - return Path(__file__).parent / '../../../planets/shader.hlsl' + return Path(__file__) / '../../../../planets/shader.hlsl' def _get_shader(self): with io.open(self.shader_path(), "r") as shader: @@ -157,22 +183,48 @@ class PlanetView(qtw.QWidget): def _do_render(self): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) - print(selected_obj) selected_obj.load_data() - print("Done loading planet data") self.renderer.set_planet(selected_obj) self.renderer.set_settings(self.get_settings()) # TODO: Deal with buffer directly try: - # img = selected_obj.render(self.get_settings()) - self.last_render = self.renderer.render() + layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) + render_bounds = self.renderOutput.get_render_coords() + self.last_render = self.renderer.render(render_bounds.topLeft()) self._display_layer_changed() - self.renderOutput.update_render(self.last_render) + self.renderOutput.update_render(self.last_render, layer) + self.exportButton.setEnabled(True) except Exception as ex: logger.exception(ex) + def _do_export(self): + prev_dir = settings.value("exportDirectory") + title = "Save Render to..." + edir = qtw.QFileDialog.getSaveFileName(self, title, + dir=f"{self.renderer.planet.oc.entity_name}.png", + filter="PNG Image (*.png)") + filename, filter = edir + if filename: + self.last_render.tex_color.save(filename, format="png") + + def _do_mouse_moved(self, new_position: QPointF): + # TODO: Do coordinate conversion inside of planet_viewer + lon = new_position.x() + lat = new_position.y() + self.lbl_currentStatus.setText(f"Mouse Position:\n" + f"\tLat: {self.coord_to_dms(lat)}\n" + f"\tLon: {self.coord_to_dms(lon)}") + + @staticmethod + def coord_to_dms(coord): + degrees = int(coord) + minutes_float = (coord - degrees) * 60 + minutes = int(minutes_float) + seconds = (minutes_float - minutes) * 60 + return f"{degrees}° {minutes}' {seconds:.2f}" + def _handle_datacore_unloading(self): if self.starmap is not None: del self.starmap diff --git a/starfab/gui/widgets/planet_viewer.py b/starfab/gui/widgets/planet_viewer.py index 79674ad..e25d77d 100644 --- a/starfab/gui/widgets/planet_viewer.py +++ b/starfab/gui/widgets/planet_viewer.py @@ -1,8 +1,10 @@ +from typing import Union, cast + import PySide6 from PIL import Image from PIL.ImageQt import ImageQt -from PySide6.QtCore import QPointF, QRect, QRectF, QPoint -from PySide6.QtGui import QPainterPath, QColor, QTransform, QBrush, QPen, Qt, QPainter +from PySide6.QtCore import QPointF, QRect, QRectF, QPoint, Signal, QSizeF +from PySide6.QtGui import QPainterPath, QColor, QTransform, QBrush, QPen, Qt, QPainter, QMouseEvent, QPixmap, QImage from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsEffect from starfab.gui import qtc, qtg, qtw @@ -11,130 +13,278 @@ from starfab.planets.planet_renderer import RenderResult class QPlanetViewer(qtw.QGraphicsView): - def __init__(self, *args, **kwargs): - # Image-space coordinates are always 0-360,000 (* 1000 to map closer to pixel sizes generally) - self._outer_perimeter: QRectF = QRectF(0, 0, 360 * 100, 180 * 100) + mouse_moved: Signal = Signal(QPointF) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._empty = True + try: + # Image-space coordinates are always 0-360,000 (* 1000 to map closer to pixel sizes generally) + self._scale_factor: int = 100 + self._outer_perimeter: QRectF = QRectF() + self._render_perimeter: QRectF = QRectF() + self._render_result: None | RenderResult = None + + self._empty = True + self._zoom = 0 + self._zoom_factor = 1.25 + # we start at a 0 zoom level when the image is changed and we fill the view with it + self._min_zoom = -2 + self._max_zoom = 50 + + self._major_grid_pen = QPen(QColor(255, 255, 255, 255), 50) + self._minor_grid_pen = QPen(QColor(200, 200, 200, 255), 20) + self._crosshair_pen = QPen(QColor(255, 0, 0, 255), 20) - self._zoom = 0 - self._zoom_factor = 1.25 - # we start at a 0 zoom level when the image is changed and we fill the view with it - self._min_zoom = -2 - self._max_zoom = 50 + self.setCursor(Qt.CrossCursor) - self._major_grid_pen = QPen(QColor(255, 255, 255, 255), 50) - self._minor_grid_pen = QPen(QColor(200, 200, 200, 255), 20) + self.scene = qtw.QGraphicsScene() + self.scene.setBackgroundBrush(QColor(0, 0, 0, 255)) + self.setScene(self.scene) - self.scene = qtw.QGraphicsScene() - self.scene.setBackgroundBrush(QColor(0, 0, 0, 255)) - self.setScene(self.scene) + self.setTransformationAnchor(qtw.QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(qtw.QGraphicsView.AnchorUnderMouse) + self.setFrameShape(qtw.QFrame.NoFrame) + self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) + + self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() + self.scene.addItem(self.image) + + self.perimeter_path: QPainterPath = QPainterPath() + self.render_outline: QPainterPath = QPainterPath() + self.perimeter_rect: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) + self.crosshair_overlay: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) + self.render_window: QGraphicsPathItem = QGraphicsPathItem(self.render_outline) + self.perimeter_effect: None | GridEffect = None + self.crosshair_overlay_effect: None | CrosshairEffect = None + self.update_bounds(QRectF(0, -90, 360, 180), QRectF(0, -90, 180, 90)) - self.setTransformationAnchor(qtw.QGraphicsView.AnchorUnderMouse) - self.setResizeAnchor(qtw.QGraphicsView.AnchorUnderMouse) - self.setFrameShape(qtw.QFrame.NoFrame) - self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) + self._grid_enabled = False + self.perimeter_rect.setZValue(1000) + self.set_grid_enabled(True) + self.scene.addItem(self.perimeter_rect) - self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() - self.scene.addItem(self.image) + self._crosshair_enabled = False + self.crosshair_overlay.setZValue(2000) + self.set_crosshair_enabled(True) + self.scene.addItem(self.crosshair_overlay) - self.perimeter_path: QPainterPath = QPainterPath() - self._update_perimeter() + self.render_window.setPen(QPen(QColor(0, 255, 0, 255), 20)) + self.render_window.setZValue(3000) + self.scene.addItem(self.render_window) + self.render_window_dragging: bool = False + self.render_window_drag_pos: None | QPointF = None - self.perimeter_rect: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) - self.perimeter_rect.setZValue(1000) - self.perimeter_rect.setGraphicsEffect(GridEffect(self._major_grid_pen, self._minor_grid_pen)) - self.scene.addItem(self.perimeter_rect) + self.setMouseTracking(True) + self.image.setCursor(Qt.CrossCursor) + self.render_window.setCursor(Qt.SizeAllCursor) + self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) + self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) + except Exception as ex: + print(ex) + raise ex + + def update_bounds(self, bounding_rect: QRectF, render_rect: QRectF): + self._outer_perimeter: QRectF = QRectF(bounding_rect.topLeft() * self._scale_factor, + bounding_rect.size() * self._scale_factor) + self._render_perimeter: QRectF = QRectF(render_rect.topLeft() * self._scale_factor, + render_rect.size() * self._scale_factor) + + # Update paths + self.perimeter_path.clear() + self.perimeter_path.addRect(self._outer_perimeter) + self.render_outline.clear() + self.render_outline.addRect(self._render_perimeter) - image_padding = 10000 # Add some extra padding around the planet bounds. + image_padding = 10 * self._scale_factor scene_area = QRectF(self._outer_perimeter.x() - image_padding, self._outer_perimeter.y() - image_padding, self._outer_perimeter.width() + 2 * image_padding, self._outer_perimeter.height() + 2 * image_padding) self.scene.setSceneRect(scene_area) - self.fitInView(self.image) - def update_render(self, new_render: RenderResult): - image: Image = new_render.tex_color - self.setImage(ImageQt(image)) + if self.perimeter_effect: + self.perimeter_effect.planet_bounds = self._outer_perimeter + if self.crosshair_overlay_effect: + self.crosshair_overlay_effect.planet_bounds = self._outer_perimeter + self.perimeter_rect.setPath(self.perimeter_path) + self.crosshair_overlay.setPath(self.perimeter_path) + self.render_window.setPath(self.render_outline) + self.perimeter_rect.update() + self.crosshair_overlay.update() + self.scene.update() + self.update() - def _update_perimeter(self): - self.perimeter_path.clear() - self.perimeter_path.addRect(self._outer_perimeter) + def get_render_coords(self): + return QRectF(self._render_perimeter.topLeft() / self._scale_factor, + self._render_perimeter.size() / self._scale_factor) + + def mousePressEvent(self, event: PySide6.QtGui.QMouseEvent) -> None: + image_space_pos = self.mapToScene(event.pos()) + global_coordinates = self.image_to_coordinates(image_space_pos) - def setImage(self, image: Image.Image): - """Set the scene's current image pixmap to the input QImage or QPixmap. - Raises a RuntimeError if the input image has type other than QImage or QPixmap. - :type image: QImage | QPixmap - :type fit: bool - """ - self._zoom = 0 - if isinstance(image, qtg.QPixmap): - pixmap = image - elif isinstance(image, qtg.QImage): - pixmap = qtg.QPixmap.fromImage(image) + if event.button() == Qt.RightButton and \ + self._render_perimeter.contains(image_space_pos): + self.render_window_dragging = True + self.render_window_drag_pos = image_space_pos else: - raise ValueError( - "QImageViewer.setImage: Argument must be a QImage or QPixmap." - ) - if pixmap and not pixmap.isNull(): - self._empty = False - self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) - self.image.setPixmap(pixmap) + super().mousePressEvent(event) + + def mouseMoveEvent(self, event: QMouseEvent): + image_space_pos = self.mapToScene(event.pos()) + global_coordinates = self.image_to_coordinates(image_space_pos) + + if self.render_window_dragging: + delta = image_space_pos - self.render_window_drag_pos + drag_bounds = QRectF(self._outer_perimeter.topLeft(), + self._outer_perimeter.size() - self._render_perimeter.size()) + + self.render_window_drag_pos = image_space_pos + self._render_perimeter.translate(delta) + + final_position_x = max(drag_bounds.left(), min(drag_bounds.right(), self._render_perimeter.x())) + final_position_y = max(drag_bounds.top(), min(drag_bounds.bottom(), self._render_perimeter.y())) + self._render_perimeter.setRect(final_position_x, final_position_y, + self._render_perimeter.width(), self._render_perimeter.height()) + + self.render_outline.clear() + self.render_outline.addRect(self._render_perimeter) + self.render_window.setPath(self.render_outline) else: - self._empty = True - self.setDragMode(qtw.QGraphicsView.NoDrag) - self.image.setPixmap(qtg.QPixmap()) - width_scale = self._outer_perimeter.width() / image.width() - height_scale = self._outer_perimeter.height() / image.height() - image_transform: QTransform = QTransform\ - .fromScale(width_scale, height_scale) \ - .translate(self._outer_perimeter.x(), self._outer_perimeter.y()) - self.image.setTransform(image_transform) + super().mouseMoveEvent(event) + + self.mouse_moved.emit(global_coordinates) + if self.crosshair_overlay_effect: + self.crosshair_overlay_effect.update_mouse_position(image_space_pos) + self.crosshair_overlay.update(self._outer_perimeter) + + def mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None: + image_space_pos = self.mapToScene(event.pos()) + global_coordinates = self.image_to_coordinates(image_space_pos) + + self.render_window_dragging = False + + super().mouseReleaseEvent(event) + + def image_to_coordinates(self, image_position: QPointF) -> QPointF: + return QPointF(image_position.x() / self._scale_factor, image_position.y() / self._scale_factor) + + def update_render(self, new_render: RenderResult, layer: str): + self._render_result = new_render + self.update_visible_layer(layer) + + def update_visible_layer(self, layer: str): + if not self._render_result: + return + + s = self._scale_factor + img = None + if layer == "surface": + img = self._render_result.tex_color + elif layer == "heightmap": + img = self._render_result.tex_heightmap + else: + raise Exception(f"Unknown layer: {layer}") + + qt_image: QPixmap = QPixmap.fromImage(ImageQt(img)) + width_scale = self._render_result.coordinate_bounds.width() / qt_image.width() + self.image.setPixmap(qt_image) + self.image.setPos(self._render_result.coordinate_bounds.left() * s, + self._render_result.coordinate_bounds.top() * s) + # TODO: Better support non-standard render sizes + self.image.setScale(width_scale * s) - self.fitInView(self.perimeter_rect, Qt.KeepAspectRatio) + def set_grid_enabled(self, enabled: bool): + self._grid_enabled = enabled + if enabled: + # Need to rebuild each time as it gets disposed of + self.perimeter_effect = GridEffect(self._major_grid_pen, self._minor_grid_pen, + self._scale_factor, self._outer_perimeter) + self.perimeter_rect.setGraphicsEffect(self.perimeter_effect) + else: + self.perimeter_rect.setGraphicsEffect(None) + self.perimeter_effect = None + self.update() + self.scene.update() - def hasImage(self): - """Returns whether or not the scene contains an image pixmap.""" - return not self._empty + def set_crosshair_enabled(self, enabled: bool): + self._crosshair_enabled = enabled + if enabled: + # Need to rebuild each time as it gets disposed of + self.crosshair_overlay_effect = CrosshairEffect(self._crosshair_pen, self._outer_perimeter) + self.crosshair_overlay.setGraphicsEffect(self.crosshair_overlay_effect) + else: + self.crosshair_overlay.setGraphicsEffect(None) + self.crosshair_overlay_effect = None + self.update() + self.scene.update() def wheelEvent(self, event): - if self.hasImage(): - factor = 0 - if event.angleDelta().y() > 0: - if self._zoom < self._max_zoom: - factor = self._zoom_factor - self._zoom += 1 - else: - if self._zoom > self._min_zoom: - factor = 1 / self._zoom_factor - self._zoom -= 1 - if factor: - self.scale(factor, factor) + factor = 0 + if event.angleDelta().y() > 0: + if self._zoom < self._max_zoom: + factor = self._zoom_factor + self._zoom += 1 + else: + if self._zoom > self._min_zoom: + factor = 1 / self._zoom_factor + self._zoom -= 1 + if factor: + self.scale(factor, factor) + + +class CrosshairEffect(QGraphicsEffect): + def __init__(self, pen: QPen, region: QRectF, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mouse_position: Union[QPointF, None] = None + self.pen = pen + self.planet_bounds = region + + def update_mouse_position(self, pos: QPointF): + self.mouse_position = pos + + def remove_mouse(self): + self.mouse_position = None + + def draw(self, painter: QPainter) -> None: + if not self.mouse_position: + return + + vp = self.planet_bounds + painter.setPen(self.pen) + painter.drawLine(vp.left(), self.mouse_position.y(), vp.right(), self.mouse_position.y()) + painter.drawLine(self.mouse_position.x(), vp.top(), self.mouse_position.x(), vp.bottom()) + painter.drawRect(vp) class GridEffect(QGraphicsEffect): - def __init__(self, primary_pen: QPen, secondary_pen: QPen, *args, **kwargs): + def __init__(self, + primary_pen: QPen, secondary_pen: QPen, + scale_factor: int, planet_bounds: QRectF, + *args, **kwargs): super().__init__(*args, **kwargs) self.primary_pen = primary_pen self.secondary_pen = secondary_pen + self.scale_factor = scale_factor + self.planet_bounds = planet_bounds def draw(self, painter: QPainter) -> None: painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference) self._draw_lines(painter, self.primary_pen, True) self._draw_lines(painter, self.secondary_pen, False) - @staticmethod - def _draw_lines(painter: QPainter, pen: QPen, primary: bool): + def _draw_lines(self, painter: QPainter, pen: QPen, primary: bool): painter.setPen(pen) + xoff = self.planet_bounds.x() + yoff = self.planet_bounds.y() for lon in range(15, 360, 15): is_primary = lon % 45 == 0 if (is_primary and primary) or (not is_primary and not primary): - painter.drawLine(lon * 100, 0, lon * 100, 180 * 100) + painter.drawLine(lon * self.scale_factor + xoff, self.planet_bounds.top(), + lon * self.scale_factor + xoff, self.planet_bounds.bottom()) for lat in range(15, 180, 15): is_primary = lat % 45 == 0 if (is_primary and primary) or (not is_primary and not primary): - painter.drawLine(0, lat * 100, 360 * 100, lat * 100) + painter.drawLine(self.planet_bounds.left(), lat * self.scale_factor + yoff, + self.planet_bounds.right(), lat * self.scale_factor + yoff) diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index 08c8e8f..03c8273 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -52,8 +52,6 @@ class Planet: ocean_mat_chunks = ChunkFile(ocean_mat_info) for chunk in ocean_mat_chunks.chunks: if isinstance(chunk, CryXMLBChunk): - print("XML Chunk!") - print(chunk.dict()) diffuse_path = chunk.dict()["Material"] print(diffuse_path) except Exception as ex: diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index df938e1..6b8b178 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -3,6 +3,7 @@ import math from typing import Union, Callable, Tuple, cast from PIL import Image +from PySide6.QtCore import QRectF, QPointF, QSizeF, QSize from compushady import Texture2D, Compute, Resource, HEAP_UPLOAD, Buffer, Texture3D, HEAP_READBACK from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT from compushady.shaders import hlsl @@ -13,10 +14,19 @@ from starfab.planets.ecosystem import EcoSystem class RenderResult: - def __init__(self, settings: RenderJobSettings, tex_color: Image, tex_heightmap: Image): + def __init__(self, + settings: RenderJobSettings, + tex_color: Image.Image, + tex_heightmap: Image.Image, + splat_dimensions: Tuple[float, float], + coordinate_bounds_planet: QRectF, + coordinate_bounds: QRectF): self.settings = settings - self.tex_color = tex_color - self.tex_heightmap = tex_heightmap + self.tex_color: Image.Image = tex_color + self.tex_heightmap: Image.Image = tex_heightmap + self.splat_resolution = splat_dimensions + self.coordinate_bounds = coordinate_bounds + self.coordinate_bounds_planet = coordinate_bounds_planet class PlanetRenderer: @@ -25,7 +35,7 @@ class PlanetRenderer: self.planet: Union[None, Planet] = None self.settings: Union[None, RenderSettings] = None self.gpu_resources: dict[str, Resource] = {} - self.render_resolution: Tuple[int, int] = buffer_resolution + self.render_resolution: QSize() = QSize(*buffer_resolution) self._create_gpu_output_resources() @@ -42,22 +52,50 @@ class PlanetRenderer: def set_resolution(self, new_dimensions: Tuple[int, int]): self._cleanup_gpu_output_resources() - self.render_resolution = new_dimensions + self.render_resolution = QSizeF(*new_dimensions) self._create_gpu_output_resources() - def render(self) -> RenderResult: + def get_outer_bounds(self) -> QRectF: + base_coordinate: QPointF = QPointF(0, -90) + if self.settings.coordinate_mode != "NASA": + base_coordinate.setX(-180) + + return QRectF(base_coordinate, QSize(360, 180)) + + def get_bounds_for_render(self, render_coords: QPointF) -> QRectF: + width_norm = self.render_resolution.width() / self.settings.resolution / self.planet.tile_count / 2 + height_norm = self.render_resolution.height() / self.settings.resolution / self.planet.tile_count + render_size_degrees: QSizeF = QSizeF(width_norm * 360, height_norm * 180) + + return QRectF(render_coords, render_size_degrees) + + def get_normalized_from_coordinates(self, coordinates: QPointF) -> QPointF: + bounds = self.get_outer_bounds() + return QPointF((coordinates.x() - bounds.x()) / bounds.width(), + (coordinates.y() - bounds.y()) / bounds.height()) + + def render(self, render_coords: QPointF) -> RenderResult: job_s = RenderJobSettings() if not self.planet: raise Exception("Planet not set yet!") + # offset_x/y are be normalized between 0-1 + norm_coords = self.get_normalized_from_coordinates(render_coords) + job_s.offset_x = norm_coords.x() + job_s.offset_y = norm_coords.y() + + # In NASA Mode we are shifting only our sampling offset + # so that [0,0] render coordinates equals [0deg, 360deg] world coordinates if self.settings.coordinate_mode == "NASA": - job_s.offset_x = 0.5 + job_s.offset_x += 0.5 + # In EarthShifted Mode we are elif self.settings.coordinate_mode == "EarthShifted": - job_s.offset_x = 0.5 - elif self.settings.coordinate_mode == "EarchUnshifted": + job_s.offset_x += 0.5 + elif self.settings.coordinate_mode == "EarthUnShifted": job_s.offset_x = 0 + job_s.offset_x = job_s.offset_x % 1.0 job_s.interpolation = self.settings.interpolation job_s.render_scale_x = self.settings.resolution * 2 job_s.render_scale_y = self.settings.resolution @@ -69,7 +107,7 @@ class PlanetRenderer: computer = self._get_computer() - computer.dispatch(self.render_resolution[0] // 8, self.render_resolution[1] // 8, 1) + computer.dispatch(self.render_resolution.width() // 8, self.render_resolution.height() // 8, 1) # TODO: Keep this around and render multiple tiles with the same Compute obj out_color: Image = self._read_frame("output_color") @@ -77,7 +115,11 @@ class PlanetRenderer: del computer - return RenderResult(job_s, out_color, out_heightmap) + planet_bouds = self.get_outer_bounds() + render_bounds = self.get_bounds_for_render(render_coords) + + return RenderResult(job_s, out_color, out_heightmap, self.render_resolution, + planet_bouds, render_bounds) def _read_frame(self, resource_name: str) -> Image: readback: Buffer = cast(Buffer, self.gpu_resources['readback']) @@ -199,7 +241,8 @@ class PlanetRenderer: # TODO: Support variable size output buffers. For now just render to fixed size and stitch # Also wasting a bit of space having the heightmap 32BPP when we only need 16 # but this makes things a lot easier to work with elsewhere :^) - (out_w, out_h) = self.render_resolution + out_w = self.render_resolution.width() + out_h = self.render_resolution.height() output_color_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) output_heightmap_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 9760bd7..ef9ba45 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -221,6 +221,13 @@
+ + + + Export + + +
-- GitLab From 08f6bcbcd33a52653dc72fb22d00c117bb09a650 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 11 Dec 2023 03:00:01 -0500 Subject: [PATCH 10/49] Some minor cleanup. --- starfab/gui/widgets/pages/page_PlanetView.py | 3 +++ starfab/gui/widgets/planet_viewer.py | 3 ++- starfab/planets/planet_renderer.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index dc3938d..c46c39c 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -133,6 +133,9 @@ class PlanetView(qtw.QWidget): self._update_planet_viewer() def _update_planet_viewer(self): + if not self.renderer.planet: + return + planet_bounds = self.renderer.get_outer_bounds() render_bounds = self.renderOutput.get_render_coords() self.renderOutput.update_bounds(planet_bounds, diff --git a/starfab/gui/widgets/planet_viewer.py b/starfab/gui/widgets/planet_viewer.py index e25d77d..98bdc8a 100644 --- a/starfab/gui/widgets/planet_viewer.py +++ b/starfab/gui/widgets/planet_viewer.py @@ -77,7 +77,8 @@ class QPlanetViewer(qtw.QGraphicsView): self.setMouseTracking(True) self.image.setCursor(Qt.CrossCursor) self.render_window.setCursor(Qt.SizeAllCursor) - self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) + self.fitInView(self._render_perimeter, Qt.KeepAspectRatio) + self.ensureVisible(QRectF(0, 0, 100, 100)) self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) except Exception as ex: print(ex) diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 6b8b178..d941266 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -32,8 +32,8 @@ class RenderResult: class PlanetRenderer: def __init__(self, buffer_resolution: Tuple[int, int]): - self.planet: Union[None, Planet] = None - self.settings: Union[None, RenderSettings] = None + self.planet: None | Planet = None + self.settings: None | RenderSettings = None self.gpu_resources: dict[str, Resource] = {} self.render_resolution: QSize() = QSize(*buffer_resolution) @@ -52,7 +52,7 @@ class PlanetRenderer: def set_resolution(self, new_dimensions: Tuple[int, int]): self._cleanup_gpu_output_resources() - self.render_resolution = QSizeF(*new_dimensions) + self.render_resolution = QSize(*new_dimensions) self._create_gpu_output_resources() def get_outer_bounds(self) -> QRectF: -- GitLab From c5ecffbc2f49339c78137ef9f71616c3b85fc85e Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 11 Dec 2023 22:06:32 -0500 Subject: [PATCH 11/49] Add missing action to mainwindow.ui --- starfab/resources/ui/mainwindow.ui | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/starfab/resources/ui/mainwindow.ui b/starfab/resources/ui/mainwindow.ui index 13d36d4..bd18308 100644 --- a/starfab/resources/ui/mainwindow.ui +++ b/starfab/resources/ui/mainwindow.ui @@ -405,6 +405,14 @@ QToolButton { Mobi
+ + + true + + + Planets + +
-- GitLab From 8b880bf9c389176e1dc598325dc04042183bd3cd Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 14 Dec 2023 00:18:43 -0500 Subject: [PATCH 12/49] A ton of separating layers in the planet renderer. --- starfab/gui/widgets/pages/page_PlanetView.py | 43 ++-- starfab/gui/widgets/planets/__init__.py | 0 .../gui/widgets/planets/crosshair_overlay.py | 29 +++ starfab/gui/widgets/planets/effect_overlay.py | 42 ++++ starfab/gui/widgets/planets/grid_overlay.py | 36 ++++ .../widgets/{ => planets}/planet_viewer.py | 185 +++++------------- starfab/planets/planet.py | 18 ++ starfab/resources/ui/PlanetView.ui | 26 ++- 8 files changed, 228 insertions(+), 151 deletions(-) create mode 100644 starfab/gui/widgets/planets/__init__.py create mode 100644 starfab/gui/widgets/planets/crosshair_overlay.py create mode 100644 starfab/gui/widgets/planets/effect_overlay.py create mode 100644 starfab/gui/widgets/planets/grid_overlay.py rename starfab/gui/widgets/{ => planets}/planet_viewer.py (51%) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index c46c39c..85dece9 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,17 +1,16 @@ import io from typing import Union -from PIL import ImageQt, Image -from PySide6.QtCore import Qt, QPointF +from PIL import Image +from PySide6.QtCore import Qt, QPointF, QRectF from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QComboBox, QPushButton, QLabel, QCheckBox +from PySide6.QtWidgets import QComboBox, QPushButton, QLabel, QCheckBox, QListView from qtpy import uic from scdatatools import StarCitizen from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance from starfab.gui import qtw, qtc -from starfab.gui.widgets.image_viewer import QImageViewer -from starfab.gui.widgets.planet_viewer import QPlanetViewer +from starfab.gui.widgets.planets.planet_viewer import QPlanetViewer from starfab.log import getLogger from starfab.planets.planet import Planet from starfab.planets.data import RenderSettings @@ -41,6 +40,8 @@ class PlanetView(qtw.QWidget): self.renderOutput: QPlanetViewer = None self.enableGridCheckBox: QCheckBox = None self.enableCrosshairCheckBox: QCheckBox = None + self.enableWaypointsCheckBox: QCheckBox = None + self.listWaypoints: QListView = None self.lbl_planetDetails: QLabel = None self.lbl_currentStatus: QLabel = None uic.loadUi(str(RES_PATH / "ui" / "PlanetView.ui"), self) # Load the ui into self @@ -107,9 +108,10 @@ class PlanetView(qtw.QWidget): self.renderButton.clicked.connect(self._do_render) self.exportButton.clicked.connect(self._do_export) self.exportButton.setEnabled(False) - self.renderOutput.mouse_moved.connect(self._do_mouse_moved) - self.enableGridCheckBox.stateChanged.connect(self.renderOutput.set_grid_enabled) - self.enableCrosshairCheckBox.stateChanged.connect(self.renderOutput.set_crosshair_enabled) + self.renderOutput.crosshair_moved.connect(self._do_crosshair_moved) + self.renderOutput.render_window_moved.connect(self._do_render_window_changed) + self.enableGridCheckBox.stateChanged.connect(self.renderOutput.lyr_grid.set_enabled) + self.enableCrosshairCheckBox.stateChanged.connect(self.renderOutput.lyr_crosshair.set_enabled) self.renderer.set_settings(self.get_settings()) @@ -212,13 +214,24 @@ class PlanetView(qtw.QWidget): if filename: self.last_render.tex_color.save(filename, format="png") - def _do_mouse_moved(self, new_position: QPointF): - # TODO: Do coordinate conversion inside of planet_viewer - lon = new_position.x() - lat = new_position.y() - self.lbl_currentStatus.setText(f"Mouse Position:\n" - f"\tLat: {self.coord_to_dms(lat)}\n" - f"\tLon: {self.coord_to_dms(lon)}") + def _do_crosshair_moved(self, new_position: QPointF): + self._update_status() + + def _do_render_window_changed(self, new_window: QRectF): + self._update_status() + + def _update_status(self): + cross: QPointF = self.renderOutput.get_crosshair_coords() + render_window: QRectF = self.renderOutput.get_render_coords() + self.lbl_currentStatus.setText(f"Crosshair:\n" + f"\tLat:\t\t{self.coord_to_dms(cross.x())}\n" + f"\tLon:\t\t{self.coord_to_dms(cross.y())}\n" + f"\n" + f"Render Window:\n" + f"\tLeft Lat: \t{self.coord_to_dms(render_window.left())}\n" + f"\tRight Lat: \t{self.coord_to_dms(render_window.right())}\n" + f"\tTop Lat: \t{self.coord_to_dms(render_window.top())}\n" + f"\tBottom Lat:\t{self.coord_to_dms(render_window.bottom())}") @staticmethod def coord_to_dms(coord): diff --git a/starfab/gui/widgets/planets/__init__.py b/starfab/gui/widgets/planets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/starfab/gui/widgets/planets/crosshair_overlay.py b/starfab/gui/widgets/planets/crosshair_overlay.py new file mode 100644 index 0000000..92f0ea5 --- /dev/null +++ b/starfab/gui/widgets/planets/crosshair_overlay.py @@ -0,0 +1,29 @@ +from typing import Union + +from PySide6.QtCore import QRectF, QPointF +from PySide6.QtGui import QPen, QPainter +from PySide6.QtWidgets import QGraphicsEffect + + +class CrosshairOverlay(QGraphicsEffect): + def __init__(self, pen: QPen, region: QRectF, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mouse_position: Union[QPointF, None] = None + self.pen = pen + self.planet_bounds = region + + def update_mouse_position(self, pos: QPointF): + self.mouse_position = pos + + def remove_mouse(self): + self.mouse_position = None + + def draw(self, painter: QPainter) -> None: + if not self.mouse_position: + return + + vp = self.planet_bounds + painter.setPen(self.pen) + painter.drawLine(vp.left(), self.mouse_position.y(), vp.right(), self.mouse_position.y()) + painter.drawLine(self.mouse_position.x(), vp.top(), self.mouse_position.x(), vp.bottom()) + painter.drawRect(vp) diff --git a/starfab/gui/widgets/planets/effect_overlay.py b/starfab/gui/widgets/planets/effect_overlay.py new file mode 100644 index 0000000..098f31d --- /dev/null +++ b/starfab/gui/widgets/planets/effect_overlay.py @@ -0,0 +1,42 @@ +from typing import Type, Callable, Any, Union + +from PySide6.QtCore import QRectF +from PySide6.QtGui import QPainterPath +from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsEffect + + +class EffectOverlay(QGraphicsPathItem): + def __init__(self, fn_make_effect: Callable[[], Union[None | QGraphicsEffect]]): + self._bounds: QRectF = QRectF() + self._bounds_path = QPainterPath() + self._overlay_effect: Union[None, QGraphicsEffect] = None + self._enabled: bool = False + self._fn_make_effect: Callable = fn_make_effect + super().__init__(self._bounds_path) + # self.set_enabled(True) + + def update_bounds(self, new_bounds: QRectF): + self._bounds = new_bounds + self._bounds_path.clear() + self._bounds_path.addRect(self._bounds) + self.setPath(self._bounds_path) + self.invalidate() + print(new_bounds) + print(self._bounds_path.boundingRect()) + + def invalidate(self): + self.update(self._bounds) + + def set_enabled(self, enabled: bool): + self._enabled = enabled + if self._enabled: + # Need to rebuild each time as it gets disposed of + self._overlay_effect = self._fn_make_effect() + self.setGraphicsEffect(self._overlay_effect) + else: + self.setGraphicsEffect(None) + self._overlay_effect = None + self.invalidate() + + def effect_instance(self) -> Union[None, QGraphicsEffect]: + return self._overlay_effect diff --git a/starfab/gui/widgets/planets/grid_overlay.py b/starfab/gui/widgets/planets/grid_overlay.py new file mode 100644 index 0000000..4d8d14d --- /dev/null +++ b/starfab/gui/widgets/planets/grid_overlay.py @@ -0,0 +1,36 @@ +from PySide6.QtCore import QRectF +from PySide6.QtGui import QPen, QPainter +from PySide6.QtWidgets import QGraphicsEffect + + +class GridOverlay(QGraphicsEffect): + def __init__(self, + primary_pen: QPen, secondary_pen: QPen, + scale_factor: int, planet_bounds: QRectF, + *args, **kwargs): + super().__init__(*args, **kwargs) + self.primary_pen = primary_pen + self.secondary_pen = secondary_pen + self.scale_factor = scale_factor + self.planet_bounds = planet_bounds + + def draw(self, painter: QPainter) -> None: + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference) + self._draw_lines(painter, self.primary_pen, True) + self._draw_lines(painter, self.secondary_pen, False) + + def _draw_lines(self, painter: QPainter, pen: QPen, primary: bool): + painter.setPen(pen) + xoff = self.planet_bounds.x() + yoff = self.planet_bounds.y() + for lon in range(15, 360, 15): + is_primary = lon % 45 == 0 + if (is_primary and primary) or (not is_primary and not primary): + painter.drawLine(lon * self.scale_factor + xoff, self.planet_bounds.top(), + lon * self.scale_factor + xoff, self.planet_bounds.bottom()) + + for lat in range(15, 180, 15): + is_primary = lat % 45 == 0 + if (is_primary and primary) or (not is_primary and not primary): + painter.drawLine(self.planet_bounds.left(), lat * self.scale_factor + yoff, + self.planet_bounds.right(), lat * self.scale_factor + yoff) diff --git a/starfab/gui/widgets/planet_viewer.py b/starfab/gui/widgets/planets/planet_viewer.py similarity index 51% rename from starfab/gui/widgets/planet_viewer.py rename to starfab/gui/widgets/planets/planet_viewer.py index 98bdc8a..2b16fbf 100644 --- a/starfab/gui/widgets/planet_viewer.py +++ b/starfab/gui/widgets/planets/planet_viewer.py @@ -1,19 +1,22 @@ -from typing import Union, cast +from typing import cast import PySide6 -from PIL import Image from PIL.ImageQt import ImageQt -from PySide6.QtCore import QPointF, QRect, QRectF, QPoint, Signal, QSizeF -from PySide6.QtGui import QPainterPath, QColor, QTransform, QBrush, QPen, Qt, QPainter, QMouseEvent, QPixmap, QImage -from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsEffect +from PySide6.QtCore import QPointF, QRectF, Signal +from PySide6.QtGui import QPainterPath, QColor, QPen, Qt, QMouseEvent, QPixmap +from PySide6.QtWidgets import QGraphicsPathItem -from starfab.gui import qtc, qtg, qtw -from starfab.gui.widgets.image_viewer import QImageViewer +from starfab.gui import qtw +from starfab.gui.widgets.planets.crosshair_overlay import CrosshairOverlay +from starfab.gui.widgets.planets.grid_overlay import GridOverlay + +from starfab.gui.widgets.planets.effect_overlay import EffectOverlay from starfab.planets.planet_renderer import RenderResult class QPlanetViewer(qtw.QGraphicsView): - mouse_moved: Signal = Signal(QPointF) + crosshair_moved: Signal = Signal(QPointF) + render_window_moved: Signal = Signal(QRectF) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -22,6 +25,7 @@ class QPlanetViewer(qtw.QGraphicsView): self._scale_factor: int = 100 self._outer_perimeter: QRectF = QRectF() self._render_perimeter: QRectF = QRectF() + self._crosshair_position: QPointF = QPointF() self._render_result: None | RenderResult = None self._empty = True @@ -49,34 +53,34 @@ class QPlanetViewer(qtw.QGraphicsView): self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() self.scene.addItem(self.image) - self.perimeter_path: QPainterPath = QPainterPath() - self.render_outline: QPainterPath = QPainterPath() - self.perimeter_rect: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) - self.crosshair_overlay: QGraphicsPathItem = QGraphicsPathItem(self.perimeter_path) - self.render_window: QGraphicsPathItem = QGraphicsPathItem(self.render_outline) - self.perimeter_effect: None | GridEffect = None - self.crosshair_overlay_effect: None | CrosshairEffect = None - self.update_bounds(QRectF(0, -90, 360, 180), QRectF(0, -90, 180, 90)) - - self._grid_enabled = False - self.perimeter_rect.setZValue(1000) - self.set_grid_enabled(True) - self.scene.addItem(self.perimeter_rect) - - self._crosshair_enabled = False - self.crosshair_overlay.setZValue(2000) - self.set_crosshair_enabled(True) - self.scene.addItem(self.crosshair_overlay) - - self.render_window.setPen(QPen(QColor(0, 255, 0, 255), 20)) - self.render_window.setZValue(3000) - self.scene.addItem(self.render_window) + self.lyr_grid: EffectOverlay = EffectOverlay( + lambda: GridOverlay(self._major_grid_pen, self._minor_grid_pen, + self._scale_factor, self._outer_perimeter)) + self.lyr_grid.setZValue(1000) + self.scene.addItem(self.lyr_grid) + + self.lyr_crosshair: EffectOverlay = EffectOverlay( + lambda: CrosshairOverlay(self._crosshair_pen, self._outer_perimeter)) + self.lyr_crosshair.setZValue(2000) + self.scene.addItem(self.lyr_crosshair) + + self.lyr_render: EffectOverlay = EffectOverlay(lambda: None) + self.lyr_render.setPen(QPen(QColor(0, 255, 0, 255), 20)) + self.lyr_render.setZValue(3000) + self.scene.addItem(self.lyr_render) + + self.update_bounds(QRectF(0, -90, 360, 180), QRectF(0, -90, 360, 180)) + self.render_window_dragging: bool = False self.render_window_drag_pos: None | QPointF = None + self.lyr_grid.set_enabled(True) + self.lyr_crosshair.set_enabled(True) + self.lyr_render.set_enabled(True) + self.setMouseTracking(True) self.image.setCursor(Qt.CrossCursor) - self.render_window.setCursor(Qt.SizeAllCursor) + self.lyr_render.setCursor(Qt.SizeAllCursor) self.fitInView(self._render_perimeter, Qt.KeepAspectRatio) self.ensureVisible(QRectF(0, 0, 100, 100)) self.setDragMode(qtw.QGraphicsView.ScrollHandDrag) @@ -90,12 +94,6 @@ class QPlanetViewer(qtw.QGraphicsView): self._render_perimeter: QRectF = QRectF(render_rect.topLeft() * self._scale_factor, render_rect.size() * self._scale_factor) - # Update paths - self.perimeter_path.clear() - self.perimeter_path.addRect(self._outer_perimeter) - self.render_outline.clear() - self.render_outline.addRect(self._render_perimeter) - # Add some extra padding around the planet bounds. image_padding = 10 * self._scale_factor scene_area = QRectF(self._outer_perimeter.x() - image_padding, @@ -104,15 +102,9 @@ class QPlanetViewer(qtw.QGraphicsView): self._outer_perimeter.height() + 2 * image_padding) self.scene.setSceneRect(scene_area) - if self.perimeter_effect: - self.perimeter_effect.planet_bounds = self._outer_perimeter - if self.crosshair_overlay_effect: - self.crosshair_overlay_effect.planet_bounds = self._outer_perimeter - self.perimeter_rect.setPath(self.perimeter_path) - self.crosshair_overlay.setPath(self.perimeter_path) - self.render_window.setPath(self.render_outline) - self.perimeter_rect.update() - self.crosshair_overlay.update() + self.lyr_grid.update_bounds(self._outer_perimeter) + self.lyr_crosshair.update_bounds(self._outer_perimeter) + self.lyr_render.update_bounds(self._render_perimeter) self.scene.update() self.update() @@ -120,6 +112,9 @@ class QPlanetViewer(qtw.QGraphicsView): return QRectF(self._render_perimeter.topLeft() / self._scale_factor, self._render_perimeter.size() / self._scale_factor) + def get_crosshair_coords(self) -> QPointF: + return self._crosshair_position + def mousePressEvent(self, event: PySide6.QtGui.QMouseEvent) -> None: image_space_pos = self.mapToScene(event.pos()) global_coordinates = self.image_to_coordinates(image_space_pos) @@ -148,16 +143,18 @@ class QPlanetViewer(qtw.QGraphicsView): self._render_perimeter.setRect(final_position_x, final_position_y, self._render_perimeter.width(), self._render_perimeter.height()) - self.render_outline.clear() - self.render_outline.addRect(self._render_perimeter) - self.render_window.setPath(self.render_outline) + self.lyr_render.update_bounds(self._render_perimeter) + self.render_window_moved.emit(self._render_perimeter) else: super().mouseMoveEvent(event) - self.mouse_moved.emit(global_coordinates) - if self.crosshair_overlay_effect: - self.crosshair_overlay_effect.update_mouse_position(image_space_pos) - self.crosshair_overlay.update(self._outer_perimeter) + self._crosshair_position = global_coordinates + self.crosshair_moved.emit(self._crosshair_position) + + crosshair_overlay: CrosshairOverlay = cast(CrosshairOverlay, self.lyr_crosshair.effect_instance()) + if crosshair_overlay: + crosshair_overlay.update_mouse_position(image_space_pos) + self.lyr_crosshair.invalidate() def mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None: image_space_pos = self.mapToScene(event.pos()) @@ -195,31 +192,6 @@ class QPlanetViewer(qtw.QGraphicsView): # TODO: Better support non-standard render sizes self.image.setScale(width_scale * s) - def set_grid_enabled(self, enabled: bool): - self._grid_enabled = enabled - if enabled: - # Need to rebuild each time as it gets disposed of - self.perimeter_effect = GridEffect(self._major_grid_pen, self._minor_grid_pen, - self._scale_factor, self._outer_perimeter) - self.perimeter_rect.setGraphicsEffect(self.perimeter_effect) - else: - self.perimeter_rect.setGraphicsEffect(None) - self.perimeter_effect = None - self.update() - self.scene.update() - - def set_crosshair_enabled(self, enabled: bool): - self._crosshair_enabled = enabled - if enabled: - # Need to rebuild each time as it gets disposed of - self.crosshair_overlay_effect = CrosshairEffect(self._crosshair_pen, self._outer_perimeter) - self.crosshair_overlay.setGraphicsEffect(self.crosshair_overlay_effect) - else: - self.crosshair_overlay.setGraphicsEffect(None) - self.crosshair_overlay_effect = None - self.update() - self.scene.update() - def wheelEvent(self, event): factor = 0 if event.angleDelta().y() > 0: @@ -232,60 +204,3 @@ class QPlanetViewer(qtw.QGraphicsView): self._zoom -= 1 if factor: self.scale(factor, factor) - - -class CrosshairEffect(QGraphicsEffect): - def __init__(self, pen: QPen, region: QRectF, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mouse_position: Union[QPointF, None] = None - self.pen = pen - self.planet_bounds = region - - def update_mouse_position(self, pos: QPointF): - self.mouse_position = pos - - def remove_mouse(self): - self.mouse_position = None - - def draw(self, painter: QPainter) -> None: - if not self.mouse_position: - return - - vp = self.planet_bounds - painter.setPen(self.pen) - painter.drawLine(vp.left(), self.mouse_position.y(), vp.right(), self.mouse_position.y()) - painter.drawLine(self.mouse_position.x(), vp.top(), self.mouse_position.x(), vp.bottom()) - painter.drawRect(vp) - - -class GridEffect(QGraphicsEffect): - def __init__(self, - primary_pen: QPen, secondary_pen: QPen, - scale_factor: int, planet_bounds: QRectF, - *args, **kwargs): - super().__init__(*args, **kwargs) - self.primary_pen = primary_pen - self.secondary_pen = secondary_pen - self.scale_factor = scale_factor - self.planet_bounds = planet_bounds - - def draw(self, painter: QPainter) -> None: - painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Difference) - self._draw_lines(painter, self.primary_pen, True) - self._draw_lines(painter, self.secondary_pen, False) - - def _draw_lines(self, painter: QPainter, pen: QPen, primary: bool): - painter.setPen(pen) - xoff = self.planet_bounds.x() - yoff = self.planet_bounds.y() - for lon in range(15, 360, 15): - is_primary = lon % 45 == 0 - if (is_primary and primary) or (not is_primary and not primary): - painter.drawLine(lon * self.scale_factor + xoff, self.planet_bounds.top(), - lon * self.scale_factor + xoff, self.planet_bounds.bottom()) - - for lat in range(15, 180, 15): - is_primary = lat % 45 == 0 - if (is_primary and primary) or (not is_primary and not primary): - painter.drawLine(self.planet_bounds.left(), lat * self.scale_factor + yoff, - self.planet_bounds.right(), lat * self.scale_factor + yoff) diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index 03c8273..b7c7f0b 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -1,6 +1,7 @@ import struct from pathlib import Path +from PySide6.QtCore import QPointF from compushady import Compute from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk, CryXMLBChunk from scdatatools.p4k import P4KInfo @@ -10,6 +11,12 @@ from starfab.planets.data import LUTData, Brush from starfab.planets.ecosystem import EcoSystem +class WaypointData: + def __init__(self, point: QPointF, container: ObjectContainer): + self.point = point + self.container = container + + class Planet: def __init__(self, oc: ObjectContainerInstance, data: JSONChunk): self.oc: ObjectContainerInstance = oc @@ -30,9 +37,20 @@ class Planet: self.lut: list[list[LUTData]] = None + self.waypoints: list[WaypointData] = [] + self.gpu_resources = {} self.gpu_computer: Compute = None + if self.oc.name.endswith("stanton4.socpak"): + self.load_waypoints() + + def load_waypoints(self): + print(f"==={self.oc.name}===") + for child_name in self.oc.children: + child_soc = self.oc.children[child_name] + print(f"{child_name} => {child_soc} {child_soc.name}/{child_soc.display_name}/{child_soc.entity_name}") + def load_data(self) -> object: if self.planet_data: return self.planet_data diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index ef9ba45..f3d2512 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -180,6 +180,30 @@
+ + + + + + + Waypoints + + + + + + + Enable Waypoints + + + + + + + true + + +
@@ -265,7 +289,7 @@ QPlanetViewer QGraphicsView -
starfab.gui.widgets.planet_viewer
+
starfab.gui.widgets.planets.planet_viewer
-- GitLab From 6d8bbeadadb79d867785035698e53e17885ace52 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 14 Dec 2023 03:05:20 -0500 Subject: [PATCH 13/49] Add basic waypoint list and selection. --- starfab/gui/widgets/pages/page_PlanetView.py | 27 +++++++++++++-- starfab/gui/widgets/planets/effect_overlay.py | 4 +-- starfab/gui/widgets/planets/planet_viewer.py | 34 +++++++++++++++++-- .../gui/widgets/planets/waypoint_overlay.py | 31 +++++++++++++++++ starfab/planets/planet.py | 22 +++++++++--- starfab/resources/ui/PlanetView.ui | 2 +- 6 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 starfab/gui/widgets/planets/waypoint_overlay.py diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 85dece9..cd8c98a 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,8 +1,8 @@ import io -from typing import Union +from typing import Union, cast from PIL import Image -from PySide6.QtCore import Qt, QPointF, QRectF +from PySide6.QtCore import Qt, QPointF, QRectF, QItemSelectionModel, QItemSelection from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import QComboBox, QPushButton, QLabel, QCheckBox, QListView from qtpy import uic @@ -11,6 +11,7 @@ from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInst from starfab.gui import qtw, qtc from starfab.gui.widgets.planets.planet_viewer import QPlanetViewer +from starfab.gui.widgets.planets.waypoint_overlay import WaypointOverlay from starfab.log import getLogger from starfab.planets.planet import Planet from starfab.planets.data import RenderSettings @@ -105,6 +106,7 @@ class PlanetView(qtw.QWidget): self.sc.datacore_model.loaded.connect(self._hack_before_load) self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) + self.planetComboBox.currentIndexChanged.connect(self._planet_changed) self.renderButton.clicked.connect(self._do_render) self.exportButton.clicked.connect(self._do_export) self.exportButton.setEnabled(False) @@ -114,6 +116,11 @@ class PlanetView(qtw.QWidget): self.enableCrosshairCheckBox.stateChanged.connect(self.renderOutput.lyr_crosshair.set_enabled) self.renderer.set_settings(self.get_settings()) + self._planet_changed() + + def _planet_changed(self): + # TODO: Pre-load ecosystem data here w/ progressbar + self._update_waypoints() def _render_scale_changed(self): new_scale = self.renderResolutionComboBox.currentData(role=Qt.UserRole) @@ -143,6 +150,22 @@ class PlanetView(qtw.QWidget): self.renderOutput.update_bounds(planet_bounds, self.renderer.get_bounds_for_render(render_bounds.topLeft())) + def _update_waypoints(self): + planet: Planet = self.planetComboBox.currentData(role=Qt.UserRole) + waypoint_records = [(wp.container.entity_name, wp) for wp in planet.waypoints] + waypoint_model = self.create_model(waypoint_records) + waypoint_selection = QItemSelectionModel(waypoint_model) + waypoint_selection.selectionChanged.connect(self._waypoint_changed) + self.listWaypoints.setModel(waypoint_model) + self.listWaypoints.setSelectionModel(waypoint_selection) + self.renderOutput.set_waypoints(planet.waypoints) + + def _waypoint_changed(self, selected: QItemSelection, removed: QItemSelection): + if selected.size() == 0: + return + waypoint = selected.indexes()[0].data(role=Qt.UserRole) + self.renderOutput.set_selected_waypoint(waypoint) + def _display_layer_changed(self): layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) self.renderOutput.update_visible_layer(layer) diff --git a/starfab/gui/widgets/planets/effect_overlay.py b/starfab/gui/widgets/planets/effect_overlay.py index 098f31d..6014888 100644 --- a/starfab/gui/widgets/planets/effect_overlay.py +++ b/starfab/gui/widgets/planets/effect_overlay.py @@ -21,8 +21,6 @@ class EffectOverlay(QGraphicsPathItem): self._bounds_path.addRect(self._bounds) self.setPath(self._bounds_path) self.invalidate() - print(new_bounds) - print(self._bounds_path.boundingRect()) def invalidate(self): self.update(self._bounds) @@ -38,5 +36,5 @@ class EffectOverlay(QGraphicsPathItem): self._overlay_effect = None self.invalidate() - def effect_instance(self) -> Union[None, QGraphicsEffect]: + def get_effect_instance(self) -> Union[None, QGraphicsEffect]: return self._overlay_effect diff --git a/starfab/gui/widgets/planets/planet_viewer.py b/starfab/gui/widgets/planets/planet_viewer.py index 2b16fbf..67176c3 100644 --- a/starfab/gui/widgets/planets/planet_viewer.py +++ b/starfab/gui/widgets/planets/planet_viewer.py @@ -2,7 +2,7 @@ from typing import cast import PySide6 from PIL.ImageQt import ImageQt -from PySide6.QtCore import QPointF, QRectF, Signal +from PySide6.QtCore import QPointF, QRectF, Signal, QItemSelection from PySide6.QtGui import QPainterPath, QColor, QPen, Qt, QMouseEvent, QPixmap from PySide6.QtWidgets import QGraphicsPathItem @@ -11,6 +11,8 @@ from starfab.gui.widgets.planets.crosshair_overlay import CrosshairOverlay from starfab.gui.widgets.planets.grid_overlay import GridOverlay from starfab.gui.widgets.planets.effect_overlay import EffectOverlay +from starfab.gui.widgets.planets.waypoint_overlay import WaypointOverlay +from starfab.planets.planet import WaypointData from starfab.planets.planet_renderer import RenderResult @@ -27,6 +29,8 @@ class QPlanetViewer(qtw.QGraphicsView): self._render_perimeter: QRectF = QRectF() self._crosshair_position: QPointF = QPointF() self._render_result: None | RenderResult = None + self._waypoints: list[WaypointData] = [] + self._selected_waypoint: None | WaypointData = None self._empty = True self._zoom = 0 @@ -53,20 +57,29 @@ class QPlanetViewer(qtw.QGraphicsView): self.image: qtw.QGraphicsPixmapItem = qtw.QGraphicsPixmapItem() self.scene.addItem(self.image) + # Layer 1: Coordinate grid self.lyr_grid: EffectOverlay = EffectOverlay( lambda: GridOverlay(self._major_grid_pen, self._minor_grid_pen, self._scale_factor, self._outer_perimeter)) self.lyr_grid.setZValue(1000) self.scene.addItem(self.lyr_grid) + # Layer 2: Crosshair overlay self.lyr_crosshair: EffectOverlay = EffectOverlay( lambda: CrosshairOverlay(self._crosshair_pen, self._outer_perimeter)) self.lyr_crosshair.setZValue(2000) self.scene.addItem(self.lyr_crosshair) + # Layer 3: Waypoint overlay + self.lyr_waypoints: EffectOverlay = EffectOverlay( + lambda: WaypointOverlay(self._waypoints, self._selected_waypoint)) + self.lyr_waypoints.setZValue(3000) + self.scene.addItem(self.lyr_waypoints) + + # Layer 4: Draggable active render window self.lyr_render: EffectOverlay = EffectOverlay(lambda: None) self.lyr_render.setPen(QPen(QColor(0, 255, 0, 255), 20)) - self.lyr_render.setZValue(3000) + self.lyr_render.setZValue(4000) self.scene.addItem(self.lyr_render) self.update_bounds(QRectF(0, -90, 360, 180), QRectF(0, -90, 360, 180)) @@ -76,6 +89,7 @@ class QPlanetViewer(qtw.QGraphicsView): self.lyr_grid.set_enabled(True) self.lyr_crosshair.set_enabled(True) + self.lyr_waypoints.set_enabled(True) self.lyr_render.set_enabled(True) self.setMouseTracking(True) @@ -104,6 +118,7 @@ class QPlanetViewer(qtw.QGraphicsView): self.lyr_grid.update_bounds(self._outer_perimeter) self.lyr_crosshair.update_bounds(self._outer_perimeter) + self.lyr_waypoints.update_bounds(self._outer_perimeter) self.lyr_render.update_bounds(self._render_perimeter) self.scene.update() self.update() @@ -115,6 +130,19 @@ class QPlanetViewer(qtw.QGraphicsView): def get_crosshair_coords(self) -> QPointF: return self._crosshair_position + def set_waypoints(self, new_wapoints: list[WaypointData]): + self._waypoints = new_wapoints + effect: WaypointOverlay = cast(WaypointOverlay, self.lyr_waypoints.get_effect_instance()) + if effect: + effect.update_waypoints(self._waypoints) + + def set_selected_waypoint(self, waypoint: None | WaypointData): + self._selected_waypoint = waypoint + print(f"New WP: {waypoint}") + effect: WaypointOverlay = cast(WaypointOverlay, self.lyr_waypoints.get_effect_instance()) + if effect: + effect.select_waypoint(self._selected_waypoint) + def mousePressEvent(self, event: PySide6.QtGui.QMouseEvent) -> None: image_space_pos = self.mapToScene(event.pos()) global_coordinates = self.image_to_coordinates(image_space_pos) @@ -151,7 +179,7 @@ class QPlanetViewer(qtw.QGraphicsView): self._crosshair_position = global_coordinates self.crosshair_moved.emit(self._crosshair_position) - crosshair_overlay: CrosshairOverlay = cast(CrosshairOverlay, self.lyr_crosshair.effect_instance()) + crosshair_overlay: CrosshairOverlay = cast(CrosshairOverlay, self.lyr_crosshair.get_effect_instance()) if crosshair_overlay: crosshair_overlay.update_mouse_position(image_space_pos) self.lyr_crosshair.invalidate() diff --git a/starfab/gui/widgets/planets/waypoint_overlay.py b/starfab/gui/widgets/planets/waypoint_overlay.py new file mode 100644 index 0000000..126cb6b --- /dev/null +++ b/starfab/gui/widgets/planets/waypoint_overlay.py @@ -0,0 +1,31 @@ +from PySide6.QtCore import QRectF +from PySide6.QtGui import QPen, QPainter, QColor +from PySide6.QtWidgets import QGraphicsEffect + +from starfab.planets.planet import WaypointData + + +class WaypointOverlay(QGraphicsEffect): + def __init__(self, waypoints: list[WaypointData], selected_waypoint: None | WaypointData, *args, **kwargs): + super().__init__(*args, **kwargs) + self._waypoints: list[WaypointData] = waypoints + self._selected_waypoint: None | WaypointData = selected_waypoint + + def draw(self, painter: QPainter) -> None: + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) + for waypoint in self._waypoints: + if self._selected_waypoint and waypoint == self._selected_waypoint: + painter.setPen(QPen(QColor(0, 255, 0, 255), 10)) + painter.drawEllipse(waypoint.point * 100, 75, 75) + else: + painter.setPen(QPen(QColor(255, 0, 0, 255), 10)) + painter.drawEllipse(waypoint.point * 100, 50, 50) + + + def update_waypoints(self, waypoints: list[WaypointData]): + self._waypoints = waypoints + self.update() + + def select_waypoint(self, waypoint: None | WaypointData): + self._selected_waypoint = waypoint + self.update() diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index b7c7f0b..fc3d1d8 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -1,5 +1,8 @@ +import math import struct +from math import atan2, sqrt from pathlib import Path +from typing import Tuple from PySide6.QtCore import QPointF from compushady import Compute @@ -12,9 +15,9 @@ from starfab.planets.ecosystem import EcoSystem class WaypointData: - def __init__(self, point: QPointF, container: ObjectContainer): - self.point = point - self.container = container + def __init__(self, point: QPointF, container: ObjectContainerInstance): + self.point: QPointF = point + self.container: ObjectContainerInstance = container class Planet: @@ -45,11 +48,20 @@ class Planet: if self.oc.name.endswith("stanton4.socpak"): self.load_waypoints() + @staticmethod + def position_to_coordinates(x: float, y: float, z: float) -> Tuple[QPointF, float]: + xy_len = sqrt(x * x + y * y) + lat = atan2(-z, xy_len) * (180 / math.pi) + lon = atan2(-x, y) * (180 / math.pi) + alt = sqrt(x * x + y * y + z * z) + # +90 if offsetting for NASA coords, gives us 0-360deg output range + return QPointF((lon + 90 + 360) % 360, lat), alt + def load_waypoints(self): - print(f"==={self.oc.name}===") for child_name in self.oc.children: child_soc = self.oc.children[child_name] - print(f"{child_name} => {child_soc} {child_soc.name}/{child_soc.display_name}/{child_soc.entity_name}") + coords = self.position_to_coordinates(child_soc.position.x, child_soc.position.y, child_soc.position.z) + self.waypoints.append(WaypointData(coords[0], child_soc)) def load_data(self) -> object: if self.planet_data: diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index f3d2512..a16499e 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -181,7 +181,7 @@
- + -- GitLab From c23ffef2cebd23bdde6b03cac7230b428c0f5aa4 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 2 May 2024 02:35:45 -0400 Subject: [PATCH 14/49] Add detection for compushady being installed or not. (Still need to update screen logic) --- starfab/planets/__init__.py | 8 ++++++++ starfab/planets/data.py | 2 +- starfab/planets/planet.py | 3 ++- starfab/planets/planet_renderer.py | 4 +--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/starfab/planets/__init__.py b/starfab/planets/__init__.py index e69de29..b1fd15a 100644 --- a/starfab/planets/__init__.py +++ b/starfab/planets/__init__.py @@ -0,0 +1,8 @@ + +try: + from compushady import Texture2D, Compute, Resource, HEAP_UPLOAD, Buffer, Texture3D, HEAP_READBACK + from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT + from compushady.shaders import hlsl + HAS_COMPUSHADY = True +except ImportError as e: + HAS_COMPUSHADY = False diff --git a/starfab/planets/data.py b/starfab/planets/data.py index 81b02e2..f6c3ba9 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -1,7 +1,7 @@ import struct from typing import Tuple -from compushady import Buffer, HEAP_UPLOAD +from . import * class LUTData: diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index fc3d1d8..3c829ec 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Tuple from PySide6.QtCore import QPointF -from compushady import Compute from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk, CryXMLBChunk from scdatatools.p4k import P4KInfo from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance @@ -13,6 +12,8 @@ from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInst from starfab.planets.data import LUTData, Brush from starfab.planets.ecosystem import EcoSystem +from . import * + class WaypointData: def __init__(self, point: QPointF, container: ObjectContainerInstance): diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index d941266..366b015 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -4,10 +4,8 @@ from typing import Union, Callable, Tuple, cast from PIL import Image from PySide6.QtCore import QRectF, QPointF, QSizeF, QSize -from compushady import Texture2D, Compute, Resource, HEAP_UPLOAD, Buffer, Texture3D, HEAP_READBACK -from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT -from compushady.shaders import hlsl +from . import * from starfab.planets.planet import Planet from starfab.planets.data import LUTData, RenderJobSettings, RenderSettings from starfab.planets.ecosystem import EcoSystem -- GitLab From d123a879e1a71a44c3db50c21ae14d66a5201292 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 2 May 2024 03:02:57 -0400 Subject: [PATCH 15/49] Hide the planet button if compushady is not installed. --- starfab/app.py | 28 +++++++++++++------- starfab/gui/widgets/pages/__init__.py | 7 +++-- starfab/gui/widgets/pages/page_PlanetView.py | 3 +++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/starfab/app.py b/starfab/app.py index afe19a2..d0f9013 100644 --- a/starfab/app.py +++ b/starfab/app.py @@ -14,8 +14,10 @@ from qtpy.QtWidgets import QMainWindow import starfab.gui.widgets.dock_widgets.datacore_widget import starfab.gui.widgets.dock_widgets.file_view + from scdatatools.sc import StarCitizen from starfab.gui.widgets.pages.content import ContentView +from starfab.planets import HAS_COMPUSHADY from . import __version__, updates from .blender import BlenderManager from .gui import qtg, qtw, qtc @@ -143,8 +145,10 @@ class StarFab(QMainWindow): self.actionContentView.triggered.connect(self._handle_workspace_action) self.actionNavView.setIcon(qta.icon("mdi6.map-marker-path")) self.actionNavView.triggered.connect(self._handle_workspace_action) - self.actionPlanetView.setIcon(qta.icon("ph.planet")) - self.actionPlanetView.triggered.connect(self._handle_workspace_action) + if HAS_COMPUSHADY: + self.actionPlanetView.setIcon(qta.icon("ph.planet")) + self.actionPlanetView.triggered.connect(self._handle_workspace_action) + self._open_settings = self.actionSettings self.actionSettings.setIcon(qta.icon("msc.settings-gear")) self._show_console = self.actionConsole @@ -187,8 +191,9 @@ class StarFab(QMainWindow): self.page_NavView = NavView(self) self.stackedWidgetWorkspace.addWidget(self.page_NavView) - self.page_PlanetView = PlanetView(self) - self.stackedWidgetWorkspace.addWidget(self.page_PlanetView) + if HAS_COMPUSHADY: + self.page_PlanetView = PlanetView(self) + self.stackedWidgetWorkspace.addWidget(self.page_PlanetView) self.dock_widgets = {} self.setup_dock_widgets() @@ -249,10 +254,11 @@ class StarFab(QMainWindow): self.nav_page_btn.released.connect(self.handle_workspace) self.workspace_panel.add_ribbon_widget(self.nav_page_btn) - self.planets_page_btn = RibbonButton(self, self.actionPlanetView, True) - self.planets_page_btn.setAutoExclusive(True) - self.planets_page_btn.released.connect(self.handle_workspace) - self.workspace_panel.add_ribbon_widget(self.planets_page_btn) + if HAS_COMPUSHADY: + self.planets_page_btn = RibbonButton(self, self.actionPlanetView, True) + self.planets_page_btn.setAutoExclusive(True) + self.planets_page_btn.released.connect(self.handle_workspace) + self.workspace_panel.add_ribbon_widget(self.planets_page_btn) self.options_panel = self.home_tab.add_ribbon_pane("Options") self.options_panel.add_ribbon_widget( @@ -761,8 +767,10 @@ Contributors: def handle_workspace(self, view=None, *args, **kwargs): self.workspace_btns = [self.data_page_btn, self.content_page_btn, - self.nav_page_btn, - self.planets_page_btn] + self.nav_page_btn] + + if HAS_COMPUSHADY: + self.workspace_btns.append(self.planets_page_btn) def _clear_checked(self): for btn in self.workspace_btns: diff --git a/starfab/gui/widgets/pages/__init__.py b/starfab/gui/widgets/pages/__init__.py index 87b398d..8e2ba8a 100644 --- a/starfab/gui/widgets/pages/__init__.py +++ b/starfab/gui/widgets/pages/__init__.py @@ -1,4 +1,7 @@ +from starfab.planets import HAS_COMPUSHADY + from .page_DataView import DataView from .page_NavView import NavView -from .page_PlanetView import PlanetView -from .content import ContentView \ No newline at end of file +if HAS_COMPUSHADY: + from .page_PlanetView import PlanetView +from .content import ContentView diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index cd8c98a..fc4f2c5 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -152,6 +152,9 @@ class PlanetView(qtw.QWidget): def _update_waypoints(self): planet: Planet = self.planetComboBox.currentData(role=Qt.UserRole) + if not planet: + return + waypoint_records = [(wp.container.entity_name, wp) for wp in planet.waypoints] waypoint_model = self.create_model(waypoint_records) waypoint_selection = QItemSelectionModel(waypoint_model) -- GitLab From 4737c05befef212b4d7d8c8fe04076eda6bb0d52 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 2 May 2024 03:16:51 -0400 Subject: [PATCH 16/49] Show the button, but disabled and with a tooltip to install compushady. --- starfab/app.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/starfab/app.py b/starfab/app.py index d0f9013..37a57eb 100644 --- a/starfab/app.py +++ b/starfab/app.py @@ -145,8 +145,8 @@ class StarFab(QMainWindow): self.actionContentView.triggered.connect(self._handle_workspace_action) self.actionNavView.setIcon(qta.icon("mdi6.map-marker-path")) self.actionNavView.triggered.connect(self._handle_workspace_action) + self.actionPlanetView.setIcon(qta.icon("ph.planet")) if HAS_COMPUSHADY: - self.actionPlanetView.setIcon(qta.icon("ph.planet")) self.actionPlanetView.triggered.connect(self._handle_workspace_action) self._open_settings = self.actionSettings @@ -254,11 +254,13 @@ class StarFab(QMainWindow): self.nav_page_btn.released.connect(self.handle_workspace) self.workspace_panel.add_ribbon_widget(self.nav_page_btn) - if HAS_COMPUSHADY: - self.planets_page_btn = RibbonButton(self, self.actionPlanetView, True) - self.planets_page_btn.setAutoExclusive(True) - self.planets_page_btn.released.connect(self.handle_workspace) - self.workspace_panel.add_ribbon_widget(self.planets_page_btn) + self.planets_page_btn = RibbonButton(self, self.actionPlanetView, True) + self.planets_page_btn.setAutoExclusive(True) + self.planets_page_btn.released.connect(self.handle_workspace) + self.workspace_panel.add_ribbon_widget(self.planets_page_btn) + if not HAS_COMPUSHADY: + self.planets_page_btn.setEnabled(False) + self.planets_page_btn.setToolTip("You must install the 'compushady' package to use the planet viewer.") self.options_panel = self.home_tab.add_ribbon_pane("Options") self.options_panel.add_ribbon_widget( -- GitLab From 6251ea2e3b40944f2f52caa67b2ebb8001e5a8e1 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sat, 11 May 2024 12:58:54 -0400 Subject: [PATCH 17/49] Allow loading of waypoints for all plents, not just microtech. Waypoints are only loaded (and cached) when selected from planet viewer. Add support for basic hillshade pass (currently doesn't actually function). Add a bunch of render job settings. --- starfab/gui/widgets/pages/page_PlanetView.py | 29 +++-- starfab/planets/data.py | 26 +++- starfab/planets/hlsl/hillshade.hlsl | 125 +++++++++++++++++++ starfab/planets/{ => hlsl}/shader.hlsl | 35 +++++- starfab/planets/planet.py | 19 ++- starfab/planets/planet_renderer.py | 44 ++++++- starfab/resources/ui/PlanetView.ui | 40 ++++-- 7 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 starfab/planets/hlsl/hillshade.hlsl rename starfab/planets/{ => hlsl}/shader.hlsl (94%) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index fc4f2c5..3473f21 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -13,6 +13,7 @@ from starfab.gui import qtw, qtc from starfab.gui.widgets.planets.planet_viewer import QPlanetViewer from starfab.gui.widgets.planets.waypoint_overlay import WaypointOverlay from starfab.log import getLogger +from starfab.planets import * from starfab.planets.planet import Planet from starfab.planets.data import RenderSettings from starfab.planets.ecosystem import EcoSystem @@ -42,6 +43,8 @@ class PlanetView(qtw.QWidget): self.enableGridCheckBox: QCheckBox = None self.enableCrosshairCheckBox: QCheckBox = None self.enableWaypointsCheckBox: QCheckBox = None + self.enableHillshadeCheckBox: QCheckBox = None + self.enableBinaryOceanMaskCheckBox: QCheckBox = None self.listWaypoints: QListView = None self.lbl_planetDetails: QLabel = None self.lbl_currentStatus: QLabel = None @@ -155,7 +158,9 @@ class PlanetView(qtw.QWidget): if not planet: return - waypoint_records = [(wp.container.entity_name, wp) for wp in planet.waypoints] + planet.load_waypoints() + + waypoint_records = [(wp.container.display_name, wp) for wp in planet.waypoints] waypoint_model = self.create_model(waypoint_records) waypoint_selection = QItemSelectionModel(waypoint_model) waypoint_selection.selectionChanged.connect(self._waypoint_changed) @@ -197,20 +202,26 @@ class PlanetView(qtw.QWidget): return model - def shader_path(self) -> Path: - return Path(__file__) / '../../../../planets/shader.hlsl' + def shader_path(self, name) -> Path: + return Path(__file__) / f'../../../../planets/hlsl/{name}' - def _get_shader(self): - with io.open(self.shader_path(), "r") as shader: - return shader.read() + def _get_shader(self, name): + with io.open(self.shader_path(name), "r") as shader: + return hlsl.compile(shader.read()) def get_settings(self): scale = self.renderResolutionComboBox.currentData(role=Qt.UserRole) coordinates = self.coordinateSystemComboBox.currentData(role=Qt.UserRole) interpolation = self.sampleModeComboBox.currentData(role=Qt.UserRole) resolution = self.outputResolutionComboBox.currentData(role=Qt.UserRole) - shader = self._get_shader() - return RenderSettings(True, scale, coordinates, shader, interpolation, resolution) + main_shader = self._get_shader("shader.hlsl") + hillshade_shader = self._get_shader("hillshade.hlsl") + hillshade_enabled = self.enableHillshadeCheckBox.isChecked() + ocean_mask_binary = self.enableBinaryOceanMaskCheckBox.isChecked() + return RenderSettings(True, scale, coordinates, + main_shader, hillshade_shader, + interpolation, resolution, + hillshade_enabled, ocean_mask_binary) def _do_render(self): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) @@ -281,7 +292,7 @@ class PlanetView(qtw.QWidget): bodies: list[Planet] = self._search_for_bodies(pu_oc) self.planetComboBox.setModel(self.create_model([ - (b.oc.entity_name, b) for b in bodies + (b.oc.display_name, b) for b in bodies ])) except Exception as ex: diff --git a/starfab/planets/data.py b/starfab/planets/data.py index f6c3ba9..db6fb5d 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -25,7 +25,9 @@ class LUTData: class RenderJobSettings: - PACK_STRING: str = "5f3i5f4i" + # NOTE: Bools are GPU-register aligned, so need to be 4 bytes, not 1 + # so we pack them as i instead of ? + PACK_STRING: str = "5f3i4f3i1f5i2f" PACK_LENGTH: int = struct.calcsize(PACK_STRING) def __init__(self): @@ -45,9 +47,16 @@ class RenderJobSettings: self.global_terrain_height_influence: float = 4000 self.ecosystem_terrain_height_influence: float = 1000 + self.ocean_enabled: bool = True + self.ocean_mask_binary: bool = False + self.ocean_heightmap_flat: bool = True self.ocean_depth: float = -2000 self.ocean_color: list[int] = [0, 0, 0, 255] + self.hillshade_enabled: bool = True + self.hillshade_zenith: float = 45 + self.hillshade_azimuth: float = 135 + def pack(self) -> bytes: return struct.pack(RenderJobSettings.PACK_STRING, self.offset_x, self.offset_y, self.size_x, self.size_y, @@ -55,7 +64,9 @@ class RenderJobSettings: self.render_scale_x, self.render_scale_y, self.local_humidity_influence, self.local_temperature_influence, self.global_terrain_height_influence, self.ecosystem_terrain_height_influence, - self.ocean_depth, *self.ocean_color) + self.ocean_enabled, self.ocean_mask_binary, self.ocean_heightmap_flat, + self.ocean_depth, *self.ocean_color, + self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth) def update_buffer(self, buffer_gpu: Buffer): data = self.pack() @@ -117,11 +128,16 @@ class LocalClimateData: class RenderSettings: - def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, hlsl: str, - interpolation: int, output_resolution: Tuple[int, int]): + def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, + shader_main: str, shader_hillshade: str, + interpolation: int, output_resolution: Tuple[int, int], + hillshade_enabled: bool, ocean_mask_binary: bool): self.gpu = gpu self.resolution = resolution self.coordinate_mode = coordinate_mode - self.hlsl = hlsl + self.shader_main = shader_main + self.shader_hillshade = shader_hillshade self.interpolation = interpolation self.output_resolution = output_resolution + self.hillshade_enabled = hillshade_enabled + self.ocean_mask_binary = ocean_mask_binary diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl new file mode 100644 index 0000000..edb2b05 --- /dev/null +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -0,0 +1,125 @@ +#define PI radians(180) +#define MODE_NN 0 +#define MODE_BI_LINEAR 1 +#define MODE_BI_CUBIC 2 + +struct RenderJobSettings +{ + float2 offset; + float2 size; + float planet_radius; + int interpolation; + int2 render_scale; + + float local_humidity_influence; + float local_temperature_influence; + float global_terrain_height_influence; + float ecosystem_terrain_height_influence; + + bool ocean_enabled; + bool ocean_mask_binary; + bool ocean_heightmap_flat; + float ocean_depth; + uint4 ocean_color; + + bool hillshade_enabled; + float hillshade_zenith; + float hillshade_azimuth; +}; + +Texture2D input_color : register(t0); +Texture2D input_heightmap: register(t1); +Texture2D input_ocean_mask: register(t2); + +ConstantBuffer jobSettings : register(b0); + +RWTexture2D output_color : register(u0); + +uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) +{ + float4 topRow = lerp(ul, ur, value.x); + float4 bottomRow = lerp(bl, br, value.x); + + return lerp(topRow, bottomRow, value.y); +} + +uint lerp2d(uint ul, uint ur, uint bl, uint br, float2 value) +{ + float topRow = lerp(ul, ur, value.x); + float bottomRow = lerp(bl, br, value.x); + + return lerp(topRow, bottomRow, value.y); +} + +uint4 interpolate_cubic(float4 v0, float4 v1, float4 v2, float4 v3, float fraction) +{ + float4 p = (v3 - v2) - (v0 - v1); + float4 q = (v0 - v1) - p; + float4 r = v2 - v0; + + return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; +} + +uint interpolate_cubic_uint(float v0, float v1, float v2, float v3, float fraction) +{ + float p = (v3 - v2) - (v0 - v1); + float q = (v0 - v1) - p; + float r = v2 - v0; + + return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; +} + +/* Texture2D implementations */ + +uint4 take_sample_nn(Texture2D texture, float2 position, int2 dimensions) +{ + return texture[position % dimensions]; +} + +uint4 take_sample_bilinear(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint4 tl = take_sample_nn(texture, position, dimensions); + uint4 tr = take_sample_nn(texture, position + int2(1, 0), dimensions); + uint4 bl = take_sample_nn(texture, position + int2(0, 1), dimensions); + uint4 br = take_sample_nn(texture, position + int2(1, 1), dimensions); + return lerp2d(tl, tr, bl, br, offset); +} + +uint4 take_sample_bicubic(Texture2D texture, float2 position, int2 dimensions) +{ + float2 offset = position - floor(position); + uint4 samples[4]; + + for (int i = 0; i < 4; ++i) + { + float4 ll = take_sample_nn(texture, position + int2(-1, i - 1), dimensions); + float4 ml = take_sample_nn(texture, position + int2( 0, i - 1), dimensions); + float4 mr = take_sample_nn(texture, position + int2( 1, i - 1), dimensions); + float4 rr = take_sample_nn(texture, position + int2( 2, i - 1), dimensions); + samples[i] = interpolate_cubic(ll, ml, mr, rr, offset.x); + } + + return interpolate_cubic(samples[0], samples[1], samples[2], samples[3], offset.y); +} + +uint4 take_sample(Texture2D texture, float2 position, int2 dimensions, int mode) +{ + if(mode == MODE_NN) { + return take_sample_nn(texture, position, dimensions); + } else if (mode == MODE_BI_LINEAR) { + return take_sample_bilinear(texture, position, dimensions); + } else if (mode == MODE_BI_CUBIC) { + return take_sample_bicubic(texture, position, dimensions); + } else { + return uint4(0, 0, 0, 0); + } +} + +[numthreads(8,8,1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + uint4 out_color = uint4(input_ocean_mask[tid.xy], 0, 0, 255); + + output_color[tid.xy] = out_color; +} \ No newline at end of file diff --git a/starfab/planets/shader.hlsl b/starfab/planets/hlsl/shader.hlsl similarity index 94% rename from starfab/planets/shader.hlsl rename to starfab/planets/hlsl/shader.hlsl index 8115733..58d48ba 100644 --- a/starfab/planets/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -16,8 +16,15 @@ struct RenderJobSettings float global_terrain_height_influence; float ecosystem_terrain_height_influence; + bool ocean_enabled; + bool ocean_mask_binary; + bool ocean_heightmap_flat; float ocean_depth; - uint3 ocean_color; + uint4 ocean_color; + + bool hillshade_enabled; + float hillshade_zenith; + float hillshade_azimuth; }; struct LocalizedWarping @@ -51,6 +58,7 @@ ConstantBuffer jobSettings : register(b0); RWTexture2D output_color : register(u0); RWTexture2D output_heightmap: register(u1); +RWTexture2D output_ocean_mask: register(u2); uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) { @@ -440,6 +448,7 @@ void main(uint3 tid : SV_DispatchThreadID) uint4 out_color = uint4(0, 0, 0, 255); float out_height = global_height * jobSettings.global_terrain_height_influence; // Value + float out_ocean_mask = 0; if (eco_influence.is_override) { out_color.xyz = eco_influence.override; @@ -455,10 +464,33 @@ void main(uint3 tid : SV_DispatchThreadID) out_color.xyz = surface_color.xyz; } + if (out_height < jobSettings.ocean_depth) { out_color.xyz = jobSettings.ocean_color.xyz; } + if (jobSettings.ocean_enabled && out_height < jobSettings.ocean_depth) { + + out_color.xyz = jobSettings.ocean_color.xyz; + + if (jobSettings.ocean_mask_binary) { + out_ocean_mask = 1.0; + } else { + float ocean_max = + - (jobSettings.ecosystem_terrain_height_influence + + jobSettings.global_terrain_height_influence + + jobSettings.ocean_depth); + out_ocean_mask = (out_height + jobSettings.ocean_depth) / ocean_max; + } + + if (jobSettings.ocean_heightmap_flat) { + out_height = jobSettings.ocean_depth; + } + } else { + //Color already applied, no need to do anything + out_ocean_mask = 0; + } + // Squash out_height from meter range to normalized +/- 1.0 range out_height /= (jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence); @@ -472,4 +504,5 @@ void main(uint3 tid : SV_DispatchThreadID) output_color[tid.xy] = out_color; //output_heightmap[tid.xy] = uint4(global_height & 0xFF, (global_height & 0xFF00) >> 8, 0, 255); output_heightmap[tid.xy] = uint4(out_height * 127 + 127, 0, 0, 255); + //output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 127 + 127, 0), 255); } \ No newline at end of file diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index 3c829ec..4f74703 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -6,6 +6,7 @@ from typing import Tuple from PySide6.QtCore import QPointF from scdatatools.engine.chunkfile import ChunkFile, Chunk, JSONChunk, CryXMLBChunk +from scdatatools.engine.cryxml import dict_from_cryxml_file from scdatatools.p4k import P4KInfo from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance @@ -46,9 +47,6 @@ class Planet: self.gpu_resources = {} self.gpu_computer: Compute = None - if self.oc.name.endswith("stanton4.socpak"): - self.load_waypoints() - @staticmethod def position_to_coordinates(x: float, y: float, z: float) -> Tuple[QPointF, float]: xy_len = sqrt(x * x + y * y) @@ -59,10 +57,23 @@ class Planet: return QPointF((lon + 90 + 360) % 360, lat), alt def load_waypoints(self): + # If we already loaded waypoints, don't do anything + if len(self.waypoints) != 0: + return + + # Need to preload *all* entities in the entdata folder to be able to map them + # We used to be able to look up based on the guid, but that's no longer valid + ent_paths = [p.filename for p in self.oc.socpak.filelist if "/entdata/" in p.filename] + ent_infos = [self.oc.socpak.p4k.getinfo(p) for p in ent_paths] + ent_data = [dict_from_cryxml_file(a.open())["Entity"] for a in ent_infos] + ent_map = {ent["@EntityCryGUID"]: ent for ent in ent_data} + for child_name in self.oc.children: - child_soc = self.oc.children[child_name] + child_soc: ObjectContainerInstance = self.oc.children[child_name] coords = self.position_to_coordinates(child_soc.position.x, child_soc.position.y, child_soc.position.z) self.waypoints.append(WaypointData(coords[0], child_soc)) + if child_soc.guid in ent_map: + child_soc.entdata = ent_map[child_soc.guid] def load_data(self) -> object: if self.planet_data: diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 366b015..fc383d7 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -97,22 +97,34 @@ class PlanetRenderer: job_s.interpolation = self.settings.interpolation job_s.render_scale_x = self.settings.resolution * 2 job_s.render_scale_y = self.settings.resolution + job_s.hillshade_enabled = self.settings.hillshade_enabled + job_s.ocean_mask_binary = self.settings.ocean_mask_binary + job_s.planet_radius = self.planet.radius_m job_s.local_humidity_influence = self.planet.humidity_influence job_s.local_temperature_influence = self.planet.temperature_influence job_s.update_buffer(cast(Buffer, self.gpu_resources['settings'])) + # Main render pass computer = self._get_computer() computer.dispatch(self.render_resolution.width() // 8, self.render_resolution.height() // 8, 1) # TODO: Keep this around and render multiple tiles with the same Compute obj + del computer + + if job_s.hillshade_enabled: + # Hill-shade pass + hillshade_compute = self._get_hillshade_computer() + + hillshade_compute.dispatch(self.render_resolution.width() // 8, self.render_resolution.height() // 8, 1) + + del hillshade_compute + out_color: Image = self._read_frame("output_color") out_heightmap: Image = self._read_frame("output_heightmap") - del computer - planet_bouds = self.get_outer_bounds() render_bounds = self.get_bounds_for_render(render_coords) @@ -143,10 +155,30 @@ class PlanetRenderer: ] output_buffers = [ - res['output_color'], res['output_heightmap'] + res['output_color'], res['output_heightmap'], res['output_ocean_mask'] + ] + + return Compute(self.settings.shader_main, + srv=samplers, + cbv=constant_buffers, + uav=output_buffers) + + def _get_hillshade_computer(self) -> Compute: + res = self.gpu_resources + + samplers = [ + res['output_color'], res['output_heightmap'], res['output_ocean_mask'] + ] + + constant_buffers = [ + res['settings'] + ] + + output_buffers = [ + res['output_color'] ] - return Compute(hlsl.compile(self.settings.hlsl), + return Compute(self.settings.shader_hillshade, srv=samplers, cbv=constant_buffers, uav=output_buffers) @@ -230,7 +262,7 @@ class PlanetRenderer: _update_from_ecosystems(self.gpu_resources['ecosystem_heightmaps'], lambda x: x.elevation_bytes) def _cleanup_gpu_output_resources(self): - self._do_cleanup('output_color', 'output_heightmap', 'readback') + self._do_cleanup('output_color', 'output_heightmap', 'output_ocean_mask', 'readback') def _create_gpu_output_resources(self): if 'output_color' in self.gpu_resources: @@ -243,9 +275,11 @@ class PlanetRenderer: out_h = self.render_resolution.height() output_color_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) output_heightmap_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) + output_ocean_mask_texture = Texture2D(out_w, out_h, R8_UINT) self.gpu_resources['output_color'] = output_color_texture self.gpu_resources['output_heightmap'] = output_heightmap_texture + self.gpu_resources['output_ocean_mask'] = output_ocean_mask_texture # NOTE: We will use the same readback buffer to read output_color and output_heightmap # We take output_color's size because it will be 2x the size of the heightmap tex self.gpu_resources['readback'] = Buffer(output_color_texture.size, HEAP_READBACK) diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index a16499e..0372a6b 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -180,16 +180,6 @@ - - - - - - - Waypoints - - - @@ -204,6 +194,36 @@ + + + + Waypoints + + + + + + + + + + Enable Binary Ocean Mask + + + + + + + + + + Enable Hillshade + + + + + +
-- GitLab From 6a5b903e32f956e08fb9b733173a149e8e15f7b0 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sun, 5 May 2024 13:52:04 -0700 Subject: [PATCH 18/49] Remove deprecated dependency to distutils.util.strtobool Per PEP 632, distutils was deprecated in Python 3.10 and removed in Python 3.12. For distutils.util.strtobool the migration recommendation was to reimplement the functionality in own code. Original official strtobool implementation was in https://github.com/pypa/distutils/blob/main/distutils/util.py and has been adapted here. --- starfab/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/starfab/utils.py b/starfab/utils.py index e4f4fef..0d4acc5 100644 --- a/starfab/utils.py +++ b/starfab/utils.py @@ -1,7 +1,6 @@ import importlib import subprocess import sys -from distutils.util import strtobool from scdatatools.engine.textures.converter import ( convert_buffer, @@ -12,6 +11,16 @@ from starfab.gui import qtw from starfab.settings import get_texconv, get_compressonatorcli +def strtobool(val: str): + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1'): + return True + elif val in ('n', 'no', 'f', 'false', 'off', '0'): + return False + else: + raise ValueError(f"Invalid boolean string value: {val!r}") + + def parsebool(val: any): if isinstance(val, bool): return val -- GitLab From 4cef50091a0ae1e22bd18366e9dbe20fd2211127 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sun, 5 May 2024 13:54:23 -0700 Subject: [PATCH 19/49] Remove deprecated dependency to distutils.version.StrictVersion. Replaced with packaging.version.Version per PEP 632. --- starfab/contrib/qtvscodestyle/stylesheet/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starfab/contrib/qtvscodestyle/stylesheet/build.py b/starfab/contrib/qtvscodestyle/stylesheet/build.py index e3ea40d..ae40a32 100644 --- a/starfab/contrib/qtvscodestyle/stylesheet/build.py +++ b/starfab/contrib/qtvscodestyle/stylesheet/build.py @@ -4,7 +4,7 @@ import json import operator import re from dataclasses import dataclass -from distutils.version import StrictVersion +from packaging.version import Version from importlib import resources from pathlib import Path from typing import Optional @@ -53,7 +53,7 @@ def _parse_env_patch(stylesheet: str) -> dict[str, str]: else: raise SyntaxError(f"invalid character in qualifier. Available qualifiers {list(operators.keys())}") - is_true = operators[qualifier](StrictVersion(QT_VERSION), StrictVersion(version)) + is_true = operators[qualifier](Version(QT_VERSION), Version(version)) replacements[match_text] = property["value"] if is_true else "" return replacements -- GitLab From 24dc5beb5fc6400cf9d86dbc876666bb72aa061c Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sun, 5 May 2024 17:34:13 -0700 Subject: [PATCH 20/49] Move planet texture cache files into a .cache subdirectory. --- starfab/planets/ecosystem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/starfab/planets/ecosystem.py b/starfab/planets/ecosystem.py index 1d39e82..f1b0297 100644 --- a/starfab/planets/ecosystem.py +++ b/starfab/planets/ecosystem.py @@ -14,6 +14,9 @@ from starfab.planets.data import LocalClimateData from starfab.utils import image_converter +CACHE_DIR = Path('.cache') + + class EcoSystem: _cache = {} _tex_root = Path("Data/Textures/planets/terrain") @@ -65,7 +68,7 @@ class EcoSystem: # TODO: Use settings to define a cache directory to store these in def _read_with_cache(subpath: str) -> Image: - check_path = Path(subpath).with_suffix(".png") + check_path = (CACHE_DIR / subpath).with_suffix(".png") if not os.path.exists(check_path.parent): os.makedirs(check_path.parent) if check_path.exists(): -- GitLab From e2fe40c8192f6d71f3b39b54072c7c97299d7bb6 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Wed, 8 May 2024 23:57:43 -0700 Subject: [PATCH 21/49] Add basic multi solar system support for the Planet renderer. --- starfab/gui/widgets/pages/page_PlanetView.py | 35 +++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 3473f21..3f52546 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -285,20 +285,37 @@ class PlanetView(qtw.QWidget): def _handle_datacore_loaded(self): logger.info("DataCore loaded") - megamap_pu = self.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] - pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value - try: - pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) - bodies: list[Planet] = self._search_for_bodies(pu_oc) + + for filename in [ + 'libs/foundry/records/megamap/pu_all.xml', # Pyro Tech-Preview builds have had the normal Stanton-only megamap.pu.xml, + # plus a Pyro only megamap, and this one contaning both + 'libs/foundry/records/megamap/megamap.pu.xml', # default megamap record for Stanton only builds + ]: + res = self.sc.datacore.search_filename(filename) + if res: + megamap_pu = res[0] + break + else: + logger.error("No megamap record found") + return + + # megamap_pu = self.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] + # pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value + + bodies: list[Planet] = [] + for solar_system in megamap_pu.properties['SolarSystems']: + pu_socpak = solar_system.properties['ObjectContainers'][0].value + try: + pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) + bodies.extend(self._search_for_bodies(pu_oc)) + except Exception as ex: + logger.exception(ex) + return self.planetComboBox.setModel(self.create_model([ (b.oc.display_name, b) for b in bodies ])) - except Exception as ex: - logger.exception(ex) - return - @staticmethod def _search_for_bodies(socpak: ObjectContainer, search_depth_after_first_body: int = 1): results: list[Planet] = [] -- GitLab From 2dde839fc5e924924b6e314b9fc03b6a08ab101b Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 01:50:48 -0700 Subject: [PATCH 22/49] Move two function calls into try loop to catch exception for logging. --- starfab/gui/widgets/pages/page_PlanetView.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 3f52546..2e973ab 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -227,11 +227,11 @@ class PlanetView(qtw.QWidget): selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) selected_obj.load_data() - self.renderer.set_planet(selected_obj) - self.renderer.set_settings(self.get_settings()) - # TODO: Deal with buffer directly try: + self.renderer.set_planet(selected_obj) + self.renderer.set_settings(self.get_settings()) + layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) render_bounds = self.renderOutput.get_render_coords() self.last_render = self.renderer.render(render_bounds.topLeft()) -- GitLab From b47e855c78cb91093135b7c43141fcff946f3c1c Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 02:04:45 -0700 Subject: [PATCH 23/49] Fix BufferException with very small texture sizes. This was first triggered by trying to render Pyro 1, which has a different data length for its offset_data compared to climate and heightmap. - In _create_planet_gpu_resources() fixed calculation of offset_size and heightmap_size. - In _update_from_bytes() fixed Buffer() size allocation to refer to the actual Resource.size attribute and added a size check. - Added debug logging. --- starfab/planets/planet_renderer.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index fc383d7..4691d68 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -6,11 +6,15 @@ from PIL import Image from PySide6.QtCore import QRectF, QPointF, QSizeF, QSize from . import * +from starfab.log import getLogger from starfab.planets.planet import Planet from starfab.planets.data import LUTData, RenderJobSettings, RenderSettings from starfab.planets.ecosystem import EcoSystem +logger = getLogger(__name__) + + class RenderResult: def __init__(self, settings: RenderJobSettings, @@ -33,13 +37,13 @@ class PlanetRenderer: self.planet: None | Planet = None self.settings: None | RenderSettings = None self.gpu_resources: dict[str, Resource] = {} - self.render_resolution: QSize() = QSize(*buffer_resolution) + self.render_resolution: QSize = QSize(*buffer_resolution) self._create_gpu_output_resources() def set_planet(self, planet: Planet): if planet != self.planet: - self._cleanup_planet_gpu_resources() + # self._cleanup_planet_gpu_resources() self._cleanup_planet_gpu_resources() self.planet = planet self._create_planet_gpu_resources() @@ -198,8 +202,8 @@ class PlanetRenderer: def _create_planet_gpu_resources(self): climate_size = int(math.sqrt(len(self.planet.climate_data) / 4)) - offset_size = int(math.sqrt(len(self.planet.climate_data) / 4)) - heightmap_size = int(math.sqrt(len(self.planet.climate_data) / 4)) + offset_size = int(math.sqrt(len(self.planet.offset_data))) + heightmap_size = int(math.sqrt(len(self.planet.heightmap_data) / 2)) ecosystem_climate_size = self.planet.ecosystems[0].climate_image.width ecosystem_heightmap_size = self.planet.ecosystems[0].elevation_size @@ -208,6 +212,7 @@ class PlanetRenderer: self.gpu_resources['planet_climate'] = Texture2D(climate_size, climate_size, R8G8B8A8_UINT) self.gpu_resources['planet_offsets'] = Texture2D(offset_size, offset_size, R8_UINT) self.gpu_resources['planet_heightmap'] = Texture2D(heightmap_size, heightmap_size, R16_UINT) + logger.debug(f"{climate_size=} {offset_size=} {heightmap_size=}") self.gpu_resources['ecosystem_climates'] = Texture3D(ecosystem_climate_size, ecosystem_climate_size, len(self.planet.ecosystems), R8G8B8A8_UINT) @@ -231,14 +236,17 @@ class PlanetRenderer: # TODO: Can we write directly into the buffer as we generate? staging.upload(bytes(data)) staging.copy_to(gpu_resource) - del staging + # del staging def _update_from_bytes(gpu_resource: Resource, data: bytearray): - staging = Buffer(len(data), HEAP_UPLOAD) + logger.debug(f"_update_from_bytes({gpu_resource.size=} {len(data)=})") + if gpu_resource.size < len(data): + raise IndexError("Resource size (%d) does not match data size (%d)", gpu_resource.size, len(data)) + staging = Buffer(gpu_resource.size, HEAP_UPLOAD) staging.upload(data) staging.copy_to(gpu_resource) - del staging - return gpu_resource + # del staging + # return gpu_resource def _update_from_ecosystems(gpu_resource: Resource, fn_data: Callable[[EcoSystem], Union[bytes, Image.Image]]): staging = Buffer(gpu_resource.size, HEAP_UPLOAD) -- GitLab From 7160e17470b3cdf7be26318cd7eeec8f38ce0aec Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 02:06:45 -0700 Subject: [PATCH 24/49] Handle invalid brush indices when building the LUT. This was noticed when rendering Pyro 4, some of the lut.brush_id values were 255. For now tiles with invalid brush IDs get set to purple, so they will stand out in the resulting render. --- starfab/planets/planet.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index 4f74703..566ff88 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -156,7 +156,10 @@ class Planet: lut.ground_texture_id = self.planet_data["data"]["groundTexIDLUT"][y][x] lut.object_preset_id = self.planet_data["data"]["objectPresetLUT"][y][x] lut.brush_id = self.planet_data["data"]["brushIDLUT"][y][x] - lut.brush_obj = self.brushes[lut.brush_id] + try: + lut.brush_obj = self.brushes[lut.brush_id] + except IndexError as e: + lut.brush_obj = None brush_data = self.planet_data["data"]["brushDataLUT"][y][x] @@ -169,13 +172,17 @@ class Planet: lut.bd_orp_blend_index = brush_data["oprBlendIndex"] lut.bd_texture_layer_index = brush_data["texturLayerIndex"] - lut.bedrockColor = _lerp_color(lut.brush_obj.bedrockGradientColorA, - lut.brush_obj.bedrockGradientColorB, - lut.bd_gradient_val_bedrock / 127) - - lut.surfaceColor = _lerp_color(lut.brush_obj.surfaceGradientColorA, - lut.brush_obj.surfaceGradientColorB, - lut.bd_gradient_val_surface / 127) + if lut.brush_obj: + lut.bedrockColor = _lerp_color(lut.brush_obj.bedrockGradientColorA, + lut.brush_obj.bedrockGradientColorB, + lut.bd_gradient_val_bedrock / 127) + + lut.surfaceColor = _lerp_color(lut.brush_obj.surfaceGradientColorA, + lut.brush_obj.surfaceGradientColorB, + lut.bd_gradient_val_surface / 127) + else: + lut.bedrockColor = [255, 0, 255, 255] # purple placeholder to stand out + lut.surfaceColor = [255, 0, 255, 255] @staticmethod def try_create(oc: ObjectContainerInstance): -- GitLab From ffd3d40573f8d651df3c125eb1fd1bfa5707badf Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 14:10:45 -0700 Subject: [PATCH 25/49] Clarify note on megamap files --- starfab/gui/widgets/pages/page_PlanetView.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 2e973ab..79e40ad 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -287,9 +287,9 @@ class PlanetView(qtw.QWidget): logger.info("DataCore loaded") for filename in [ - 'libs/foundry/records/megamap/pu_all.xml', # Pyro Tech-Preview builds have had the normal Stanton-only megamap.pu.xml, - # plus a Pyro only megamap, and this one contaning both - 'libs/foundry/records/megamap/megamap.pu.xml', # default megamap record for Stanton only builds + 'libs/foundry/records/megamap/pu_all.xml', # Pyro Tech-Preview builds used pu_all.xml containing both Stanton and Pyro + # (however the Stanton-only megamap.pu.xml plus a Pyro-only pyro.xml were also included) + 'libs/foundry/records/megamap/megamap.pu.xml', # default megamap record for Stanton-only builds ]: res = self.sc.datacore.search_filename(filename) if res: -- GitLab From a31814ba5e2533af12581aff84688a6ce4b28a29 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 14:23:50 -0700 Subject: [PATCH 26/49] Fix error message and add extra info logging to _update_from_bytes() --- starfab/planets/planet_renderer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 4691d68..b3a264b 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -241,7 +241,9 @@ class PlanetRenderer: def _update_from_bytes(gpu_resource: Resource, data: bytearray): logger.debug(f"_update_from_bytes({gpu_resource.size=} {len(data)=})") if gpu_resource.size < len(data): - raise IndexError("Resource size (%d) does not match data size (%d)", gpu_resource.size, len(data)) + raise ValueError(f"Resource size ({gpu_resource.size}) does not match data size ({len(data)})") + elif gpu_resource.size > len(data): + logger.info(f"Resource size ({gpu_resource.size}) mismatch with data size ({len(data)})") staging = Buffer(gpu_resource.size, HEAP_UPLOAD) staging.upload(data) staging.copy_to(gpu_resource) -- GitLab From 6d369d3b5e866eca007c2878b2691acf78bb3934 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 15:55:13 -0700 Subject: [PATCH 27/49] Fix compatibility with logging log(). The 'extra' argument is a kw-only argument. --- starfab/log.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/starfab/log.py b/starfab/log.py index 925d705..d9c0216 100644 --- a/starfab/log.py +++ b/starfab/log.py @@ -41,15 +41,16 @@ class ThreadLogger: def __init__(self, name): self.logger = logging.getLogger(name) - def log(self, level, msg, extra=None, *args, **kwargs): - if extra is None: - extra = {} - extra["qThreadName"] = QThread.currentThread().objectName() - if not extra["qThreadName"]: - extra["qThreadName"] = threading.get_ident() + def log(self, level, msg, *args, **kwargs): + kwargs.setdefault('extra', {}) + + kwargs['extra']["qThreadName"] = QThread.currentThread().objectName() + if not kwargs['extra']["qThreadName"]: + kwargs['extra']["qThreadName"] = threading.get_ident() else: - extra["qThreadName"] += f"-{threading.get_ident()}" - self.logger.log(level, msg, extra=extra, *args, **kwargs) + kwargs['extra']["qThreadName"] += f"-{threading.get_ident()}" + + self.logger.log(level, msg, *args, **kwargs) def debug(self, msg, *args, **kwargs): self.log(logging.DEBUG, msg, *args, **kwargs) -- GitLab From a9677815aa18deeee53da5549d46a6fb78986767 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 15:56:20 -0700 Subject: [PATCH 28/49] Add error log message when an EcoSystem definition is not found. --- starfab/planets/ecosystem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/starfab/planets/ecosystem.py b/starfab/planets/ecosystem.py index f1b0297..320a0b9 100644 --- a/starfab/planets/ecosystem.py +++ b/starfab/planets/ecosystem.py @@ -10,10 +10,13 @@ from scdatatools import StarCitizen from scdatatools.engine.textures import unsplit_dds from scdatatools.p4k import P4KInfo +from starfab.log import getLogger from starfab.planets.data import LocalClimateData from starfab.utils import image_converter +logger = getLogger(__name__) + CACHE_DIR = Path('.cache') @@ -24,7 +27,11 @@ class EcoSystem: @staticmethod def find_in_cache_(guid: str): - return EcoSystem._cache[guid] if guid in EcoSystem._cache else None + if guid in EcoSystem._cache: + return EcoSystem._cache[guid] + else: + logger.error("Could not find EcoSystem with guid %r", guid) + return None @staticmethod def read_eco_headers(sc: StarCitizen): -- GitLab From ddc8d6a2d9f003a4dfd460977f8779e643b0d227 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 16:09:16 -0700 Subject: [PATCH 29/49] Add a log warning and debug logging for invalid brushIDLUT values. --- starfab/planets/planet.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/starfab/planets/planet.py b/starfab/planets/planet.py index 566ff88..ac75c52 100644 --- a/starfab/planets/planet.py +++ b/starfab/planets/planet.py @@ -10,12 +10,16 @@ from scdatatools.engine.cryxml import dict_from_cryxml_file from scdatatools.p4k import P4KInfo from scdatatools.sc.object_container import ObjectContainer, ObjectContainerInstance +from starfab.log import getLogger from starfab.planets.data import LUTData, Brush from starfab.planets.ecosystem import EcoSystem from . import * +logger = getLogger(__name__) + + class WaypointData: def __init__(self, point: QPointF, container: ObjectContainerInstance): self.point: QPointF = point @@ -150,6 +154,8 @@ class Planet: _clamp(_lerp_int(a[3], b[3], val), 0, 255) ] + brush_id_errors = [] + for y in range(128): for x in range(128): lut = self.lut[x][y] @@ -160,6 +166,7 @@ class Planet: lut.brush_obj = self.brushes[lut.brush_id] except IndexError as e: lut.brush_obj = None + brush_id_errors.append((x, y, lut.brush_id)) brush_data = self.planet_data["data"]["brushDataLUT"][y][x] @@ -184,6 +191,10 @@ class Planet: lut.bedrockColor = [255, 0, 255, 255] # purple placeholder to stand out lut.surfaceColor = [255, 0, 255, 255] + if brush_id_errors: + logger.warning("One or more tiles with invalid brushIDLUT, used placeholder color.") + logger.debug("brush_id_errors: %r", brush_id_errors) + @staticmethod def try_create(oc: ObjectContainerInstance): json_chunk = Planet.find_planet_data(oc) -- GitLab From 0a715c1e49a12b5f636ed94afffb461acdf6cad5 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Thu, 9 May 2024 19:29:32 -0700 Subject: [PATCH 30/49] Change a print call to logger.info --- starfab/planets/ecosystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starfab/planets/ecosystem.py b/starfab/planets/ecosystem.py index 320a0b9..b1d3cbf 100644 --- a/starfab/planets/ecosystem.py +++ b/starfab/planets/ecosystem.py @@ -94,4 +94,4 @@ class EcoSystem: self.elevation_bytes = bytearray(o.read()) self.elevation_size = int(math.sqrt(len(self.elevation_bytes) / 2)) - print(f"Textures loaded for {self.name}") + logger.info(f"Textures loaded for {self.name}") -- GitLab From 1c128d847bf01f77104052ceacd6011bb7ea41e8 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Fri, 10 May 2024 00:33:02 -0700 Subject: [PATCH 31/49] Properly show and hide waypoints layer. --- starfab/gui/widgets/pages/page_PlanetView.py | 1 + 1 file changed, 1 insertion(+) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 79e40ad..2c9e3a3 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -117,6 +117,7 @@ class PlanetView(qtw.QWidget): self.renderOutput.render_window_moved.connect(self._do_render_window_changed) self.enableGridCheckBox.stateChanged.connect(self.renderOutput.lyr_grid.set_enabled) self.enableCrosshairCheckBox.stateChanged.connect(self.renderOutput.lyr_crosshair.set_enabled) + self.enableWaypointsCheckBox.stateChanged.connect(self.renderOutput.lyr_waypoints.set_enabled) self.renderer.set_settings(self.get_settings()) self._planet_changed() -- GitLab From 603e386525135ca0f8f2755e43f6c2c4c1a8ce83 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Fri, 10 May 2024 00:56:40 -0700 Subject: [PATCH 32/49] Export either the surface or the heightmap depending on layer selection. --- starfab/gui/widgets/pages/page_PlanetView.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 2c9e3a3..4c3bb81 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -250,7 +250,13 @@ class PlanetView(qtw.QWidget): filter="PNG Image (*.png)") filename, filter = edir if filename: - self.last_render.tex_color.save(filename, format="png") + layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) + if layer == 'surface': + self.last_render.tex_color.save(filename, format="png") + elif layer == 'heightmap': + self.last_render.tex_heightmap.save(filename, format="png") + else: + raise ValueError() def _do_crosshair_moved(self, new_position: QPointF): self._update_status() -- GitLab From b0c61b5fd534cb99ae46678fc5a2442e00322ba2 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Fri, 10 May 2024 13:12:28 -0700 Subject: [PATCH 33/49] Add detection for megamap record used in Pyro Playground builds. --- starfab/gui/widgets/pages/page_PlanetView.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 4c3bb81..419213e 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -293,10 +293,17 @@ class PlanetView(qtw.QWidget): def _handle_datacore_loaded(self): logger.info("DataCore loaded") + # TODO: A more graceful selection and/or detection of solar systems may be needed eventually. + # Normal Stanton-only builds use megamap.pu.xml and have only Stanton in their 'SolarSystems' property. + # The Pyro Playground Tech-Preview from late 2023 added a pyro.xml containing only Pyro, but the + # Stanton-only megamap.xml was also still preset (but not functional due to other files missing). + # The server meshing Tech-Preview from March 2024 introduced a pu_all.xml containing both Stanton and Pyro, + # but the other megamaps were also still present. + # For now, check all three megamaps in decreasing order of interest and use the first one found. for filename in [ - 'libs/foundry/records/megamap/pu_all.xml', # Pyro Tech-Preview builds used pu_all.xml containing both Stanton and Pyro - # (however the Stanton-only megamap.pu.xml plus a Pyro-only pyro.xml were also included) - 'libs/foundry/records/megamap/megamap.pu.xml', # default megamap record for Stanton-only builds + 'libs/foundry/records/megamap/pu_all.xml', # Server meshing Tech-Preview builds containing both Stanton and Pyro + 'libs/foundry/records/megamap/pyro.xml', # Pyro playground Tech-Preview + 'libs/foundry/records/megamap/megamap.pu.xml', # default megamap record for Stanton-only builds ]: res = self.sc.datacore.search_filename(filename) if res: -- GitLab From ffadfdcf94bb4ea555d2b857e3558694518860f6 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Fri, 10 May 2024 00:03:25 -0700 Subject: [PATCH 34/49] Enhance the Planet page with a system dropdown and updated labeling. --- starfab/gui/widgets/pages/page_PlanetView.py | 58 +++++++++++++++++--- starfab/resources/ui/PlanetView.ui | 58 ++++++++++++-------- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 419213e..8947c9f 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -1,5 +1,6 @@ import io -from typing import Union, cast +from operator import itemgetter +from typing import Union, cast, NamedTuple from PIL import Image from PySide6.QtCore import Qt, QPointF, QRectF, QItemSelectionModel, QItemSelection @@ -26,12 +27,19 @@ from pathlib import Path logger = getLogger(__name__) +class SolarSystem(NamedTuple): + id: str + name: str + model: QStandardItemModel + + class PlanetView(qtw.QWidget): def __init__(self, sc): super().__init__(parent=None) self.renderButton: QPushButton = None self.exportButton: QPushButton = None + self.systemComboBox: QComboBox = None self.planetComboBox: QComboBox = None self.renderResolutionComboBox: QComboBox = None self.coordinateSystemComboBox: QComboBox = None @@ -52,6 +60,9 @@ class PlanetView(qtw.QWidget): self.starmap = None + self.solar_systems: dict[str, SolarSystem] = {} + self.planets: dict[str, Planet] = {} + self.renderer = PlanetRenderer((2048, 1024)) self.last_render: Union[None, RenderResult] = None @@ -109,6 +120,7 @@ class PlanetView(qtw.QWidget): self.sc.datacore_model.loaded.connect(self._hack_before_load) self.sc.datacore_model.unloading.connect(self._handle_datacore_unloading) + self.systemComboBox.currentIndexChanged.connect(self._system_changed) self.planetComboBox.currentIndexChanged.connect(self._planet_changed) self.renderButton.clicked.connect(self._do_render) self.exportButton.clicked.connect(self._do_export) @@ -122,6 +134,17 @@ class PlanetView(qtw.QWidget): self.renderer.set_settings(self.get_settings()) self._planet_changed() + def _get_selected_planet(self): + planet_id: str = self.planetComboBox.currentData(role=Qt.UserRole) + try: + return self.planets[planet_id] + except KeyError: + return None + + def _system_changed(self): + system_id: str = self.systemComboBox.currentData(role=Qt.UserRole) + self.planetComboBox.setModel(self.solar_systems[system_id].model) + def _planet_changed(self): # TODO: Pre-load ecosystem data here w/ progressbar self._update_waypoints() @@ -155,9 +178,10 @@ class PlanetView(qtw.QWidget): self.renderer.get_bounds_for_render(render_bounds.topLeft())) def _update_waypoints(self): - planet: Planet = self.planetComboBox.currentData(role=Qt.UserRole) + planet = self._get_selected_planet() if not planet: return + planet.load_waypoints() planet.load_waypoints() @@ -225,7 +249,9 @@ class PlanetView(qtw.QWidget): hillshade_enabled, ocean_mask_binary) def _do_render(self): - selected_obj: Planet = self.planetComboBox.currentData(role=Qt.UserRole) + selected_obj = self._get_selected_planet() + if not selected_obj: + return selected_obj.load_data() # TODO: Deal with buffer directly @@ -316,19 +342,35 @@ class PlanetView(qtw.QWidget): # megamap_pu = self.sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] # pu_socpak = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value - bodies: list[Planet] = [] for solar_system in megamap_pu.properties['SolarSystems']: + solar_system_guid = str(solar_system.properties['Record']) + solar_system_record = self.sc.datacore.records_by_guid[solar_system_guid] + pu_socpak = solar_system.properties['ObjectContainers'][0].value + body_records = [] + try: pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) - bodies.extend(self._search_for_bodies(pu_oc)) + + for body in self._search_for_bodies(pu_oc): + id = body.oc.entity_name + label = body.oc.display_name + if label != id: + label += f" ({id})" + body_records.append((label, id)) + + if id in self.planets: + raise KeyError("Duplcate entity_name: %s", id) + self.planets[id] = body except Exception as ex: logger.exception(ex) return - self.planetComboBox.setModel(self.create_model([ - (b.oc.display_name, b) for b in bodies - ])) + self.solar_systems[solar_system_guid] = SolarSystem(solar_system_guid, solar_system_record.name, self.create_model(sorted(body_records, key=itemgetter(1)))) + + self.systemComboBox.setModel( + self.create_model([(solar_system.name, solar_system.id) for guid, solar_system in self.solar_systems.items()]) + ) @staticmethod def _search_for_bodies(socpak: ObjectContainer, search_depth_after_first_body: int = 1): diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 0372a6b..2e46881 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -83,111 +83,121 @@ + + + System + + + + + + + Planet - + - + Render Resolution - + - + Coordinate System - + - + Sample Mode - + - + Output Resolution - + - + Display Mode - + - + Display Layer - + - + Enable Grid - + true - + Enable Crosshair - + true - + Enable Waypoints - + true @@ -201,27 +211,27 @@ - + - + Enable Binary Ocean Mask - + - + Enable Hillshade - + -- GitLab From fe82e19b0d864b73f1a34b10d849b797c34cf927 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sat, 11 May 2024 14:30:37 -0700 Subject: [PATCH 35/49] Fix minor merge error on label placement --- starfab/resources/ui/PlanetView.ui | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 2e46881..6442c7e 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -204,17 +204,17 @@ - - + + - Waypoints + Enable Hillshade - - + + - + Enable Binary Ocean Mask @@ -224,15 +224,15 @@ - - + + - Enable Hillshade + Waypoints - - + + -- GitLab From 492db48ec194357ca67352281b98bf8ce41d5955 Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sat, 11 May 2024 14:51:38 -0700 Subject: [PATCH 36/49] Minor cleanup and error checking for solar system dropdown model --- starfab/gui/widgets/pages/page_PlanetView.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 8947c9f..de88229 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -60,8 +60,8 @@ class PlanetView(qtw.QWidget): self.starmap = None - self.solar_systems: dict[str, SolarSystem] = {} - self.planets: dict[str, Planet] = {} + self.solar_systems: dict[str, SolarSystem] = {} # mapping of solar system GUID to SolarSystem namedtuple + self.planets: dict[str, Planet] = {} # mapping of planet entity_name to Planet object self.renderer = PlanetRenderer((2048, 1024)) self.last_render: Union[None, RenderResult] = None @@ -347,7 +347,7 @@ class PlanetView(qtw.QWidget): solar_system_record = self.sc.datacore.records_by_guid[solar_system_guid] pu_socpak = solar_system.properties['ObjectContainers'][0].value - body_records = [] + planet_records: list[tuple[str, str]] = [] # records for the model, tuples of label and id/entity_name try: pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) @@ -356,8 +356,10 @@ class PlanetView(qtw.QWidget): id = body.oc.entity_name label = body.oc.display_name if label != id: - label += f" ({id})" - body_records.append((label, id)) + # display_name falls back to entity_name if missing, + # only append the entity_name if it's different + label += f" - {id}" + planet_records.append((label, id)) if id in self.planets: raise KeyError("Duplcate entity_name: %s", id) @@ -366,7 +368,13 @@ class PlanetView(qtw.QWidget): logger.exception(ex) return - self.solar_systems[solar_system_guid] = SolarSystem(solar_system_guid, solar_system_record.name, self.create_model(sorted(body_records, key=itemgetter(1)))) + if solar_system_guid in self.solar_systems: + raise KeyError("Duplicate solar system GUID: %s", solar_system_guid) + self.solar_systems[solar_system_guid] = SolarSystem( + solar_system_guid, + solar_system_record.name, + self.create_model(sorted(planet_records, key=itemgetter(1))) + ) self.systemComboBox.setModel( self.create_model([(solar_system.name, solar_system.id) for guid, solar_system in self.solar_systems.items()]) -- GitLab From 4fffcd1fbdb4c56c7316613d3ef9582163b3be5a Mon Sep 17 00:00:00 2001 From: Dominik Lehner Date: Sat, 11 May 2024 14:59:06 -0700 Subject: [PATCH 37/49] Fix minor merge duplication and cleanup. --- starfab/gui/widgets/pages/page_PlanetView.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index de88229..019ec6a 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -183,8 +183,6 @@ class PlanetView(qtw.QWidget): return planet.load_waypoints() - planet.load_waypoints() - waypoint_records = [(wp.container.display_name, wp) for wp in planet.waypoints] waypoint_model = self.create_model(waypoint_records) waypoint_selection = QItemSelectionModel(waypoint_model) @@ -353,17 +351,16 @@ class PlanetView(qtw.QWidget): pu_oc = self.sc.oc_manager.load_socpak(pu_socpak) for body in self._search_for_bodies(pu_oc): - id = body.oc.entity_name label = body.oc.display_name - if label != id: + if label != body.oc.entity_name: # display_name falls back to entity_name if missing, # only append the entity_name if it's different - label += f" - {id}" - planet_records.append((label, id)) + label += f" - {body.oc.entity_name}" + planet_records.append((label, body.oc.entity_name)) - if id in self.planets: - raise KeyError("Duplcate entity_name: %s", id) - self.planets[id] = body + if body.oc.entity_name in self.planets: + raise KeyError("Duplcate entity_name: %s", body.oc.entity_name) + self.planets[body.oc.entity_name] = body except Exception as ex: logger.exception(ex) return -- GitLab From aa4ceca5bb8fa9e83dfd20e27ae8563a4646b2e7 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sun, 12 May 2024 00:56:20 -0400 Subject: [PATCH 38/49] Fixed some issues with ocean depth calculation, and cleaned up code. --- starfab/planets/hlsl/shader.hlsl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 58d48ba..c1c7e26 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -428,6 +428,7 @@ void main(uint3 tid : SV_DispatchThreadID) float2 projected_size = float2(6000, 6000) * terrain_scaling; float2 physical_size = float2(4000, 4000) * terrain_scaling; float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); + float2 max_deformation = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; // Calculate normalized position in the world (ie: 0,0 = top-left, 1,1 = bottom-right) float2 normalized_position = tid.xy / float2(clim_sz) / jobSettings.render_scale; @@ -476,11 +477,11 @@ void main(uint3 tid : SV_DispatchThreadID) if (jobSettings.ocean_mask_binary) { out_ocean_mask = 1.0; } else { - float ocean_max = - - (jobSettings.ecosystem_terrain_height_influence + - jobSettings.global_terrain_height_influence + - jobSettings.ocean_depth); - out_ocean_mask = (out_height + jobSettings.ocean_depth) / ocean_max; + float ocean_bottom = -max_deformation; + float relative_depth = jobSettings.ocean_depth - out_height; + float ocean_max_depth = jobSettings.ocean_depth - ocean_bottom; + + out_ocean_mask = relative_depth / ocean_max_depth; } if (jobSettings.ocean_heightmap_flat) { @@ -492,7 +493,7 @@ void main(uint3 tid : SV_DispatchThreadID) } // Squash out_height from meter range to normalized +/- 1.0 range - out_height /= (jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence); + out_height /= max_deformation; // DEBUG: Grid rendering int2 cell_position = int2(normalized_position * out_sz * jobSettings.render_scale) % jobSettings.render_scale; @@ -504,5 +505,5 @@ void main(uint3 tid : SV_DispatchThreadID) output_color[tid.xy] = out_color; //output_heightmap[tid.xy] = uint4(global_height & 0xFF, (global_height & 0xFF00) >> 8, 0, 255); output_heightmap[tid.xy] = uint4(out_height * 127 + 127, 0, 0, 255); - //output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 127 + 127, 0), 255); + output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 255, 0), 255); } \ No newline at end of file -- GitLab From 824aef12f61148022b68421b615310882a6b163c Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 13 May 2024 10:49:11 -0400 Subject: [PATCH 39/49] Add heightmap bit depth selection. --- starfab/planets/data.py | 7 ++-- starfab/planets/hlsl/hillshade.hlsl | 7 +++- starfab/planets/hlsl/shader.hlsl | 52 ++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/starfab/planets/data.py b/starfab/planets/data.py index db6fb5d..09cb3f0 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -27,7 +27,7 @@ class LUTData: class RenderJobSettings: # NOTE: Bools are GPU-register aligned, so need to be 4 bytes, not 1 # so we pack them as i instead of ? - PACK_STRING: str = "5f3i4f3i1f5i2f" + PACK_STRING: str = "5f3i4f3i1f5i2f1i" PACK_LENGTH: int = struct.calcsize(PACK_STRING) def __init__(self): @@ -57,6 +57,8 @@ class RenderJobSettings: self.hillshade_zenith: float = 45 self.hillshade_azimuth: float = 135 + self.heightmap_bit_depth: int = 16 + def pack(self) -> bytes: return struct.pack(RenderJobSettings.PACK_STRING, self.offset_x, self.offset_y, self.size_x, self.size_y, @@ -66,7 +68,8 @@ class RenderJobSettings: self.global_terrain_height_influence, self.ecosystem_terrain_height_influence, self.ocean_enabled, self.ocean_mask_binary, self.ocean_heightmap_flat, self.ocean_depth, *self.ocean_color, - self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth) + self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth, + self.heightmap_bit_depth) def update_buffer(self, buffer_gpu: Buffer): data = self.pack() diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl index edb2b05..9be2192 100644 --- a/starfab/planets/hlsl/hillshade.hlsl +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -25,6 +25,8 @@ struct RenderJobSettings bool hillshade_enabled; float hillshade_zenith; float hillshade_azimuth; + + int heightmap_bit_depth; }; Texture2D input_color : register(t0); @@ -121,5 +123,8 @@ void main(uint3 tid : SV_DispatchThreadID) { uint4 out_color = uint4(input_ocean_mask[tid.xy], 0, 0, 255); - output_color[tid.xy] = out_color; + if(input_ocean_mask[tid.xy] != 0) + return; + + //output_color[tid.xy] = out_color; } \ No newline at end of file diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index c1c7e26..932bb95 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -25,6 +25,8 @@ struct RenderJobSettings bool hillshade_enabled; float hillshade_zenith; float hillshade_azimuth; + + int heightmap_bit_depth; }; struct LocalizedWarping @@ -415,6 +417,45 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa return result; } +uint4 PackFloatToUInt4(float value, int bit_depth) +{ + if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) + return uint4(0, 0, 0, 0); + + // Clamp the input value to the range [-1.0, 1.0] + value = clamp(value, -1.0f, 1.0f); + + // Map the range [-1.0, 1.0] to the range [0.0, 1.0] + value = value * 0.5f + 0.5f; + + // Convert the float value to 32-bit unsigned integer + float factor = (1 << bit_depth) - 1.0f; + uint intValue = uint(value * factor); + + // Pack the unsigned integer value into a uint4 + uint4 packedValue; + + packedValue.x = (intValue >> 0) & 0xFF; + + if (bit_depth == 8) { //Render as greyscale + packedValue.y = (intValue >> 0) & 0xFF; + packedValue.z = (intValue >> 0) & 0xFF; + packedValue.w = 255; + } else { + //Valid for 16, 24 and 32 bit + packedValue.y = (intValue >> 8) & 0xFF; + packedValue.z = (intValue >> 16) & 0xFF; + + if (bit_depth == 32) { + packedValue.w = (intValue >> 24) & 0xFF; + } else { + packedValue.w = 255; + } + } + + return packedValue; +} + // TODO: Use the z-thread ID for doing sub-pixel searching [numthreads(8,8,1)] void main(uint3 tid : SV_DispatchThreadID) @@ -428,7 +469,7 @@ void main(uint3 tid : SV_DispatchThreadID) float2 projected_size = float2(6000, 6000) * terrain_scaling; float2 physical_size = float2(4000, 4000) * terrain_scaling; float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); - float2 max_deformation = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; + float max_deformation = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; // Calculate normalized position in the world (ie: 0,0 = top-left, 1,1 = bottom-right) float2 normalized_position = tid.xy / float2(clim_sz) / jobSettings.render_scale; @@ -465,11 +506,6 @@ void main(uint3 tid : SV_DispatchThreadID) out_color.xyz = surface_color.xyz; } - - if (out_height < jobSettings.ocean_depth) { - out_color.xyz = jobSettings.ocean_color.xyz; - } - if (jobSettings.ocean_enabled && out_height < jobSettings.ocean_depth) { out_color.xyz = jobSettings.ocean_color.xyz; @@ -503,7 +539,7 @@ void main(uint3 tid : SV_DispatchThreadID) } output_color[tid.xy] = out_color; - //output_heightmap[tid.xy] = uint4(global_height & 0xFF, (global_height & 0xFF00) >> 8, 0, 255); - output_heightmap[tid.xy] = uint4(out_height * 127 + 127, 0, 0, 255); + + output_heightmap[tid.xy] = PackFloatToUInt4(out_height, jobSettings.heightmap_bit_depth); output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 255, 0), 255); } \ No newline at end of file -- GitLab From a1dc8b0b0b75acc001abef3bfd2265c8fbe12b09 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 13 May 2024 11:20:09 -0400 Subject: [PATCH 40/49] Add heightmap bit depth selection to the UI, and wire it up fully. --- starfab/gui/widgets/pages/page_PlanetView.py | 12 +++++- starfab/planets/data.py | 4 +- starfab/planets/planet_renderer.py | 1 + starfab/resources/ui/PlanetView.ui | 42 ++++++++++++-------- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 019ec6a..630ef72 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -45,6 +45,7 @@ class PlanetView(qtw.QWidget): self.coordinateSystemComboBox: QComboBox = None self.sampleModeComboBox: QComboBox = None self.outputResolutionComboBox: QComboBox = None + self.heightmapBitDepthComboBox: QComboBox = None self.displayModeComboBox: QComboBox = None self.displayLayerComboBox: QComboBox = None self.renderOutput: QPlanetViewer = None @@ -100,6 +101,13 @@ class PlanetView(qtw.QWidget): ])) self.outputResolutionComboBox.currentIndexChanged.connect(self._display_resolution_changed) + self.heightmapBitDepthComboBox.setModel(self.create_model([ + ("8-Bit Greyscale", 8), + ("16-Bit (R,G)", 16), + ("24-Bit (R,G,B)", 24), + ("32-Bit (R,G,B,A)", 32) + ])) + self.displayModeComboBox.setModel(self.create_model([ ("Pixel-Perfect", qtc.Qt.FastTransformation), ("Smooth", qtc.Qt.SmoothTransformation) @@ -237,6 +245,7 @@ class PlanetView(qtw.QWidget): coordinates = self.coordinateSystemComboBox.currentData(role=Qt.UserRole) interpolation = self.sampleModeComboBox.currentData(role=Qt.UserRole) resolution = self.outputResolutionComboBox.currentData(role=Qt.UserRole) + hm_bitdepth = self.heightmapBitDepthComboBox.currentData(role=Qt.UserRole) main_shader = self._get_shader("shader.hlsl") hillshade_shader = self._get_shader("hillshade.hlsl") hillshade_enabled = self.enableHillshadeCheckBox.isChecked() @@ -244,7 +253,8 @@ class PlanetView(qtw.QWidget): return RenderSettings(True, scale, coordinates, main_shader, hillshade_shader, interpolation, resolution, - hillshade_enabled, ocean_mask_binary) + hillshade_enabled, ocean_mask_binary, + hm_bitdepth) def _do_render(self): selected_obj = self._get_selected_planet() diff --git a/starfab/planets/data.py b/starfab/planets/data.py index 09cb3f0..d27ba6c 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -134,7 +134,8 @@ class RenderSettings: def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, shader_main: str, shader_hillshade: str, interpolation: int, output_resolution: Tuple[int, int], - hillshade_enabled: bool, ocean_mask_binary: bool): + hillshade_enabled: bool, ocean_mask_binary: bool, + heightmap_bit_depth: int): self.gpu = gpu self.resolution = resolution self.coordinate_mode = coordinate_mode @@ -144,3 +145,4 @@ class RenderSettings: self.output_resolution = output_resolution self.hillshade_enabled = hillshade_enabled self.ocean_mask_binary = ocean_mask_binary + self.heightmap_bit_depth = heightmap_bit_depth diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index b3a264b..a831f49 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -103,6 +103,7 @@ class PlanetRenderer: job_s.render_scale_y = self.settings.resolution job_s.hillshade_enabled = self.settings.hillshade_enabled job_s.ocean_mask_binary = self.settings.ocean_mask_binary + job_s.heightmap_bit_depth = self.settings.heightmap_bit_depth job_s.planet_radius = self.planet.radius_m job_s.local_humidity_influence = self.planet.humidity_influence diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 6442c7e..173fa43 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -142,98 +142,108 @@ - + Display Mode - + - + Display Layer - + - + Enable Grid - + true - + Enable Crosshair - + true - + Enable Waypoints - + true - + Enable Hillshade - + - + Enable Binary Ocean Mask - + - + Waypoints - + + + + + Heightmap Bit Depth + + + + + + -- GitLab From 8da8ab3998a2ad88c4ab0932264f3c83350add4c Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 13 May 2024 16:58:29 -0400 Subject: [PATCH 41/49] Added super basic hill shading pass. Doesn't seem to add as much detail as I remember. --- starfab/planets/hlsl/hillshade.hlsl | 93 ++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl index 9be2192..c58add8 100644 --- a/starfab/planets/hlsl/hillshade.hlsl +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -118,13 +118,102 @@ uint4 take_sample(Texture2D texture, float2 position, int2 dimensions, in } } +float UnpackUInt4ToFloat(uint4 packedValue, int bit_depth) +{ + // Ensure the bit depth is valid + if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) + return 0.0; + + // Combine the components of packedValue to reconstruct the float value + float factor = (1L << bit_depth) - 1.0f; + float reconstructedValue = 0.0; + + if (bit_depth == 8) { // Greyscale + reconstructedValue = packedValue.x / factor; + } else { + // Combine components based on bit depth + reconstructedValue += packedValue.x / factor; + reconstructedValue += packedValue.y / factor * 256.0; + reconstructedValue += packedValue.z / factor * 65536.0; + + if (bit_depth == 32) { + reconstructedValue += packedValue.w / factor * 16777216.0; + } + } + + // Map the range [0.0, 1.0] back to [-1.0, 1.0] + reconstructedValue = reconstructedValue * 2.0f - 1.0f; + + return reconstructedValue; +} + +float read_height(uint2 coordinate, int2 relative, int2 dimensions) +{ + uint4 samp = take_sample_nn(input_heightmap, coordinate + relative, dimensions); + float max_deform = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; + return UnpackUInt4ToFloat(samp, jobSettings.heightmap_bit_depth) * max_deform; +} + + + [numthreads(8,8,1)] void main(uint3 tid : SV_DispatchThreadID) { - uint4 out_color = uint4(input_ocean_mask[tid.xy], 0, 0, 255); + uint2 hm_sz; input_heightmap.GetDimensions(hm_sz.x, hm_sz.y); if(input_ocean_mask[tid.xy] != 0) return; - //output_color[tid.xy] = out_color; + float ZFactor = 1.0f; + + float a = read_height(tid.xy, int2(1, 1), hm_sz); + float b = read_height(tid.xy, int2(0, -1), hm_sz); + float c = read_height(tid.xy, int2(1, -1), hm_sz); + float d = read_height(tid.xy, int2(-1, 0), hm_sz); + float f = read_height(tid.xy, int2(1, 0), hm_sz); + float g = read_height(tid.xy, int2(-1, 1), hm_sz); + float h = read_height(tid.xy, int2(0, 1), hm_sz); + float i = read_height(tid.xy, int2(1, 1), hm_sz); + + //Cellsize needs to be the same scale as the X/Y distance + //This calculation does not take into account projection warping at all + float planet_circ_m = PI * 2 * jobSettings.planet_radius; + float cellsize_m = planet_circ_m / (hm_sz.x * jobSettings.render_scale * 2); + + float dzdx = ((c + 2 * f + i) - (a + 2 * d + g)) / (8 * cellsize_m); + float dzdy = ((g + 2 * h + i) - (a + 2 * b + c)) / (8 * cellsize_m); + float aspect = 0.0; + + float slope = atan(ZFactor * sqrt(dzdx * dzdx + dzdy * dzdy)); + + if (dzdx != 0) + { + aspect = atan2(dzdy, -dzdx); + if (aspect < 0) + aspect += PI * 2; + } + else + { + if (dzdy > 0) + aspect = PI / 2; + else if (dzdy < 0) + aspect = (PI * 2) - (PI / 2); + } + + //Normalize slope to +/- 1 + slope = min(PI, max(-PI, slope)) / PI; + + int hillshade_amount = 255 * ( + (cos(jobSettings.hillshade_zenith) * cos(slope)) + + (sin(jobSettings.hillshade_zenith) * sin(slope) * cos(jobSettings.hillshade_azimuth - aspect))); + //Tone down hillshade, and make centered around 0 + hillshade_amount = (hillshade_amount - 127) / 4; + + uint3 final_color = output_color[tid.xy].xyz; + + final_color.x = max(0, min(255, final_color.x + hillshade_amount)); + final_color.y = max(0, min(255, final_color.y + hillshade_amount)); + final_color.z = max(0, min(255, final_color.z + hillshade_amount)); + + output_color[tid.xy].xyz = final_color; } \ No newline at end of file -- GitLab From 2cdeca86f8637abe2cc29a622f63548fe681e12e Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Tue, 14 May 2024 23:13:42 -0400 Subject: [PATCH 42/49] Add ocean mask as exportable layer. --- starfab/gui/widgets/pages/page_PlanetView.py | 13 +++++++---- starfab/gui/widgets/planets/planet_viewer.py | 2 ++ starfab/planets/planet_renderer.py | 24 +++++++++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 630ef72..d8add8e 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -116,7 +116,8 @@ class PlanetView(qtw.QWidget): self.displayLayerComboBox.setModel(self.create_model([ ("Surface", "surface"), - ("Heightmap", "heightmap") + ("Heightmap", "heightmap"), + ("Ocean Mask", "ocean_mask") ])) self.displayLayerComboBox.currentIndexChanged.connect(self._display_layer_changed) @@ -233,11 +234,13 @@ class PlanetView(qtw.QWidget): return model - def shader_path(self, name) -> Path: + @staticmethod + def shader_path(name) -> Path: return Path(__file__) / f'../../../../planets/hlsl/{name}' - def _get_shader(self, name): - with io.open(self.shader_path(name), "r") as shader: + @staticmethod + def _get_shader(name): + with io.open(PlanetView.shader_path(name), "r") as shader: return hlsl.compile(shader.read()) def get_settings(self): @@ -289,6 +292,8 @@ class PlanetView(qtw.QWidget): self.last_render.tex_color.save(filename, format="png") elif layer == 'heightmap': self.last_render.tex_heightmap.save(filename, format="png") + elif layer == 'ocean_mask': + self.last_render.tex_oceanmask.save(filename, format="png") else: raise ValueError() diff --git a/starfab/gui/widgets/planets/planet_viewer.py b/starfab/gui/widgets/planets/planet_viewer.py index 67176c3..3196775 100644 --- a/starfab/gui/widgets/planets/planet_viewer.py +++ b/starfab/gui/widgets/planets/planet_viewer.py @@ -209,6 +209,8 @@ class QPlanetViewer(qtw.QGraphicsView): img = self._render_result.tex_color elif layer == "heightmap": img = self._render_result.tex_heightmap + elif layer == "ocean_mask": + img = self._render_result.tex_oceanmask else: raise Exception(f"Unknown layer: {layer}") diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index a831f49..3562f4d 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -1,6 +1,6 @@ import gc import math -from typing import Union, Callable, Tuple, cast +from typing import Union, Callable, Tuple, cast, Literal from PIL import Image from PySide6.QtCore import QRectF, QPointF, QSizeF, QSize @@ -20,15 +20,21 @@ class RenderResult: settings: RenderJobSettings, tex_color: Image.Image, tex_heightmap: Image.Image, + tex_oceanmask: Image.Image, splat_dimensions: Tuple[float, float], coordinate_bounds_planet: QRectF, coordinate_bounds: QRectF): self.settings = settings self.tex_color: Image.Image = tex_color self.tex_heightmap: Image.Image = tex_heightmap + self.tex_oceanmask: Image.Image = tex_oceanmask self.splat_resolution = splat_dimensions self.coordinate_bounds = coordinate_bounds self.coordinate_bounds_planet = coordinate_bounds_planet + start_norm_x = (coordinate_bounds.left() - coordinate_bounds_planet.left()) / coordinate_bounds_planet.width() + start_norm_y = (coordinate_bounds.bottom() - coordinate_bounds_planet.bottom()) / coordinate_bounds_planet.height() + size_norm = coordinate_bounds.height() / coordinate_bounds_planet.height() + self.coordinate_normalized = QRectF(QPointF(start_norm_x, start_norm_y), QSizeF(size_norm, size_norm)) class PlanetRenderer: @@ -127,23 +133,25 @@ class PlanetRenderer: del hillshade_compute - out_color: Image = self._read_frame("output_color") - out_heightmap: Image = self._read_frame("output_heightmap") + out_color: Image = self._read_frame("output_color", "RGBA") + out_heightmap: Image = self._read_frame("output_heightmap", "RGBA") + out_oceanmask: Image = self._read_frame("output_ocean_mask", "L") planet_bouds = self.get_outer_bounds() render_bounds = self.get_bounds_for_render(render_coords) - return RenderResult(job_s, out_color, out_heightmap, self.render_resolution, - planet_bouds, render_bounds) + return RenderResult(job_s, out_color, out_heightmap, out_oceanmask, + self.render_resolution, planet_bouds, render_bounds) - def _read_frame(self, resource_name: str) -> Image: - readback: Buffer = cast(Buffer, self.gpu_resources['readback']) + def _read_frame(self, resource_name: str, mode: Literal['L', 'RGBA']) -> Image: + resource: Resource = self.gpu_resources['readback'] + readback: Buffer = cast(Buffer, resource) destination: Texture2D = cast(Texture2D, self.gpu_resources[resource_name]) destination.copy_to(readback) output_bytes = readback.readback() del readback - return Image.frombuffer('RGBA', + return Image.frombuffer(mode, (destination.width, destination.height), output_bytes) -- GitLab From 10f7edfd9e1342e7dd2577df075a651d3e948d43 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Wed, 15 May 2024 03:42:14 -0400 Subject: [PATCH 43/49] Added simple proof-of-concept tile server that can serve up surface and heightmap data in realtime. --- starfab/planets/planet_renderer.py | 2 +- starfab/planets/planet_server.py | 241 +++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 starfab/planets/planet_server.py diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 3562f4d..045b7af 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -32,7 +32,7 @@ class RenderResult: self.coordinate_bounds = coordinate_bounds self.coordinate_bounds_planet = coordinate_bounds_planet start_norm_x = (coordinate_bounds.left() - coordinate_bounds_planet.left()) / coordinate_bounds_planet.width() - start_norm_y = (coordinate_bounds.bottom() - coordinate_bounds_planet.bottom()) / coordinate_bounds_planet.height() + start_norm_y = (coordinate_bounds.top() - coordinate_bounds_planet.top()) / coordinate_bounds_planet.height() size_norm = coordinate_bounds.height() / coordinate_bounds_planet.height() self.coordinate_normalized = QRectF(QPointF(start_norm_x, start_norm_y), QSizeF(size_norm, size_norm)) diff --git a/starfab/planets/planet_server.py b/starfab/planets/planet_server.py new file mode 100644 index 0000000..e5bea1f --- /dev/null +++ b/starfab/planets/planet_server.py @@ -0,0 +1,241 @@ +import asyncio +from threading import Lock + +from PySide6.QtCore import QPointF +from flask import Flask, send_file, request +from flask_cors import CORS +from io import BytesIO +from PIL import Image +from math import pi, atan, sinh, degrees, floor + +from scdatatools import StarCitizen + +from starfab.planets.data import RenderSettings +from starfab.planets.ecosystem import EcoSystem +from starfab.planets.planet_renderer import PlanetRenderer, RenderResult + +app = Flask(__name__) +CORS(app) + +from starfab.planets import planet_renderer +from starfab.planets.planet import Planet +from starfab.gui.widgets.pages.page_PlanetView import PlanetView + +def tile_to_lon_lat(z, x, y): + n = 2.0 ** z + lon_deg = x / n * 360.0 - 180.0 + lat_rad = atan(sinh(pi * (1 - 2 * y / n))) + lat_deg = degrees(lat_rad) + return lon_deg, lat_deg + + +def create_gradient_tile(z, x, y): + # Assuming each tile is 256x256 pixels + tile_size = 256 + map_size = 2 ** z * tile_size + + # Create a gradient image + image = Image.new('RGB', (tile_size, tile_size)) + pixels = image.load() + + for i in range(tile_size): + for j in range(tile_size): + u = (x * tile_size + i) / map_size # U coordinate + v = (y * tile_size + j) / map_size # V coordinate + + lon = u * 360.0 - 180.0 + lat_rad = atan(sinh(pi * (1 - 2 * v))) + lat = degrees(lat_rad) + + red = int((1 - v) * 255) # Red based on V (inverse of latitude) + green = int(u * 255) # Green based on U (longitude) + blue = 0 # No blue channel + pixels[i, j] = (red, green, blue) + + return image + + +def get_rendersize_from_zoom(z): + tile_size = 256 + map_size = 2 ** z * tile_size + planet_size = renderer.planet.tile_count + + if map_size <= planet_size: + return 1 + else: + return int(map_size / planet_size) + + +def get_xyz_normalized(z, x, y) -> QPointF: + tile_size = 256 + map_size = 2 ** z * tile_size + + n_x = (x * tile_size) / map_size # U coordinate + n_y = (y * tile_size) / map_size # V coordinate + + return QPointF(n_x, n_y) + + +def find_render(z, x, y) -> RenderResult: + for cached_result in result_stack: + if contains_tile(cached_result[1], z, x, y): + # print("Cache HIT!") + cached_result[0] += 1 + return cached_result[1] + + # print("Cache MISS!") + new_render = render_tile(z, x, y) + + if not contains_tile(new_render, z, x, y): + #breakpoint() + # TODO: Something still isn't quite right... + pass + + if len(result_stack) == 10: + # Remove the cache entry with the fewest hits + result_stack.remove(min(result_stack, key=lambda r: r[0])) + + result_stack.append([1, new_render]) + + return new_render + + +def render_tile(z, x, y) -> RenderResult: + renderscale = get_rendersize_from_zoom(z) + render_settings.resolution = renderscale + factor = 2 ** z + + tile_norm = get_xyz_normalized(z, x, y) + + # round x and y to the nearest 2^z interval + render_x = floor(tile_norm.x() * renderscale) / renderscale + render_y = floor(tile_norm.y() * renderscale) / renderscale + + # print(f"x={x},y={y},z={z} => {render_x},{render_y},{tile_norm}") + + if factor * 256 <= render_settings.output_resolution[1]: + return do_render(0, 0) + + return do_render(render_x, render_y) + + +def contains_tile(result: RenderResult, z, x, y): + tile_size = 256 + map_size = 2 ** z * tile_size + + # Even the smallest buffer fits 4 tiles tall, so is good to 3 zoom levels by itself + if result.tex_color.height >= map_size: + return True + + planet_scale = result.coordinate_bounds_planet.height() / result.coordinate_bounds.height() + planet_size = result.tex_color.height * planet_scale + + # If this is intended for a different zoom level + if map_size != planet_size: + return False + + n_r = result.coordinate_normalized + n_x = (x * tile_size) / map_size # U coordinate + n_y = (y * tile_size) / map_size # V coordinate + + if n_r.left() <= n_x < n_r.right() and \ + n_r.top() <= n_y < n_r.bottom(): + return True + else: + return False + + +def extract_tile(result: RenderResult, z, x, y, layer: str): + tile_size = 256 + map_size = 2 ** z * tile_size + planet_scale = result.coordinate_bounds_planet.height() / result.coordinate_bounds.height() + planet_size = result.tex_color.height * planet_scale + + n_r = result.coordinate_normalized + n_t = get_xyz_normalized(z, x, y) + + sample_offset_x = (n_t.x() - n_r.left()) / n_r.width() * result.tex_color.width + sample_offset_y = (n_t.y() - n_r.top()) / n_r.height() * result.tex_color.height + + # Outermost zoom levels + # source_box is left, upper, *right*, *bottom* + if map_size <= planet_size: + upscale = planet_size / map_size + source_box = (sample_offset_x, sample_offset_y, + sample_offset_x + (tile_size * 2 * upscale), sample_offset_y + (tile_size * upscale)) + else: + source_box = (sample_offset_x, sample_offset_y, + sample_offset_x + (tile_size * 2), sample_offset_y + tile_size) + target_size = (tile_size, tile_size) + + # print(f"x={x},y={y},z={z}") + # print(f"n_r={n_r}, n_t={n_t}") + # print(f"{result.coordinate_bounds}, {result.coordinate_bounds_planet} => {result.coordinate_normalized}") + # print(source_box) + source_image: Image + + if layer == "surface": + source_image = result.tex_color + elif layer == "heightmap": + source_image = result.tex_heightmap + else: + raise Exception(f"Unknown layer: {layer}") + + return source_image.resize(target_size, box=source_box) + + +def do_render(x, y): + with render_lock: + # TODO: Check the cache first + renderer.set_settings(render_settings) + result = renderer.render(QPointF(x * 360, -90 + y * 180)) + return result + + +@app.route('////.png') +def get_tile(layer, z, x, y): + result: RenderResult = find_render(z, x, y) + image = extract_tile(result, z, x, y, layer) + + # Convert the image to bytes + img_bytes = BytesIO() + image.save(img_bytes, format='PNG') + img_bytes.seek(0) + + # Return the image as a PNG file + return send_file(img_bytes, mimetype='image/png') + +print("SC Init") +sc = StarCitizen("E:\\SC\\StarCitizen\\PTU", p4k_load_monitor=None) +sc.load_all() + +EcoSystem.read_eco_headers(sc) + +print("Megamap Load") +megamap_pu = sc.datacore.search_filename(f'libs/foundry/records/megamap/megamap.pu.xml')[0] +pu_socpak_path = megamap_pu.properties['SolarSystems'][0].properties['ObjectContainers'][0].value +pu_oc = sc.oc_manager.load_socpak(pu_socpak_path) + +print("Loading Body") + +bodies = PlanetView._search_for_bodies(pu_oc) +body = bodies[0] +body.load_data() + + +main_shader = PlanetView._get_shader("shader.hlsl") +hillshade_shader = PlanetView._get_shader("hillshade.hlsl") + +renderer: PlanetRenderer = PlanetRenderer((2048, 1024)) +renderer.set_planet(body) +render_lock = Lock() +render_settings = RenderSettings(True, 1, "NASA", main_shader, hillshade_shader, 1, (2048, 1024), True, False, 16) + +base_render = do_render(0, 0) + +result_stack: list[list[int | RenderResult]] = [] + +if __name__ == '__main__': + app.run(debug=False, port=8082) + + -- GitLab From 6e82f2f2957c47e53633da6195492f3db2088b18 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 16 May 2024 20:33:38 -0400 Subject: [PATCH 44/49] Add toggle option to disable ecosystem blending. --- starfab/gui/widgets/pages/page_PlanetView.py | 3 ++ starfab/planets/data.py | 7 ++++- starfab/planets/hlsl/hillshade.hlsl | 2 ++ starfab/planets/hlsl/shader.hlsl | 30 ++++++++++++-------- starfab/planets/planet_renderer.py | 1 + starfab/resources/ui/PlanetView.ui | 26 +++++++++++++---- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index d8add8e..cbf85db 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -52,6 +52,7 @@ class PlanetView(qtw.QWidget): self.enableGridCheckBox: QCheckBox = None self.enableCrosshairCheckBox: QCheckBox = None self.enableWaypointsCheckBox: QCheckBox = None + self.enableEcosystemBlendingCheckBox: QCheckBox = None self.enableHillshadeCheckBox: QCheckBox = None self.enableBinaryOceanMaskCheckBox: QCheckBox = None self.listWaypoints: QListView = None @@ -251,11 +252,13 @@ class PlanetView(qtw.QWidget): hm_bitdepth = self.heightmapBitDepthComboBox.currentData(role=Qt.UserRole) main_shader = self._get_shader("shader.hlsl") hillshade_shader = self._get_shader("hillshade.hlsl") + blending_enabled = self.enableEcosystemBlendingCheckBox.isChecked() hillshade_enabled = self.enableHillshadeCheckBox.isChecked() ocean_mask_binary = self.enableBinaryOceanMaskCheckBox.isChecked() return RenderSettings(True, scale, coordinates, main_shader, hillshade_shader, interpolation, resolution, + blending_enabled, hillshade_enabled, ocean_mask_binary, hm_bitdepth) diff --git a/starfab/planets/data.py b/starfab/planets/data.py index d27ba6c..6f3abda 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -27,7 +27,7 @@ class LUTData: class RenderJobSettings: # NOTE: Bools are GPU-register aligned, so need to be 4 bytes, not 1 # so we pack them as i instead of ? - PACK_STRING: str = "5f3i4f3i1f5i2f1i" + PACK_STRING: str = "5f3i4f3i1f6i2f1i" PACK_LENGTH: int = struct.calcsize(PACK_STRING) def __init__(self): @@ -53,6 +53,8 @@ class RenderJobSettings: self.ocean_depth: float = -2000 self.ocean_color: list[int] = [0, 0, 0, 255] + self.blending_enabled: bool = True + self.hillshade_enabled: bool = True self.hillshade_zenith: float = 45 self.hillshade_azimuth: float = 135 @@ -68,6 +70,7 @@ class RenderJobSettings: self.global_terrain_height_influence, self.ecosystem_terrain_height_influence, self.ocean_enabled, self.ocean_mask_binary, self.ocean_heightmap_flat, self.ocean_depth, *self.ocean_color, + self.blending_enabled, self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth, self.heightmap_bit_depth) @@ -134,6 +137,7 @@ class RenderSettings: def __init__(self, gpu: bool, resolution: int, coordinate_mode: str, shader_main: str, shader_hillshade: str, interpolation: int, output_resolution: Tuple[int, int], + blending_enabled: bool, hillshade_enabled: bool, ocean_mask_binary: bool, heightmap_bit_depth: int): self.gpu = gpu @@ -143,6 +147,7 @@ class RenderSettings: self.shader_hillshade = shader_hillshade self.interpolation = interpolation self.output_resolution = output_resolution + self.blending_enabled = blending_enabled self.hillshade_enabled = hillshade_enabled self.ocean_mask_binary = ocean_mask_binary self.heightmap_bit_depth = heightmap_bit_depth diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl index c58add8..0aba02c 100644 --- a/starfab/planets/hlsl/hillshade.hlsl +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -22,6 +22,8 @@ struct RenderJobSettings float ocean_depth; uint4 ocean_color; + bool blending_enabled; + bool hillshade_enabled; float hillshade_zenith; float hillshade_azimuth; diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 932bb95..7f2170c 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -22,6 +22,8 @@ struct RenderJobSettings float ocean_depth; uint4 ocean_color; + bool blending_enabled; + bool hillshade_enabled; float hillshade_zenith; float hillshade_azimuth; @@ -485,24 +487,28 @@ void main(uint3 tid : SV_DispatchThreadID) global_height = (global_height - 32767) / 32767.0f; global_climate = uint4(global_climate.xy / 2, 0, 0); - // Calculate influence of all neighboring terrain - ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position, projected_size, physical_size); - uint4 out_color = uint4(0, 0, 0, 255); float out_height = global_height * jobSettings.global_terrain_height_influence; // Value float out_ocean_mask = 0; - if (eco_influence.is_override) { - out_color.xyz = eco_influence.override; - } else { - if(eco_influence.mask_total > 0) { - eco_influence.temp_humidity /= eco_influence.mask_total; - global_climate.yx += eco_influence.temp_humidity * local_influence; - out_height += eco_influence.elevation * jobSettings.ecosystem_terrain_height_influence; - } + if (jobSettings.blending_enabled) { + // Calculate influence of all neighboring terrain + ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position, projected_size, physical_size); - uint4 surface_color = take_sample(surface, global_climate.yx, int2(128, 128), jobSettings.interpolation); + if (eco_influence.is_override) { + out_color.xyz = eco_influence.override; + } else { + if(eco_influence.mask_total > 0) { + eco_influence.temp_humidity /= eco_influence.mask_total; + global_climate.yx += eco_influence.temp_humidity * local_influence; + out_height += eco_influence.elevation * jobSettings.ecosystem_terrain_height_influence; + } + uint4 surface_color = take_sample(surface, global_climate.yx, int2(128, 128), jobSettings.interpolation); + out_color.xyz = surface_color.xyz; + } + } else { + uint4 surface_color = take_sample(surface, global_climate.yx, int2(128, 128), jobSettings.interpolation); out_color.xyz = surface_color.xyz; } diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 045b7af..90cb244 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -107,6 +107,7 @@ class PlanetRenderer: job_s.interpolation = self.settings.interpolation job_s.render_scale_x = self.settings.resolution * 2 job_s.render_scale_y = self.settings.resolution + job_s.blending_enabled = self.settings.blending_enabled job_s.hillshade_enabled = self.settings.hillshade_enabled job_s.ocean_mask_binary = self.settings.ocean_mask_binary job_s.heightmap_bit_depth = self.settings.heightmap_bit_depth diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 173fa43..828d411 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -204,34 +204,34 @@ - + Enable Hillshade - + - + Enable Binary Ocean Mask - + - + Waypoints - + @@ -244,6 +244,20 @@ + + + + Enable Ecosystem Blending + + + + + + + true + + + -- GitLab From 5b21b39d42ee33dbd3a2833e98a1dcbd3f289d1a Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sat, 18 May 2024 21:35:21 -0400 Subject: [PATCH 45/49] Remove the need for compressanatorcli for planet renders. --- starfab/planets/ecosystem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/starfab/planets/ecosystem.py b/starfab/planets/ecosystem.py index b1d3cbf..e835779 100644 --- a/starfab/planets/ecosystem.py +++ b/starfab/planets/ecosystem.py @@ -71,7 +71,13 @@ class EcoSystem: in p4k_dds_files if not record.orig_filename.endswith("a")} res: bytes = unsplit_dds(dds_files) - return image_converter.convert_buffer(res, "dds", "png") + + test = Image.open(io.BytesIO(res), formats=["DDS"]) + test2 = test.convert("RGBA") + out_buffer = io.BytesIO() + test2.save(out_buffer, format="PNG") + out_buffer.seek(0) + return out_buffer.read() # TODO: Use settings to define a cache directory to store these in def _read_with_cache(subpath: str) -> Image: -- GitLab From 3124b81f6f08c2af7c39b67d7a4e7ddf82578170 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Sun, 19 May 2024 20:04:52 -0400 Subject: [PATCH 46/49] Fix some value clamping in the shader. --- starfab/planets/hlsl/shader.hlsl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 7f2170c..36b2695 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -416,6 +416,12 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa } } + result.temp_humidity = min(float2(1, 1), max(float2(-1, -1), result.temp_humidity)); + result.elevation = min(1, max(-1, result.elevation)); + + result.temp_humidity /= result.mask_total; + result.elevation /= result.mask_total; + return result; } @@ -470,7 +476,7 @@ void main(uint3 tid : SV_DispatchThreadID) float terrain_scaling = 1; float2 projected_size = float2(6000, 6000) * terrain_scaling; float2 physical_size = float2(4000, 4000) * terrain_scaling; - float2 local_influence = float2(jobSettings.local_humidity_influence, jobSettings.local_temperature_influence); + float2 local_influence = float2(jobSettings.local_temperature_influence, jobSettings.local_humidity_influence); float max_deformation = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; // Calculate normalized position in the world (ie: 0,0 = top-left, 1,1 = bottom-right) @@ -499,7 +505,6 @@ void main(uint3 tid : SV_DispatchThreadID) out_color.xyz = eco_influence.override; } else { if(eco_influence.mask_total > 0) { - eco_influence.temp_humidity /= eco_influence.mask_total; global_climate.yx += eco_influence.temp_humidity * local_influence; out_height += eco_influence.elevation * jobSettings.ecosystem_terrain_height_influence; } -- GitLab From 84c692337eb43bfb5d626fe231e3b2d4d121f8ff Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Mon, 20 May 2024 22:36:26 -0400 Subject: [PATCH 47/49] Added debug options to help debug, and better explain/understand the rendering process. --- starfab/gui/widgets/pages/page_PlanetView.py | 20 ++++++- starfab/planets/data.py | 9 ++- starfab/planets/hlsl/hillshade.hlsl | 2 + starfab/planets/hlsl/shader.hlsl | 62 ++++++++++++++++---- starfab/planets/planet_renderer.py | 1 + starfab/resources/ui/PlanetView.ui | 21 +++++++ 6 files changed, 98 insertions(+), 17 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index cbf85db..9842b94 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -55,6 +55,8 @@ class PlanetView(qtw.QWidget): self.enableEcosystemBlendingCheckBox: QCheckBox = None self.enableHillshadeCheckBox: QCheckBox = None self.enableBinaryOceanMaskCheckBox: QCheckBox = None + self.debugGridCheckBox: QCheckBox = None + self.debugModeComboBox: QComboBox = None self.listWaypoints: QListView = None self.lbl_planetDetails: QLabel = None self.lbl_currentStatus: QLabel = None @@ -122,6 +124,17 @@ class PlanetView(qtw.QWidget): ])) self.displayLayerComboBox.currentIndexChanged.connect(self._display_layer_changed) + self.debugModeComboBox.setModel(self.create_model([ + ("None", 0), + ("Mask", 1 << 1), + ("Terrain UV (no mask)", 1 << 2), + ("Patch UV (no mask)", 1 << 3), + ("Terrain Ecosystem (no mask)", 1 << 4), + ("Terrain UV", 1 << 5), + ("Patch UV", 1 << 6), + ("Terrain Ecosystem", 1 << 7) + ])) + if isinstance(sc, StarCitizen): self.sc = sc self._handle_datacore_loaded() @@ -255,12 +268,17 @@ class PlanetView(qtw.QWidget): blending_enabled = self.enableEcosystemBlendingCheckBox.isChecked() hillshade_enabled = self.enableHillshadeCheckBox.isChecked() ocean_mask_binary = self.enableBinaryOceanMaskCheckBox.isChecked() + + debug_mode = 0 + debug_mode += 1 if self.debugGridCheckBox.isChecked() else 0 + debug_mode += self.debugModeComboBox.currentData(role=Qt.UserRole) + return RenderSettings(True, scale, coordinates, main_shader, hillshade_shader, interpolation, resolution, blending_enabled, hillshade_enabled, ocean_mask_binary, - hm_bitdepth) + hm_bitdepth, debug_mode) def _do_render(self): selected_obj = self._get_selected_planet() diff --git a/starfab/planets/data.py b/starfab/planets/data.py index 6f3abda..b00acf6 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -27,7 +27,7 @@ class LUTData: class RenderJobSettings: # NOTE: Bools are GPU-register aligned, so need to be 4 bytes, not 1 # so we pack them as i instead of ? - PACK_STRING: str = "5f3i4f3i1f6i2f1i" + PACK_STRING: str = "5f3i4f3i1f6i2f2i" PACK_LENGTH: int = struct.calcsize(PACK_STRING) def __init__(self): @@ -61,6 +61,8 @@ class RenderJobSettings: self.heightmap_bit_depth: int = 16 + self.debug_mode: int = 0 + def pack(self) -> bytes: return struct.pack(RenderJobSettings.PACK_STRING, self.offset_x, self.offset_y, self.size_x, self.size_y, @@ -72,7 +74,7 @@ class RenderJobSettings: self.ocean_depth, *self.ocean_color, self.blending_enabled, self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth, - self.heightmap_bit_depth) + self.heightmap_bit_depth, self.debug_mode) def update_buffer(self, buffer_gpu: Buffer): data = self.pack() @@ -139,7 +141,7 @@ class RenderSettings: interpolation: int, output_resolution: Tuple[int, int], blending_enabled: bool, hillshade_enabled: bool, ocean_mask_binary: bool, - heightmap_bit_depth: int): + heightmap_bit_depth: int, debug_mode: int): self.gpu = gpu self.resolution = resolution self.coordinate_mode = coordinate_mode @@ -151,3 +153,4 @@ class RenderSettings: self.hillshade_enabled = hillshade_enabled self.ocean_mask_binary = ocean_mask_binary self.heightmap_bit_depth = heightmap_bit_depth + self.debug_mode = debug_mode diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl index 0aba02c..45ac622 100644 --- a/starfab/planets/hlsl/hillshade.hlsl +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -29,6 +29,8 @@ struct RenderJobSettings float hillshade_azimuth; int heightmap_bit_depth; + + int debug_mode; }; Texture2D input_color : register(t0); diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 36b2695..743a1c0 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -3,6 +3,21 @@ #define MODE_BI_LINEAR 1 #define MODE_BI_CUBIC 2 +#define DEBUG_GRID 1 << 0 + +#define DEBUG_MASK 1 << 1 +#define DEBUG_UV_TERRAIN_NO_MASK 1 << 2 +#define DEBUG_UV_PATCH_NO_MASK 1 << 3 +#define DEBUG_ECOSYSTEM_NO_MASK 1 << 4 +#define DEBUG_UV_TERRAIN 1 << 5 +#define DEBUG_UV_PATCH 1 << 6 +#define DEBUG_ECOSYSTEM 1 << 7 + +#define DEBUG_ANY (DEBUG_MASK | \ + DEBUG_UV_TERRAIN_NO_MASK | DEBUG_UV_TERRAIN | \ + DEBUG_UV_PATCH_NO_MASK | DEBUG_UV_PATCH | \ + DEBUG_ECOSYSTEM_NO_MASK | DEBUG_ECOSYSTEM) + struct RenderJobSettings { float2 offset; @@ -29,6 +44,8 @@ struct RenderJobSettings float hillshade_azimuth; int heightmap_bit_depth; + + int debug_mode; }; struct LocalizedWarping @@ -398,15 +415,32 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa int4 local_eco_data = take_sample_nn_3d(ecosystem_climates, terrain_uv * eco_sz.xy, eco_sz.xy, ecosystem_id); float4 local_eco_normalized = (local_eco_data - 127) / 127.0f; - // TODO: Heightmaps + float local_eco_height = take_sample_nn_3d(ecosystem_heightmaps, terrain_uv * eco_sz.xy, eco_sz.xy, ecosystem_id); local_eco_height = (local_eco_height - 32767) / 32767.0f; - if (false && (round(search_x_px % 20) == 0 && round(search_y_px % 20) == 0)) { - result.is_override = true; - //result.override = uint3(255 * terrain_uv.x, 255 * terrain_uv.y, 0) * local_mask_value; - result.override = uint3(offset * 256, 0, 0); - return result; + if (jobSettings.debug_mode & DEBUG_ANY) + { + if ((round(search_x_px % 10) == 0 && round(search_y_px % 10) == 0)) + { + result.is_override = true; + if (jobSettings.debug_mode & DEBUG_MASK) { + result.override = uint3(local_mask_value * 256, local_mask_value * 256, local_mask_value * 256); + } else if (jobSettings.debug_mode & DEBUG_UV_TERRAIN_NO_MASK) { + result.override = uint3(255 * terrain_uv.x, 255 * terrain_uv.y, 0); + } else if (jobSettings.debug_mode & DEBUG_UV_PATCH_NO_MASK) { + result.override = uint3(255 * patch_uv.x, 255 * patch_uv.y, 0); + } else if (jobSettings.debug_mode & DEBUG_ECOSYSTEM_NO_MASK) { + result.override = uint3(local_eco_data.x, local_eco_data.y, 0); + } else if (jobSettings.debug_mode & DEBUG_UV_TERRAIN) { + result.override = uint3(255 * terrain_uv.x, 255 * terrain_uv.y, 0) * local_mask_value; + } else if (jobSettings.debug_mode & DEBUG_UV_PATCH) { + result.override = uint3(255 * patch_uv.x, 255 * patch_uv.y, 0) * local_mask_value; + } else if (jobSettings.debug_mode & DEBUG_ECOSYSTEM) { + result.override = uint3(local_eco_data.x, local_eco_data.y, 0) * local_mask_value; + } + return result; + } } result.temp_humidity += local_eco_normalized.xy * local_mask_value; @@ -539,18 +573,20 @@ void main(uint3 tid : SV_DispatchThreadID) out_ocean_mask = 0; } - // Squash out_height from meter range to normalized +/- 1.0 range - out_height /= max_deformation; - // DEBUG: Grid rendering - int2 cell_position = int2(normalized_position * out_sz * jobSettings.render_scale) % jobSettings.render_scale; - if(false && (cell_position.x == 0 || cell_position.y == 0)) + if(jobSettings.debug_mode & DEBUG_GRID) { - out_color.xyz = uint3(255, 0, 0); + int2 cell_position = (normalized_position.xy * clim_sz * jobSettings.render_scale) % jobSettings.render_scale; + if (cell_position.x == 0 || cell_position.y == 0) + { + out_color.xyz = uint3(255, 0, 0); + } } - output_color[tid.xy] = out_color; + // Squash out_height from meter range to normalized +/- 1.0 range + out_height /= max_deformation; + output_color[tid.xy] = out_color; output_heightmap[tid.xy] = PackFloatToUInt4(out_height, jobSettings.heightmap_bit_depth); output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 255, 0), 255); } \ No newline at end of file diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 90cb244..60a6cf7 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -111,6 +111,7 @@ class PlanetRenderer: job_s.hillshade_enabled = self.settings.hillshade_enabled job_s.ocean_mask_binary = self.settings.ocean_mask_binary job_s.heightmap_bit_depth = self.settings.heightmap_bit_depth + job_s.debug_mode = self.settings.debug_mode job_s.planet_radius = self.planet.radius_m job_s.local_humidity_influence = self.planet.humidity_influence diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index 828d411..d986319 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -275,6 +275,27 @@ QFrame::Raised + + + + + + Debug Settings + + + + + + + Grid + + + + + + + + -- GitLab From d36505460690d8e54c80ee5a7d2a6d6dd8518a01 Mon Sep 17 00:00:00 2001 From: Ben Abraham Date: Thu, 6 Jun 2024 02:59:37 -0400 Subject: [PATCH 48/49] Major improvements to tile blending at higher resolutions without impacting lower. Now projection isn't considered for bounds checking, and instead cells are overlapped by a consistent amount, while still offsetting for random variation. --- starfab/planets/hlsl/shader.hlsl | 54 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 743a1c0..1b1641f 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -18,6 +18,9 @@ DEBUG_UV_PATCH_NO_MASK | DEBUG_UV_PATCH | \ DEBUG_ECOSYSTEM_NO_MASK | DEBUG_ECOSYSTEM) +#define HORIZONTAL_BLEED 0.50f +#define VERTICAL_BLEED 0.50f + struct RenderJobSettings { float2 offset; @@ -333,9 +336,9 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa //TODO Vary terrain step from 1 at pole_distance to TileCount at the pole if (position_px.y < pole_distance / 2 || position_px.y >= clim_sz.y - pole_distance / 2) { - terrain_step = 8; + //terrain_step = 3; }else if(position_px.y < pole_distance || position_px.y >= clim_sz.y - pole_distance) { - terrain_step = 4; + //terrain_step = 2; } //Search vertically all cells that our projection overlaps with @@ -371,7 +374,7 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa // TODO: Use global random offset data float offset = take_sample_nn(planet_offsets, search_pos_normal * off_sz, off_sz) / 256.0f; - float2 terrain_center = float2(search_x_px, search_y_px) + offset; + float2 terrain_center = float2(search_x_px, search_y_px) + float2(0.5f, 0.5f);// + offset; //Now finally calculate the local distortion at the center of the terrain float2 terrain_center_m = pixels_to_meters(terrain_center, clim_sz.y); @@ -379,39 +382,40 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa float half_terrain_width_projected_px = (projected_size.x / 2 / terrain_circumference) * clim_sz.y; float half_terrain_width_physical_px = (terrain_size.x / 2 / terrain_circumference) * clim_sz.y; + // Both of these coordinates represent 0-1 on the scale of a single pixel in the climate output_heightmap + // So we can simply subtract them to get a UV-like coordinate for our position in this grid cell + float2 grid_uv = (position_px - search_pos) + offset; + float terrain_left_edge = terrain_center.x - half_terrain_width_projected_px; float terrain_right_edge = terrain_center.x + half_terrain_width_projected_px; float terrain_top_edge = terrain_center.y - (projection_warping.vertical_delta * clim_sz.y); float terrain_bottom_edge = terrain_center.y + (projection_warping.vertical_delta * clim_sz.y); - //Reject pixels outside of the terrains projected pixel borders - if (position_px.x < terrain_left_edge || position_px.x > terrain_right_edge) - continue; - if (position_px.y < terrain_top_edge || position_px.y > terrain_bottom_edge) + //Reject pixels outside of the grid square + if( grid_uv.x < -HORIZONTAL_BLEED || grid_uv.x >= 1 + HORIZONTAL_BLEED || + grid_uv.y < -VERTICAL_BLEED || grid_uv.y >= 1 + VERTICAL_BLEED) + { continue; + } //Finally calculate UV coordinates and return result - float terrain_u = ((position_px.x - terrain_center.x) / half_terrain_width_physical_px / 2) + 0.5f; - float terrain_v = ((position_px.y - terrain_center.y) / (physical_warping.vertical_delta * clim_sz.y * 2)) + 0.5f; + float terrain_u = ((position_px.x - terrain_center.x) / half_terrain_width_physical_px / 2); + float terrain_v = ((position_px.y - terrain_center.y) / (physical_warping.vertical_delta * clim_sz.y * 2)); float patch_u = ((position_px.x - terrain_left_edge) / (half_terrain_width_projected_px * 2)); float patch_v = ((position_px.y - terrain_top_edge) / (projection_warping.vertical_delta * clim_sz.y * 2)); - if (terrain_u < 0) terrain_u += 1; - if (terrain_v < 0) terrain_v += 1; - if (terrain_u >= 1) terrain_u -= 1; - if (terrain_v >= 1) terrain_v -= 1; + float2 terrain_uv = float2(terrain_u, terrain_v) + offset; + float2 patch_uv = float2(patch_u, patch_v); - if (patch_u < 0) patch_u += 1; - if (patch_v < 0) patch_v += 1; - if (patch_u >= 1) patch_u -= 1; - if (patch_v >= 1) patch_v -= 1; + terrain_uv = (terrain_uv + 10) % 1; + patch_uv = (patch_uv + 10) % 1; - float2 terrain_uv = float2(terrain_u, terrain_v); - float2 patch_uv = float2(patch_u, patch_v); + float2 delta = grid_uv - float2(0.5f, 0.5f); + float center_distance = sqrt(delta.x * delta.x + delta.y * delta.y); + float local_mask_value = 1 - (center_distance / float2(1 + HORIZONTAL_BLEED, 1 + VERTICAL_BLEED)); + //(float)(center_distance > 0.5f ? 0 : cos(center_distance * PI)); - float2 delta = patch_uv - float2(0.5f, 0.5f); - float center_distance = sqrt(delta.x * delta.x + delta.y * delta.y) * 1; - float local_mask_value = (float)(center_distance > 0.5f ? 0 : cos(center_distance * PI)); + local_mask_value = pow(clamp(local_mask_value, 0, 1), 3); int4 local_eco_data = take_sample_nn_3d(ecosystem_climates, terrain_uv * eco_sz.xy, eco_sz.xy, ecosystem_id); float4 local_eco_normalized = (local_eco_data - 127) / 127.0f; @@ -527,6 +531,7 @@ void main(uint3 tid : SV_DispatchThreadID) global_height = (global_height - 32767) / 32767.0f; global_climate = uint4(global_climate.xy / 2, 0, 0); + bool out_override = false; uint4 out_color = uint4(0, 0, 0, 255); float out_height = global_height * jobSettings.global_terrain_height_influence; // Value float out_ocean_mask = 0; @@ -536,6 +541,7 @@ void main(uint3 tid : SV_DispatchThreadID) ProjectedTerrainInfluence eco_influence = calculate_projected_tiles(normalized_position, projected_size, physical_size); if (eco_influence.is_override) { + out_override = true; out_color.xyz = eco_influence.override; } else { if(eco_influence.mask_total > 0) { @@ -553,7 +559,9 @@ void main(uint3 tid : SV_DispatchThreadID) if (jobSettings.ocean_enabled && out_height < jobSettings.ocean_depth) { - out_color.xyz = jobSettings.ocean_color.xyz; + if(!out_override) { + out_color.xyz = jobSettings.ocean_color.xyz; + } if (jobSettings.ocean_mask_binary) { out_ocean_mask = 1.0; -- GitLab From ff6047dc3b46b7d9741ae6ce3a7be519b48c196b Mon Sep 17 00:00:00 2001 From: gulbrillo Date: Sat, 3 Aug 2024 18:20:18 -0400 Subject: [PATCH 49/49] Hightmap and Hillshader improvements Heightmap now renders as single channel grayscale PNG/TIFF (8, 16, 32 bit) Fixed pixel clipping in Hillshader, added ray marching and long shadows --- starfab/gui/widgets/pages/page_PlanetView.py | 38 +++- starfab/gui/widgets/planets/planet_viewer.py | 4 + starfab/planets/__init__.py | 2 +- starfab/planets/data.py | 9 +- starfab/planets/hlsl/hillshade.hlsl | 212 ++++++++++++++----- starfab/planets/hlsl/shader.hlsl | 127 ++++++----- starfab/planets/planet_renderer.py | 48 ++++- starfab/resources/ui/PlanetView.ui | 84 ++++---- 8 files changed, 354 insertions(+), 170 deletions(-) diff --git a/starfab/gui/widgets/pages/page_PlanetView.py b/starfab/gui/widgets/pages/page_PlanetView.py index 9842b94..42fee4f 100644 --- a/starfab/gui/widgets/pages/page_PlanetView.py +++ b/starfab/gui/widgets/pages/page_PlanetView.py @@ -54,6 +54,7 @@ class PlanetView(qtw.QWidget): self.enableWaypointsCheckBox: QCheckBox = None self.enableEcosystemBlendingCheckBox: QCheckBox = None self.enableHillshadeCheckBox: QCheckBox = None + self.hillshadeLevelComboBox: QComboBox = None self.enableBinaryOceanMaskCheckBox: QCheckBox = None self.debugGridCheckBox: QCheckBox = None self.debugModeComboBox: QComboBox = None @@ -92,7 +93,7 @@ class PlanetView(qtw.QWidget): self.sampleModeComboBox.setModel(self.create_model([ ("Nearest Neighbor", 0), ("Bi-Linear", 1), - ("Bi-Cubic", 2), + ("Bi-Cubic", 2) ])) self.sampleModeComboBox.setCurrentIndex(1) @@ -105,10 +106,9 @@ class PlanetView(qtw.QWidget): self.outputResolutionComboBox.currentIndexChanged.connect(self._display_resolution_changed) self.heightmapBitDepthComboBox.setModel(self.create_model([ - ("8-Bit Greyscale", 8), - ("16-Bit (R,G)", 16), - ("24-Bit (R,G,B)", 24), - ("32-Bit (R,G,B,A)", 32) + ("8-Bit Greyscale (PNG)", 8), + ("16-Bit Greyscale (PNG)", 16), + ("32-Bit Greyscale (TIFF)", 32) ])) self.displayModeComboBox.setModel(self.create_model([ @@ -124,6 +124,12 @@ class PlanetView(qtw.QWidget): ])) self.displayLayerComboBox.currentIndexChanged.connect(self._display_layer_changed) + self.hillshadeLevelComboBox.setModel(self.create_model([ + ("Weak", 0.5), + ("Normal", 1), + ("Strong", 2) + ])) + self.debugModeComboBox.setModel(self.create_model([ ("None", 0), ("Mask", 1 << 1), @@ -267,6 +273,7 @@ class PlanetView(qtw.QWidget): hillshade_shader = self._get_shader("hillshade.hlsl") blending_enabled = self.enableEcosystemBlendingCheckBox.isChecked() hillshade_enabled = self.enableHillshadeCheckBox.isChecked() + hillshade_level = self.hillshadeLevelComboBox.currentData(role=Qt.UserRole) ocean_mask_binary = self.enableBinaryOceanMaskCheckBox.isChecked() debug_mode = 0 @@ -277,7 +284,7 @@ class PlanetView(qtw.QWidget): main_shader, hillshade_shader, interpolation, resolution, blending_enabled, - hillshade_enabled, ocean_mask_binary, + hillshade_enabled, hillshade_level, ocean_mask_binary, hm_bitdepth, debug_mode) def _do_render(self): @@ -302,17 +309,28 @@ class PlanetView(qtw.QWidget): def _do_export(self): prev_dir = settings.value("exportDirectory") + layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) + title = "Save Render to..." + format = "png" + formatfilter = "PNG Image (*.png)" + if self.renderer.settings.heightmap_bit_depth == 32 and layer == 'heightmap': + format = "tiff" + formatfilter = "TIFF Image (*.tiff)" + edir = qtw.QFileDialog.getSaveFileName(self, title, - dir=f"{self.renderer.planet.oc.entity_name}.png", - filter="PNG Image (*.png)") + dir=f"{self.renderer.planet.oc.entity_name}.{format}", + filter=formatfilter) + filename, filter = edir if filename: - layer = self.displayLayerComboBox.currentData(role=Qt.UserRole) if layer == 'surface': self.last_render.tex_color.save(filename, format="png") elif layer == 'heightmap': - self.last_render.tex_heightmap.save(filename, format="png") + if self.renderer.settings.heightmap_bit_depth == 32: + self.last_render.tex_heightmap.save(filename, format="tiff") + else: + self.last_render.tex_heightmap.save(filename, format="png") elif layer == 'ocean_mask': self.last_render.tex_oceanmask.save(filename, format="png") else: diff --git a/starfab/gui/widgets/planets/planet_viewer.py b/starfab/gui/widgets/planets/planet_viewer.py index 3196775..4936e23 100644 --- a/starfab/gui/widgets/planets/planet_viewer.py +++ b/starfab/gui/widgets/planets/planet_viewer.py @@ -214,6 +214,10 @@ class QPlanetViewer(qtw.QGraphicsView): else: raise Exception(f"Unknown layer: {layer}") + # Convert the image to a mode supported by QPixmap + if img.mode not in ["RGB", "RGBA"]: + img = img.convert("RGBA") + qt_image: QPixmap = QPixmap.fromImage(ImageQt(img)) width_scale = self._render_result.coordinate_bounds.width() / qt_image.width() self.image.setPixmap(qt_image) diff --git a/starfab/planets/__init__.py b/starfab/planets/__init__.py index b1fd15a..23a015e 100644 --- a/starfab/planets/__init__.py +++ b/starfab/planets/__init__.py @@ -1,7 +1,7 @@ try: from compushady import Texture2D, Compute, Resource, HEAP_UPLOAD, Buffer, Texture3D, HEAP_READBACK - from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT + from compushady.formats import R8G8B8A8_UINT, R8_UINT, R16_UINT, R32_SINT from compushady.shaders import hlsl HAS_COMPUSHADY = True except ImportError as e: diff --git a/starfab/planets/data.py b/starfab/planets/data.py index b00acf6..1481a54 100644 --- a/starfab/planets/data.py +++ b/starfab/planets/data.py @@ -27,7 +27,7 @@ class LUTData: class RenderJobSettings: # NOTE: Bools are GPU-register aligned, so need to be 4 bytes, not 1 # so we pack them as i instead of ? - PACK_STRING: str = "5f3i4f3i1f6i2f2i" + PACK_STRING: str = "5f3i4f3i1f6i3f2i" PACK_LENGTH: int = struct.calcsize(PACK_STRING) def __init__(self): @@ -56,6 +56,8 @@ class RenderJobSettings: self.blending_enabled: bool = True self.hillshade_enabled: bool = True + self.hillshade_level : float = 1 + self.hillshade_zenith: float = 45 self.hillshade_azimuth: float = 135 @@ -73,7 +75,7 @@ class RenderJobSettings: self.ocean_enabled, self.ocean_mask_binary, self.ocean_heightmap_flat, self.ocean_depth, *self.ocean_color, self.blending_enabled, - self.hillshade_enabled, self.hillshade_zenith, self.hillshade_azimuth, + self.hillshade_enabled, self.hillshade_level, self.hillshade_zenith, self.hillshade_azimuth, self.heightmap_bit_depth, self.debug_mode) def update_buffer(self, buffer_gpu: Buffer): @@ -140,7 +142,7 @@ class RenderSettings: shader_main: str, shader_hillshade: str, interpolation: int, output_resolution: Tuple[int, int], blending_enabled: bool, - hillshade_enabled: bool, ocean_mask_binary: bool, + hillshade_enabled: bool, hillshade_level: float, ocean_mask_binary: bool, heightmap_bit_depth: int, debug_mode: int): self.gpu = gpu self.resolution = resolution @@ -151,6 +153,7 @@ class RenderSettings: self.output_resolution = output_resolution self.blending_enabled = blending_enabled self.hillshade_enabled = hillshade_enabled + self.hillshade_level = hillshade_level self.ocean_mask_binary = ocean_mask_binary self.heightmap_bit_depth = heightmap_bit_depth self.debug_mode = debug_mode diff --git a/starfab/planets/hlsl/hillshade.hlsl b/starfab/planets/hlsl/hillshade.hlsl index 45ac622..cfc19d1 100644 --- a/starfab/planets/hlsl/hillshade.hlsl +++ b/starfab/planets/hlsl/hillshade.hlsl @@ -25,6 +25,7 @@ struct RenderJobSettings bool blending_enabled; bool hillshade_enabled; + float hillshade_level; float hillshade_zenith; float hillshade_azimuth; @@ -34,7 +35,7 @@ struct RenderJobSettings }; Texture2D input_color : register(t0); -Texture2D input_heightmap: register(t1); +Texture2D input_heightmap: register(t1); Texture2D input_ocean_mask: register(t2); ConstantBuffer jobSettings : register(b0); @@ -75,6 +76,14 @@ uint interpolate_cubic_uint(float v0, float v1, float v2, float v3, float fracti return (fraction * ((fraction * ((fraction * p) + q)) + r)) + v1; } +/* Texture2D implementations */ + + +int take_sample_nn_int(Texture2D texture, float2 position, int2 dimensions) +{ + return texture[position % dimensions]; +} + /* Texture2D implementations */ uint4 take_sample_nn(Texture2D texture, float2 position, int2 dimensions) @@ -122,102 +131,189 @@ uint4 take_sample(Texture2D texture, float2 position, int2 dimensions, in } } -float UnpackUInt4ToFloat(uint4 packedValue, int bit_depth) + +float UnpackIntToFloat(int packedValue, int bit_depth) { // Ensure the bit depth is valid - if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) + if (bit_depth != 8 && bit_depth != 16 && bit_depth != 32) return 0.0; - // Combine the components of packedValue to reconstruct the float value - float factor = (1L << bit_depth) - 1.0f; - float reconstructedValue = 0.0; + float normalizedValue = 0.0; - if (bit_depth == 8) { // Greyscale - reconstructedValue = packedValue.x / factor; - } else { - // Combine components based on bit depth - reconstructedValue += packedValue.x / factor; - reconstructedValue += packedValue.y / factor * 256.0; - reconstructedValue += packedValue.z / factor * 65536.0; - - if (bit_depth == 32) { - reconstructedValue += packedValue.w / factor * 16777216.0; - } + if (bit_depth == 8) + { + // Convert unsigned 8-bit to signed range [-1, 1] + normalizedValue = (packedValue / 255.0f) * 2.0f - 1.0f; + } + else if (bit_depth == 16) + { + // Convert unsigned 16-bit to signed range [-1, 1] + normalizedValue = (packedValue / 65535.0f) * 2.0f - 1.0f; + } + else if (bit_depth == 32) + { + // Convert signed 32-bit to signed range [-1, 1] + normalizedValue = packedValue / 2147483647.0f; // 2^31 - 1 (maximum positive value for a 32-bit signed integer) } - // Map the range [0.0, 1.0] back to [-1.0, 1.0] - reconstructedValue = reconstructedValue * 2.0f - 1.0f; - - return reconstructedValue; + return normalizedValue; } + + float read_height(uint2 coordinate, int2 relative, int2 dimensions) { - uint4 samp = take_sample_nn(input_heightmap, coordinate + relative, dimensions); float max_deform = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; - return UnpackUInt4ToFloat(samp, jobSettings.heightmap_bit_depth) * max_deform; + + int packedValue = take_sample_nn_int(input_heightmap, coordinate + relative, dimensions); //input_heightmap is uint for 8 and 16 bit! + float height = UnpackIntToFloat(packedValue, jobSettings.heightmap_bit_depth); //UnpackIntToFloat should deal with 8 and 16 bit cases (hopefully) + + return height * max_deform; } -[numthreads(8,8,1)] + +// Function to read and smooth the height map +float read_and_smooth_height(uint2 tid, int2 offset, uint2 hm_sz, Texture2D input_heightmap) +{ + + int smoothing = 1; + if (jobSettings.render_scale[1] == 2) { + smoothing = 2; + } else if (jobSettings.render_scale[1] == 4) { + smoothing = 3; + } else if (jobSettings.render_scale[1] == 8) { + smoothing = 4; + } else if (jobSettings.render_scale[1] == 16) { + smoothing = 5; + } + + float sum = 0.0; + int count = 0; + for (int y = -1 * smoothing; y <= smoothing; y++) + { + for (int x = -1 * smoothing; x <= smoothing; x++) + { + int2 sample_pos = tid + offset + int2(x, y); + if (sample_pos.x >= 0 && sample_pos.x < hm_sz.x && sample_pos.y >= 0 && sample_pos.y < hm_sz.y) + { + float sample_height = input_heightmap.Load(int3(sample_pos, 0)); + sum += sample_height; + count++; + } + } + } + return sum / count; +} + +[numthreads(8, 8, 1)] void main(uint3 tid : SV_DispatchThreadID) { - uint2 hm_sz; input_heightmap.GetDimensions(hm_sz.x, hm_sz.y); + uint2 hm_sz; + input_heightmap.GetDimensions(hm_sz.x, hm_sz.y); - if(input_ocean_mask[tid.xy] != 0) + if (input_ocean_mask[tid.xy] != 0) return; - float ZFactor = 1.0f; + float pi = 3.14159265359f; + float ZFactor = 100.0f; - float a = read_height(tid.xy, int2(1, 1), hm_sz); - float b = read_height(tid.xy, int2(0, -1), hm_sz); - float c = read_height(tid.xy, int2(1, -1), hm_sz); - float d = read_height(tid.xy, int2(-1, 0), hm_sz); - float f = read_height(tid.xy, int2(1, 0), hm_sz); - float g = read_height(tid.xy, int2(-1, 1), hm_sz); - float h = read_height(tid.xy, int2(0, 1), hm_sz); - float i = read_height(tid.xy, int2(1, 1), hm_sz); + // Calculating the cell size considering equirectangular projection + float planet_circ_m = pi * 2 * jobSettings.planet_radius; + float lat = (tid.y / (float)hm_sz.y) * pi - pi / 2; // Convert y to latitude in radians + float cosLat = cos(lat); + float cellsize_x = planet_circ_m * cosLat / (hm_sz.x * jobSettings.render_scale); + float cellsize_y = planet_circ_m / (hm_sz.y * jobSettings.render_scale); - //Cellsize needs to be the same scale as the X/Y distance - //This calculation does not take into account projection warping at all - float planet_circ_m = PI * 2 * jobSettings.planet_radius; - float cellsize_m = planet_circ_m / (hm_sz.x * jobSettings.render_scale * 2); - - float dzdx = ((c + 2 * f + i) - (a + 2 * d + g)) / (8 * cellsize_m); - float dzdy = ((g + 2 * h + i) - (a + 2 * b + c)) / (8 * cellsize_m); - float aspect = 0.0; + // Slope calculations with larger feature capture + float heights[9]; + int2 offsets[9] = { int2(-2, -2), int2(0, -2), int2(2, -2), int2(-2, 0), int2(2, 0), int2(-2, 2), int2(0, 2), int2(2, 2), int2(0, 0) }; + for (int i = 0; i < 9; i++) + { + heights[i] = read_and_smooth_height(tid.xy, offsets[i], hm_sz, input_heightmap); + } + float dzdx = ((heights[2] + 2 * heights[4] + heights[5]) - (heights[0] + 2 * heights[3] + heights[6])) / (8 * cellsize_x); + float dzdy = ((heights[6] + 2 * heights[7] + heights[5]) - (heights[0] + 2 * heights[1] + heights[2])) / (8 * cellsize_y); float slope = atan(ZFactor * sqrt(dzdx * dzdx + dzdy * dzdy)); + float aspect = 0.0; if (dzdx != 0) { aspect = atan2(dzdy, -dzdx); if (aspect < 0) - aspect += PI * 2; + aspect += pi * 2; } else { if (dzdy > 0) - aspect = PI / 2; + aspect = pi / 2; else if (dzdy < 0) - aspect = (PI * 2) - (PI / 2); + aspect = 3 * pi / 2; } - //Normalize slope to +/- 1 - slope = min(PI, max(-PI, slope)) / PI; + // Normalize slope to [0, 1] + slope = min(pi, max(0, slope)) / pi; + + // Calculate hillshade + float cos_zenith = cos(jobSettings.hillshade_zenith); + float sin_zenith = sin(jobSettings.hillshade_zenith); + float cos_slope = cos(slope); + float sin_slope = sin(slope); + float cos_azimuth_aspect = cos(jobSettings.hillshade_azimuth - aspect); - int hillshade_amount = 255 * ( - (cos(jobSettings.hillshade_zenith) * cos(slope)) + - (sin(jobSettings.hillshade_zenith) * sin(slope) * cos(jobSettings.hillshade_azimuth - aspect))); - //Tone down hillshade, and make centered around 0 + int hillshade_amount = (int)(255 * (cos_zenith * cos_slope + sin_zenith * sin_slope * cos_azimuth_aspect)); hillshade_amount = (hillshade_amount - 127) / 4; - uint3 final_color = output_color[tid.xy].xyz; + // Ray marching for longer shadows + float dx = cos(jobSettings.hillshade_azimuth) * cos(jobSettings.hillshade_zenith); + float dy = sin(jobSettings.hillshade_azimuth) * cos(jobSettings.hillshade_zenith); + float dz = sin(jobSettings.hillshade_zenith); - final_color.x = max(0, min(255, final_color.x + hillshade_amount)); - final_color.y = max(0, min(255, final_color.y + hillshade_amount)); - final_color.z = max(0, min(255, final_color.z + hillshade_amount)); + float max_distance = 1000.0; // Maximum distance to march the ray + float step_size = 1.0; // Step size for ray marching + float shadow_intensity = 0.0; // Shadow intensity + float current_distance = 0.0; + + float3 position = float3(tid.xy, read_and_smooth_height(tid.xy, int2(0, 0), hm_sz, input_heightmap) * ZFactor); + + while (current_distance < max_distance) + { + position += float3(dx * step_size, dy * step_size, dz * step_size); + current_distance += step_size; + + int2 sample_pos = int2(position.xy); + if (sample_pos.x < 0 || sample_pos.x >= hm_sz.x || sample_pos.y < 0 || sample_pos.y >= hm_sz.y) + break; + + float sample_height = read_and_smooth_height(sample_pos, int2(0, 0), hm_sz, input_heightmap) * ZFactor; + + if (position.z < sample_height) + { + shadow_intensity = 1.0; + break; + } + } + + int ray_march_shadow_amount = (int)(255 * (1.0 - shadow_intensity)); + ray_march_shadow_amount = (ray_march_shadow_amount - 127) / 16; + + // Combine hillshade and ray marching shadows + int combined_shadow_amount = (hillshade_amount + ray_march_shadow_amount) * jobSettings.hillshade_level; + + // Apply combined shadows to color + int3 final_color = output_color[tid.xy].xyz; + + final_color.x = max(0, min(255, final_color.x + combined_shadow_amount)); + final_color.y = max(0, min(255, final_color.y + combined_shadow_amount)); + final_color.z = max(0, min(255, final_color.z + combined_shadow_amount)); + + uint3 final_color_uint; + final_color_uint.x = (uint)final_color.x; + final_color_uint.y = (uint)final_color.y; + final_color_uint.z = (uint)final_color.z; + + output_color[tid.xy].xyz = final_color_uint; +} - output_color[tid.xy].xyz = final_color; -} \ No newline at end of file diff --git a/starfab/planets/hlsl/shader.hlsl b/starfab/planets/hlsl/shader.hlsl index 1b1641f..718e346 100644 --- a/starfab/planets/hlsl/shader.hlsl +++ b/starfab/planets/hlsl/shader.hlsl @@ -43,6 +43,7 @@ struct RenderJobSettings bool blending_enabled; bool hillshade_enabled; + float hillshade_level; float hillshade_zenith; float hillshade_azimuth; @@ -81,7 +82,7 @@ Texture3D ecosystem_heightmaps: register(t6); ConstantBuffer jobSettings : register(b0); RWTexture2D output_color : register(u0); -RWTexture2D output_heightmap: register(u1); +RWTexture2D output_heightmap: register(u1); RWTexture2D output_ocean_mask: register(u2); uint4 lerp2d(uint4 ul, uint4 ur, uint4 bl, uint4 br, float2 value) @@ -100,6 +101,14 @@ uint lerp2d(uint ul, uint ur, uint bl, uint br, float2 value) return lerp(topRow, bottomRow, value.y); } +float lanczos(float x, float a) +{ + if (x == 0.0) return 1.0; + if (x < -a || x > a) return 0.0; + x *= PI; + return a * sin(x) * sin(x / a) / (x * x); +} + uint4 interpolate_cubic(float4 v0, float4 v1, float4 v2, float4 v3, float fraction) { float4 p = (v3 - v2) - (v0 - v1); @@ -454,56 +463,56 @@ ProjectedTerrainInfluence calculate_projected_tiles(float2 normal_position, floa } } + // Ensure safe division + result.temp_humidity /= max(result.mask_total, 1.0f); + result.elevation /= max(result.mask_total, 1.0f); + + // Normalize and clamp result.temp_humidity = min(float2(1, 1), max(float2(-1, -1), result.temp_humidity)); result.elevation = min(1, max(-1, result.elevation)); - result.temp_humidity /= result.mask_total; - result.elevation /= result.mask_total; - return result; } -uint4 PackFloatToUInt4(float value, int bit_depth) +uint PackFloatToUInt(float value, int bit_depth) { - if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) - return uint4(0, 0, 0, 0); - // Clamp the input value to the range [-1.0, 1.0] value = clamp(value, -1.0f, 1.0f); // Map the range [-1.0, 1.0] to the range [0.0, 1.0] value = value * 0.5f + 0.5f; - // Convert the float value to 32-bit unsigned integer - float factor = (1 << bit_depth) - 1.0f; - uint intValue = uint(value * factor); + uint intValue; - // Pack the unsigned integer value into a uint4 - uint4 packedValue; + if (bit_depth == 8) { + // Convert the float value to an 8-bit unsigned integer + intValue = uint(value * 255.0f); + } else if (bit_depth == 16) { + // Convert the float value to an 16-bit unsigned integer + intValue = uint(value * 65535.0f); + } else { + intValue = 0; + } - packedValue.x = (intValue >> 0) & 0xFF; + return intValue; +} - if (bit_depth == 8) { //Render as greyscale - packedValue.y = (intValue >> 0) & 0xFF; - packedValue.z = (intValue >> 0) & 0xFF; - packedValue.w = 255; - } else { - //Valid for 16, 24 and 32 bit - packedValue.y = (intValue >> 8) & 0xFF; - packedValue.z = (intValue >> 16) & 0xFF; +int PackFloatToInt(float value, int bit_depth) +{ + // Clamp the input value to the range [-1.0, 1.0] + value = clamp(value, -1.0f, 1.0f); - if (bit_depth == 32) { - packedValue.w = (intValue >> 24) & 0xFF; - } else { - packedValue.w = 255; - } - } + // Map the range [-1.0, 1.0] to the range [0.0, 1.0] + value = value * 0.5f + 0.5f; - return packedValue; + // Convert the float value to an 32-bit signed integer + int intValue = int(value * 4294967295.0f - 2147483648.0f); + + return intValue; } -// TODO: Use the z-thread ID for doing sub-pixel searching -[numthreads(8,8,1)] + +[numthreads(8, 8, 4)] // 4 threads in the z dimension for sub-pixel sampling void main(uint3 tid : SV_DispatchThreadID) { uint2 out_sz; output_color.GetDimensions(out_sz.x, out_sz.y); @@ -518,23 +527,28 @@ void main(uint3 tid : SV_DispatchThreadID) float max_deformation = jobSettings.global_terrain_height_influence + jobSettings.ecosystem_terrain_height_influence; // Calculate normalized position in the world (ie: 0,0 = top-left, 1,1 = bottom-right) - float2 normalized_position = tid.xy / float2(clim_sz) / jobSettings.render_scale; - normalized_position.xy += jobSettings.offset; - //normalized_position.y += 0.25f; - normalized_position = normalized_position % 1; + float2 normalized_position = tid.xy / float2(clim_sz) / jobSettings.render_scale; + normalized_position.xy += jobSettings.offset; + normalized_position = normalized_position % 1; + + // Offsets for sub-pixel sampling based on z-thread ID + float sub_pixel_offset = (tid.z / 4.0) - 0.375; // Example offset, adjust as needed + + // Adjust the normalized position with the sub-pixel offset + normalized_position += sub_pixel_offset / float2(out_sz); // Sample global data - uint4 global_climate = take_sample(planet_climate, normalized_position * clim_sz, clim_sz, jobSettings.interpolation); - float global_height = take_sample(planet_heightmap, normalized_position * hm_sz, hm_sz, jobSettings.interpolation); - uint global_offset = take_sample_nn(planet_offsets, normalized_position * off_sz, off_sz); + uint4 global_climate = take_sample(planet_climate, normalized_position * clim_sz, clim_sz, jobSettings.interpolation); + float global_height = take_sample(planet_heightmap, normalized_position * hm_sz, hm_sz, jobSettings.interpolation); + uint global_offset = take_sample_nn(planet_offsets, normalized_position * off_sz, off_sz); - global_height = (global_height - 32767) / 32767.0f; + global_height = (global_height - 32767) / 32767.0f; global_climate = uint4(global_climate.xy / 2, 0, 0); bool out_override = false; - uint4 out_color = uint4(0, 0, 0, 255); - float out_height = global_height * jobSettings.global_terrain_height_influence; // Value - float out_ocean_mask = 0; + uint4 out_color = uint4(0, 0, 0, 255); + float out_height = global_height * jobSettings.global_terrain_height_influence; // Value + float out_ocean_mask = 0; if (jobSettings.blending_enabled) { // Calculate influence of all neighboring terrain @@ -544,7 +558,7 @@ void main(uint3 tid : SV_DispatchThreadID) out_override = true; out_color.xyz = eco_influence.override; } else { - if(eco_influence.mask_total > 0) { + if (eco_influence.mask_total > 0) { global_climate.yx += eco_influence.temp_humidity * local_influence; out_height += eco_influence.elevation * jobSettings.ecosystem_terrain_height_influence; } @@ -558,8 +572,7 @@ void main(uint3 tid : SV_DispatchThreadID) } if (jobSettings.ocean_enabled && out_height < jobSettings.ocean_depth) { - - if(!out_override) { + if (!out_override) { out_color.xyz = jobSettings.ocean_color.xyz; } @@ -577,24 +590,28 @@ void main(uint3 tid : SV_DispatchThreadID) out_height = jobSettings.ocean_depth; } } else { - //Color already applied, no need to do anything + // Color already applied, no need to do anything out_ocean_mask = 0; } - // DEBUG: Grid rendering - if(jobSettings.debug_mode & DEBUG_GRID) - { + // DEBUG: Grid rendering + if (jobSettings.debug_mode & DEBUG_GRID) { int2 cell_position = (normalized_position.xy * clim_sz * jobSettings.render_scale) % jobSettings.render_scale; - if (cell_position.x == 0 || cell_position.y == 0) - { + if (cell_position.x == 0 || cell_position.y == 0) { out_color.xyz = uint3(255, 0, 0); } - } + } // Squash out_height from meter range to normalized +/- 1.0 range out_height /= max_deformation; - output_color[tid.xy] = out_color; - output_heightmap[tid.xy] = PackFloatToUInt4(out_height, jobSettings.heightmap_bit_depth); - output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 255, 0), 255); + output_color[tid.xy] = out_color; + + if (jobSettings.heightmap_bit_depth == 32) { + output_heightmap[tid.xy] = PackFloatToInt(out_height, jobSettings.heightmap_bit_depth); + } else { + output_heightmap[tid.xy] = PackFloatToUInt(out_height, jobSettings.heightmap_bit_depth); + } + + output_ocean_mask[tid.xy] = min(max(out_ocean_mask * 255, 0), 255); } \ No newline at end of file diff --git a/starfab/planets/planet_renderer.py b/starfab/planets/planet_renderer.py index 60a6cf7..8ee98c6 100644 --- a/starfab/planets/planet_renderer.py +++ b/starfab/planets/planet_renderer.py @@ -56,7 +56,9 @@ class PlanetRenderer: self._write_planet_input_resources() def set_settings(self, settings: RenderSettings): + self._cleanup_gpu_output_resources() # Cleanup current GPU resources self.settings = settings + self._create_gpu_output_resources() # Create new GPU resources with updated settings def set_resolution(self, new_dimensions: Tuple[int, int]): self._cleanup_gpu_output_resources() @@ -109,6 +111,7 @@ class PlanetRenderer: job_s.render_scale_y = self.settings.resolution job_s.blending_enabled = self.settings.blending_enabled job_s.hillshade_enabled = self.settings.hillshade_enabled + job_s.hillshade_level = self.settings.hillshade_level job_s.ocean_mask_binary = self.settings.ocean_mask_binary job_s.heightmap_bit_depth = self.settings.heightmap_bit_depth job_s.debug_mode = self.settings.debug_mode @@ -136,7 +139,21 @@ class PlanetRenderer: del hillshade_compute out_color: Image = self._read_frame("output_color", "RGBA") - out_heightmap: Image = self._read_frame("output_heightmap", "RGBA") + + # Check if self.settings is defined; otherwise, revert to R8_UINT + if self.settings is None: + out_heightmap: Image = self._read_frame("output_heightmap", "L") + else: + # Determine the appropriate format for the heightmap texture based on the bit depth + if self.settings.heightmap_bit_depth == 8: + out_heightmap: Image = self._read_frame("output_heightmap", "L") + elif self.settings.heightmap_bit_depth == 16: + out_heightmap: Image = self._read_frame("output_heightmap", "I;16") + elif self.settings.heightmap_bit_depth == 32: + out_heightmap: Image = self._read_frame("output_heightmap", "I;32") + else: + raise ValueError(f"Unsupported heightmap bit depth: {self.settings.heightmap_bit_depth}") + out_oceanmask: Image = self._read_frame("output_ocean_mask", "L") planet_bouds = self.get_outer_bounds() @@ -145,7 +162,7 @@ class PlanetRenderer: return RenderResult(job_s, out_color, out_heightmap, out_oceanmask, self.render_resolution, planet_bouds, render_bounds) - def _read_frame(self, resource_name: str, mode: Literal['L', 'RGBA']) -> Image: + def _read_frame(self, resource_name: str, mode: Literal['L', 'RGBA', 'I;16', 'I;32']) -> Image: resource: Resource = self.gpu_resources['readback'] readback: Buffer = cast(Buffer, resource) destination: Texture2D = cast(Texture2D, self.gpu_resources[resource_name]) @@ -153,9 +170,14 @@ class PlanetRenderer: output_bytes = readback.readback() del readback - return Image.frombuffer(mode, - (destination.width, destination.height), - output_bytes) + if mode == 'I;16': # 16-bit unsigned integer, single channel + return Image.frombuffer('I;16', (destination.width, destination.height), output_bytes, 'raw', 'I;16', 0, 1) + elif mode == 'I;32': # 32-bit signed integer, single channel + return Image.frombuffer('I', (destination.width, destination.height), output_bytes, 'raw', 'I', 0, 1) + else: + return Image.frombuffer(mode, + (destination.width, destination.height), + output_bytes) def _get_computer(self) -> Compute: res = self.gpu_resources @@ -295,7 +317,21 @@ class PlanetRenderer: out_w = self.render_resolution.width() out_h = self.render_resolution.height() output_color_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) - output_heightmap_texture = Texture2D(out_w, out_h, R8G8B8A8_UINT) + + # Check if self.settings is defined; otherwise, revert to R8_UINT + if self.settings is None: + output_heightmap_texture = Texture2D(out_w, out_h, R8_UINT) + else: + # Determine the appropriate format for the heightmap texture based on the bit depth + if self.settings.heightmap_bit_depth == 8: + output_heightmap_texture = Texture2D(out_w, out_h, R8_UINT) + elif self.settings.heightmap_bit_depth == 16: + output_heightmap_texture = Texture2D(out_w, out_h, R16_UINT) + elif self.settings.heightmap_bit_depth == 32: + output_heightmap_texture = Texture2D(out_w, out_h, R32_SINT) + else: + raise ValueError(f"Unsupported heightmap bit depth: {self.settings.heightmap_bit_depth}") + output_ocean_mask_texture = Texture2D(out_w, out_h, R8_UINT) self.gpu_resources['output_color'] = output_color_texture diff --git a/starfab/resources/ui/PlanetView.ui b/starfab/resources/ui/PlanetView.ui index d986319..182ebfb 100644 --- a/starfab/resources/ui/PlanetView.ui +++ b/starfab/resources/ui/PlanetView.ui @@ -142,62 +142,82 @@ + + + + Heightmap Bit Depth + + + + + + + + + Hillshade Level + + + + + + + Display Mode - + - + Display Layer - + - + Enable Grid - + true - + Enable Crosshair - + true - + Enable Waypoints - + true @@ -205,58 +225,48 @@ - + - Enable Hillshade + Enable Ecosystem Blending - + + + true + + - + - Enable Binary Ocean Mask + Enable Hillshade - + - + - Waypoints + Enable Binary Ocean Mask - - - - - - Heightmap Bit Depth - - - - - + - - + + - Enable Ecosystem Blending + Waypoints - - - - true - - + + -- GitLab