meltygui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
"""Voxel presentation: volume passes, camera interaction and in-scene labels."""
|
|
2
|
+
from meltygui.core.graphics.tensor_core import _voxels_cleanup
|
|
3
|
+
from meltygui.hdr_color import pack_color
|
|
4
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
5
|
+
from meltygui.model.lut_model import Lut, LutPalette
|
|
6
|
+
from meltygui.model.tensor_model import TensorDim
|
|
7
|
+
from meltygui.model.tensor_model import TensorDims
|
|
8
|
+
from meltygui.core.rendering.modes import Modes
|
|
9
|
+
from meltygui.core.core_render import render_func
|
|
10
|
+
from meltygui.core.rendering.shaped import Shaped
|
|
11
|
+
from meltygui.core.runtime.toggles import SwooshMode
|
|
12
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
13
|
+
from meltygui.view.header_view import draw_header
|
|
14
|
+
import OpenGL.GL as gl
|
|
15
|
+
import math
|
|
16
|
+
import meltygui_imgui as imgui
|
|
17
|
+
import numpy as np
|
|
18
|
+
import ctypes
|
|
19
|
+
from meltygui.core.styling.fonts import Font
|
|
20
|
+
from meltygui.core.graphics.text_texture import bake_texts
|
|
21
|
+
from meltygui.model.camera_model import basis as _cam_basis
|
|
22
|
+
from meltygui.core.graphics.shader_func import shader_func
|
|
23
|
+
from meltygui.core.graphics.tensor_core import source_identity
|
|
24
|
+
from meltygui.core.windowing.glfw_utils import print_stack_trace
|
|
25
|
+
from meltygui.model.texture_model import _cached_volume_texture, _upload_cuda_image
|
|
26
|
+
from meltygui.state.voxel_state import VoxelState
|
|
27
|
+
from meltygui.view.texture_view import image_blit_pass
|
|
28
|
+
from meltygui.view.tensor_view import _describe_tensor, _view_size, _draw_image_notice
|
|
29
|
+
from meltygui.view.tensor_view import _draw_tensor_meta, _draw_voxel_error, _draw_slice_sliders, _tick_values
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@render_func(is_default_for=("GLTexture",
|
|
33
|
+
# 3-D+ tensors by shape; 1-D/2-D route to
|
|
34
|
+
# draw_line_graph. The bare "Tensor" name stays
|
|
35
|
+
# as the fallback for 0-D / anything unmatched
|
|
36
|
+
# (the error card is the right place for those).
|
|
37
|
+
Shaped("Tensor", (None, None, None, ...)),
|
|
38
|
+
Shaped("ndarray", (None, None, None, ...)),
|
|
39
|
+
"Tensor", "ndarray"),
|
|
40
|
+
show_bg=True, selectable=True,
|
|
41
|
+
auto_resize=False, min_width=269, with_header=draw_header,
|
|
42
|
+
bg_offset=0, min_height=293, disable_scroll=True, use_cache=True,
|
|
43
|
+
on_cleanup=_voxels_cleanup)
|
|
44
|
+
def draw_voxels(input_value: object = None, gl_state: GLState = None, selectable=False,
|
|
45
|
+
draw_state=None,
|
|
46
|
+
# ── camera + shading: cam_* names dodge the legacy DrawState
|
|
47
|
+
# zoom/brightness/contrast fields (name-colliding params are
|
|
48
|
+
# excluded from auto-state). Gestures/panel write
|
|
49
|
+
# draw_state.<name>; diverged values persist. ──
|
|
50
|
+
tilt=0.283, spin=0.724, roll=0.0, cam_zoom=3.4,
|
|
51
|
+
# [tint=(0.084, 0.472, 0.148, 1.0)]
|
|
52
|
+
pan_x=0.0, pan_y=0.0, pan_z=0.0, ortho=False,
|
|
53
|
+
cam_brightness=1.332, cam_contrast=1.0,
|
|
54
|
+
# density = the old densityScale (haze gain over the opacity
|
|
55
|
+
# gate); threshold = the old opacityThreshold (higher → lower
|
|
56
|
+
# gate → more opaque)
|
|
57
|
+
density=0.7, threshold=0.297, centered=False,
|
|
58
|
+
nearest=True, lut=Lut("jet"), step_size=0.0005, max_steps=4096,
|
|
59
|
+
# ── shadow catcher: the invisible plane the box rests on -
|
|
60
|
+
# it renders nothing but the volume's cast shadow (one-sided:
|
|
61
|
+
# no shadow from below). shadow_opacity scales how dark the
|
|
62
|
+
# caught shadow composites; shadow_softness scales the
|
|
63
|
+
# screen-space penumbra blur (radius grows with occluder
|
|
64
|
+
# height) - 0 = hard edge, bigger = wider penumbra. ──
|
|
65
|
+
draw_plane=True, shadow_opacity=1.0, shadow_softness=0.15,
|
|
66
|
+
# ── lighting: draw_shading lights the floor (per-s., the
|
|
67
|
+
# raymarched cast shadow) and the volume (gradient-normal
|
|
68
|
+
# Lambert); self_shading adds the per-sample transmittance
|
|
69
|
+
# march inside the volume - the expensive tier. ambient_light
|
|
70
|
+
# is the shadow floor: how much light survives everywhere.
|
|
71
|
+
# shading_strength scales how much the volume's cast normal
|
|
72
|
+
# may darken its LUT color (0 = shading off, plane still
|
|
73
|
+
# catches). ──
|
|
74
|
+
draw_shading=True, self_shading=True,
|
|
75
|
+
light_pos=(50.0, -50.0, 200.0), light_tint=(1.0, 1.0, 1.0),
|
|
76
|
+
light_brightness=1.622, ambient_light=0.3, shading_strength=0.7,
|
|
77
|
+
# ── axis mapping: dims by index OR NAME. The first three dims
|
|
78
|
+
# by default; None still means "derive" (last three → z/y/x)
|
|
79
|
+
# for anything that clears one. ──
|
|
80
|
+
dim_names=("layer", "batch", "token", "feature"),
|
|
81
|
+
x_dim=TensorDim(0), y_dim=TensorDim(2), z_dim=TensorDim(2),
|
|
82
|
+
slices=(),
|
|
83
|
+
mean_dims=TensorDims(()), sort_dim=TensorDim(-1),
|
|
84
|
+
normalize=False, nf_on=False, nf_chop=TensorDim(-1),
|
|
85
|
+
nf_along=TensorDim(-1), nf_chunk=128,
|
|
86
|
+
# ── volume furniture (screen px) ──
|
|
87
|
+
name_size=17.0, name_padding=30.1, name_opacity=1.1,
|
|
88
|
+
num_size=17.1, num_padding=5.5, num_opacity=0.8,
|
|
89
|
+
num_spacing=1.0, num_angle=0.0, z_offset=1,
|
|
90
|
+
middle_mouse_drag=None, double_right_mouse_drag=None,
|
|
91
|
+
scroll_y_changed=None, space_mouse_changed=None,
|
|
92
|
+
left_mouse_double_clicked=None,
|
|
93
|
+
kp_7_pressed=None, kp_1_pressed=None, kp_3_pressed=None,
|
|
94
|
+
kp_5_pressed=None, slash_pressed=None, kp_divide_pressed=None,
|
|
95
|
+
kp_decimal_pressed=None, font_manager=None,
|
|
96
|
+
luts: LutPalette = None, voxel_state: VoxelState = None,
|
|
97
|
+
keyboard_available=True, pointer_buttons_down=False, **kwargs):
|
|
98
|
+
"""Render voxels using CUDA for CUDA tensors and OpenGL for other inputs."""
|
|
99
|
+
parameters = locals()
|
|
100
|
+
extra = parameters.pop("kwargs")
|
|
101
|
+
return _draw_voxels(**(extra | parameters | {"backend": "auto"}))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@render_func(show_bg=True, selectable=True,
|
|
105
|
+
auto_resize=False, min_width=269, with_header=draw_header,
|
|
106
|
+
bg_offset=0, min_height=293, disable_scroll=True, use_cache=True,
|
|
107
|
+
on_cleanup=_voxels_cleanup)
|
|
108
|
+
def draw_voxels_opengl(input_value: object = None, gl_state: GLState = None, selectable=False,
|
|
109
|
+
draw_state=None,
|
|
110
|
+
tilt=0.283, spin=0.724, roll=0.0, cam_zoom=3.4,
|
|
111
|
+
pan_x=0.0, pan_y=0.0, pan_z=0.0, ortho=False,
|
|
112
|
+
cam_brightness=1.332, cam_contrast=1.0,
|
|
113
|
+
density=0.7, threshold=0.297, centered=False,
|
|
114
|
+
nearest=True, lut=Lut("jet"), step_size=0.0005, max_steps=4096,
|
|
115
|
+
draw_plane=True, shadow_opacity=1.0, shadow_softness=0.15,
|
|
116
|
+
draw_shading=True, self_shading=True,
|
|
117
|
+
light_pos=(50.0, -50.0, 200.0), light_tint=(1.0, 1.0, 1.0),
|
|
118
|
+
light_brightness=1.622, ambient_light=0.3, shading_strength=0.7,
|
|
119
|
+
dim_names=("layer", "batch", "token", "feature"),
|
|
120
|
+
x_dim=TensorDim(0), y_dim=TensorDim(2), z_dim=TensorDim(2),
|
|
121
|
+
slices=(),
|
|
122
|
+
mean_dims=TensorDims(()), sort_dim=TensorDim(-1),
|
|
123
|
+
normalize=False, nf_on=False, nf_chop=TensorDim(-1),
|
|
124
|
+
nf_along=TensorDim(-1), nf_chunk=128,
|
|
125
|
+
name_size=17.0, name_padding=30.1, name_opacity=1.1,
|
|
126
|
+
num_size=17.1, num_padding=5.5, num_opacity=0.8,
|
|
127
|
+
num_spacing=1.0, num_angle=0.0, z_offset=1,
|
|
128
|
+
middle_mouse_drag=None, double_right_mouse_drag=None,
|
|
129
|
+
scroll_y_changed=None, space_mouse_changed=None,
|
|
130
|
+
left_mouse_double_clicked=None,
|
|
131
|
+
kp_7_pressed=None, kp_1_pressed=None, kp_3_pressed=None,
|
|
132
|
+
kp_5_pressed=None, slash_pressed=None, kp_divide_pressed=None,
|
|
133
|
+
kp_decimal_pressed=None, font_manager=None,
|
|
134
|
+
luts: LutPalette = None, voxel_state: VoxelState = None,
|
|
135
|
+
keyboard_available=True, pointer_buttons_down=False, **kwargs):
|
|
136
|
+
"""Raymarch tensors, NumPy arrays or GLTexture values with OpenGL."""
|
|
137
|
+
parameters = locals()
|
|
138
|
+
extra = parameters.pop("kwargs")
|
|
139
|
+
return _draw_voxels(**(extra | parameters | {"backend": "opengl"}))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@render_func(show_bg=True, selectable=True,
|
|
143
|
+
auto_resize=False, min_width=269, with_header=draw_header,
|
|
144
|
+
bg_offset=0, min_height=293, disable_scroll=True, use_cache=True,
|
|
145
|
+
on_cleanup=_voxels_cleanup)
|
|
146
|
+
def draw_voxels_cuda(input_value: object = None, gl_state: GLState = None, selectable=False,
|
|
147
|
+
draw_state=None,
|
|
148
|
+
tilt=0.283, spin=0.724, roll=0.0, cam_zoom=3.4,
|
|
149
|
+
pan_x=0.0, pan_y=0.0, pan_z=0.0, ortho=False,
|
|
150
|
+
cam_brightness=1.332, cam_contrast=1.0,
|
|
151
|
+
density=0.7, threshold=0.297, centered=False,
|
|
152
|
+
nearest=True, lut=Lut("jet"), step_size=0.0005, max_steps=4096,
|
|
153
|
+
draw_plane=True, shadow_opacity=1.0, shadow_softness=0.15,
|
|
154
|
+
draw_shading=True, self_shading=True,
|
|
155
|
+
light_pos=(50.0, -50.0, 200.0), light_tint=(1.0, 1.0, 1.0),
|
|
156
|
+
light_brightness=1.622, ambient_light=0.3, shading_strength=0.7,
|
|
157
|
+
dim_names=("layer", "batch", "token", "feature"),
|
|
158
|
+
x_dim=TensorDim(0), y_dim=TensorDim(2), z_dim=TensorDim(2),
|
|
159
|
+
slices=(),
|
|
160
|
+
mean_dims=TensorDims(()), sort_dim=TensorDim(-1),
|
|
161
|
+
normalize=False, nf_on=False, nf_chop=TensorDim(-1),
|
|
162
|
+
nf_along=TensorDim(-1), nf_chunk=128,
|
|
163
|
+
name_size=17.0, name_padding=30.1, name_opacity=1.1,
|
|
164
|
+
num_size=17.1, num_padding=5.5, num_opacity=0.8,
|
|
165
|
+
num_spacing=1.0, num_angle=0.0, z_offset=1,
|
|
166
|
+
middle_mouse_drag=None, double_right_mouse_drag=None,
|
|
167
|
+
scroll_y_changed=None, space_mouse_changed=None,
|
|
168
|
+
left_mouse_double_clicked=None,
|
|
169
|
+
kp_7_pressed=None, kp_1_pressed=None, kp_3_pressed=None,
|
|
170
|
+
kp_5_pressed=None, slash_pressed=None, kp_divide_pressed=None,
|
|
171
|
+
kp_decimal_pressed=None, font_manager=None,
|
|
172
|
+
luts: LutPalette = None, voxel_state: VoxelState = None,
|
|
173
|
+
keyboard_available=True, pointer_buttons_down=False, **kwargs):
|
|
174
|
+
"""Raymarch a CUDA tensor in place on its own GPU."""
|
|
175
|
+
parameters = locals()
|
|
176
|
+
extra = parameters.pop("kwargs")
|
|
177
|
+
return _draw_voxels(**(extra | parameters | {"backend": "cuda"}))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _draw_voxels(input_value: object = None, gl_state: GLState = None, selectable=False,
|
|
181
|
+
draw_state=None,
|
|
182
|
+
# ── camera + shading: cam_* names dodge the legacy DrawState
|
|
183
|
+
# zoom/brightness/contrast fields (name-colliding params are
|
|
184
|
+
# excluded from auto-state). Gestures/panel write
|
|
185
|
+
# draw_state.<name>; diverged values persist. ──
|
|
186
|
+
tilt=0.283, spin=0.724, roll=0.0, cam_zoom=3.4,
|
|
187
|
+
# [tint=(0.084, 0.472, 0.148, 1.0)]
|
|
188
|
+
pan_x=0.0, pan_y=0.0, pan_z=0.0, ortho=False,
|
|
189
|
+
cam_brightness=1.332, cam_contrast=1.0,
|
|
190
|
+
# density = the old densityScale (haze gain over the opacity
|
|
191
|
+
# gate); threshold = the old opacityThreshold (higher → lower
|
|
192
|
+
# gate → more opaque)
|
|
193
|
+
density=0.7, threshold=0.297, centered=False,
|
|
194
|
+
nearest=True, lut=Lut("jet"), step_size=0.0005, max_steps=4096,
|
|
195
|
+
# ── shadow catcher: the invisible plane the box rests on -
|
|
196
|
+
# it renders nothing but the volume's cast shadow (one-sided:
|
|
197
|
+
# no shadow from below). shadow_opacity scales how dark the
|
|
198
|
+
# caught shadow composites; shadow_softness scales the
|
|
199
|
+
# screen-space penumbra blur (radius grows with occluder
|
|
200
|
+
# height) - 0 = hard edge, bigger = wider penumbra. ──
|
|
201
|
+
draw_plane=True, shadow_opacity=1.0, shadow_softness=0.15,
|
|
202
|
+
# ── lighting: draw_shading lights the floor (per-s., the
|
|
203
|
+
# raymarched cast shadow) and the volume (gradient-normal
|
|
204
|
+
# Lambert); self_shading adds the per-sample transmittance
|
|
205
|
+
# march inside the volume - the expensive tier. ambient_light
|
|
206
|
+
# is the shadow floor: how much light survives everywhere.
|
|
207
|
+
# shading_strength scales how much the volume's cast normal
|
|
208
|
+
# may darken its LUT color (0 = shading off, plane still
|
|
209
|
+
# catches). ──
|
|
210
|
+
draw_shading=True, self_shading=True,
|
|
211
|
+
light_pos=(50.0, -50.0, 200.0), light_tint=(1.0, 1.0, 1.0),
|
|
212
|
+
light_brightness=1.622, ambient_light=0.3, shading_strength=0.7,
|
|
213
|
+
# ── axis mapping: dims by index OR NAME. The first three dims
|
|
214
|
+
# by default; None still means "derive" (last three → z/y/x)
|
|
215
|
+
# for anything that clears one. ──
|
|
216
|
+
dim_names=("layer", "batch", "token", "feature"),
|
|
217
|
+
x_dim=TensorDim(0), y_dim=TensorDim(2), z_dim=TensorDim(2),
|
|
218
|
+
slices=(),
|
|
219
|
+
mean_dims=TensorDims(()), sort_dim=TensorDim(-1),
|
|
220
|
+
normalize=False, nf_on=False, nf_chop=TensorDim(-1),
|
|
221
|
+
nf_along=TensorDim(-1), nf_chunk=128,
|
|
222
|
+
# ── volume furniture (screen px) ──
|
|
223
|
+
name_size=17.0, name_padding=30.1, name_opacity=1.1,
|
|
224
|
+
num_size=17.1, num_padding=5.5, num_opacity=0.8,
|
|
225
|
+
num_spacing=1.0, num_angle=0.0, z_offset=1,
|
|
226
|
+
middle_mouse_drag=None, double_right_mouse_drag=None,
|
|
227
|
+
scroll_y_changed=None, space_mouse_changed=None,
|
|
228
|
+
left_mouse_double_clicked=None,
|
|
229
|
+
kp_7_pressed=None, kp_1_pressed=None, kp_3_pressed=None,
|
|
230
|
+
kp_5_pressed=None, slash_pressed=None, kp_divide_pressed=None,
|
|
231
|
+
kp_decimal_pressed=None, font_manager=None,
|
|
232
|
+
luts: LutPalette = None, voxel_state: VoxelState = None,
|
|
233
|
+
keyboard_available=True, pointer_buttons_down=False, backend="auto", **kwargs):
|
|
234
|
+
"""The voxel renderer — owner of every render and mapping decision.
|
|
235
|
+
Input is a tensor/ndarray (sliced + uploaded HERE, re-keyed by gl_state
|
|
236
|
+
deps on source identity/_version/mapping) or an already-uploaded
|
|
237
|
+
GLTexture (rendered as-is). Tensor METADATA — full shape, dim count —
|
|
238
|
+
rides the uploaded buffer; EVERYTHING else is a parameter on this
|
|
239
|
+
signature (auto draw_state params: gestures and the controls panel
|
|
240
|
+
write draw_state.<name>, only diverged values persist/serialize)."""
|
|
241
|
+
from meltygui.core.graphics.gl_state import GLTexture
|
|
242
|
+
from meltygui.core.graphics.gl_state import gl_limits
|
|
243
|
+
from meltygui.core.graphics.gl_state import texture3d_fit
|
|
244
|
+
from meltygui.model.camera_model import apply_space_mouse
|
|
245
|
+
from meltygui.model.tensor_model import CudaVolumeView
|
|
246
|
+
from meltygui.model.tensor_model import _AXIS_POS
|
|
247
|
+
from meltygui.model.tensor_model import _clean_dim_name
|
|
248
|
+
from meltygui.model.tensor_model import _resolve_dim
|
|
249
|
+
from meltygui.model.tensor_model import _volume_scale
|
|
250
|
+
from meltygui.model.tensor_model import auto_neural_flow
|
|
251
|
+
from meltygui.model.tensor_model import slice_volume
|
|
252
|
+
from meltygui.model.tensor_model import slice_volume_view
|
|
253
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
254
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
255
|
+
|
|
256
|
+
src = input_value
|
|
257
|
+
if src is None:
|
|
258
|
+
# Between a live value being released (a new run's first publish
|
|
259
|
+
# drops the previous generation) and the new data arriving, the
|
|
260
|
+
# window renders with no value. Hold the LAST image - the FBO
|
|
261
|
+
# survives that release - and keep the slice sliders up (their
|
|
262
|
+
# geometry comes from the metadata still on the cached volume
|
|
263
|
+
# entry) instead of flashing an error card; a window that never
|
|
264
|
+
# rendered shows nothing.
|
|
265
|
+
fb = gl_state.peek("target")
|
|
266
|
+
if fb is not None:
|
|
267
|
+
dim_names = tuple(_clean_dim_name(x, i) for i, x in enumerate(dim_names or ()))
|
|
268
|
+
slices = tuple(int(v) for v in (slices or ()))
|
|
269
|
+
mean_dims = tuple(int(v) for v in (mean_dims or ()))
|
|
270
|
+
meta = None
|
|
271
|
+
for key in ("volume_cuda", "volume", "cuda_view"):
|
|
272
|
+
rec = gl_state.peek(key)
|
|
273
|
+
if rec is not None:
|
|
274
|
+
meta = getattr(rec, "texture", rec)
|
|
275
|
+
break
|
|
276
|
+
source_shape = tuple(getattr(meta, "source_shape", ()) or ())
|
|
277
|
+
mapping = getattr(meta, "mapping", None)
|
|
278
|
+
# the marker's dim_names merge skips a None value; use the names
|
|
279
|
+
# the last valid frame stamped
|
|
280
|
+
dim_names = tuple(getattr(meta, "dim_names", None) or dim_names)
|
|
281
|
+
slider_dims = []
|
|
282
|
+
if mapping is not None and len(source_shape) > 3:
|
|
283
|
+
slider_dims = [d for d in range(len(source_shape))
|
|
284
|
+
if d not in mapping and d not in mean_dims
|
|
285
|
+
and source_shape[d] > 1]
|
|
286
|
+
width, height = _view_size(draw_state)
|
|
287
|
+
if slider_dims:
|
|
288
|
+
height = max(100, height - int(imgui.get_frame_height_with_spacing())
|
|
289
|
+
* len(slider_dims))
|
|
290
|
+
img_pos = imgui.get_cursor_screen_pos()
|
|
291
|
+
imgui.image(fb.texture_id, width, height, uv0=(0, 1), uv1=(1, 0))
|
|
292
|
+
# the 2-D axis outline is drawn over the image each frame (not
|
|
293
|
+
# rendered into the FBO) - recompute it from the last volume's
|
|
294
|
+
# extents + the current camera so it doesn't blink either
|
|
295
|
+
shape = tuple(getattr(meta, "shape", ()) or ())
|
|
296
|
+
if len(shape) == 3:
|
|
297
|
+
try:
|
|
298
|
+
edges = _axis_edges(tilt, spin, cam_zoom, width / height, width, height,
|
|
299
|
+
scale=_volume_scale(shape),
|
|
300
|
+
pan=(pan_x, pan_y, pan_z), ortho=ortho, roll=roll)
|
|
301
|
+
if edges:
|
|
302
|
+
_draw_axis_lines(imgui.get_window_draw_list(), img_pos, edges)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
_draw_slice_sliders(draw_state, slider_dims, dim_names, slices, source_shape, width)
|
|
306
|
+
return False, None
|
|
307
|
+
# A 1-D texture is a LUT, not a volume - don't try to raymarch it.
|
|
308
|
+
if getattr(src, "target", None) == int(gl.GL_TEXTURE_1D):
|
|
309
|
+
imgui.text(f"{src!r} — a LUT, not a volume")
|
|
310
|
+
return False, None
|
|
311
|
+
|
|
312
|
+
dim_names = tuple(_clean_dim_name(x, i) for i, x in enumerate(dim_names or ()))
|
|
313
|
+
slices = tuple(int(v) for v in (slices or ()))
|
|
314
|
+
mean_dims = tuple(int(v) for v in (mean_dims or ()))
|
|
315
|
+
|
|
316
|
+
# ── source → display volume → GPU, parameter-driven and stateless:
|
|
317
|
+
# slice_volume is a pure function of the params, the upload re-runs
|
|
318
|
+
# exactly when its deps change, and tensor metadata rides the buffer.
|
|
319
|
+
if backend == "cuda" and not bool(getattr(src, "is_cuda", False)):
|
|
320
|
+
_draw_voxel_error(draw_state, "CUDA voxel rendering requires a CUDA tensor.")
|
|
321
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda"); gl_state.drop("cuda_view")
|
|
322
|
+
return False, input_value
|
|
323
|
+
if isinstance(src, GLTexture):
|
|
324
|
+
tex, mapping = src, None
|
|
325
|
+
source_shape = tuple(getattr(src, "source_shape", src.shape))
|
|
326
|
+
else:
|
|
327
|
+
try:
|
|
328
|
+
import torch
|
|
329
|
+
except ImportError:
|
|
330
|
+
_draw_voxel_error(draw_state, "Voxel rendering of arrays and tensors requires torch:\n"
|
|
331
|
+
"pip install meltygui[tensor]")
|
|
332
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
333
|
+
return False, input_value
|
|
334
|
+
try:
|
|
335
|
+
t = src if isinstance(src, torch.Tensor) else torch.from_numpy(np.asarray(src))
|
|
336
|
+
except (TypeError, ValueError, RuntimeError) as e:
|
|
337
|
+
_draw_voxel_error(draw_state, f"{type(src).__name__} is not tensor-shaped:\n{e}")
|
|
338
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
339
|
+
return False, None
|
|
340
|
+
# ── preflight: slicing + upload can fail on BAD DATA (odd dtypes,
|
|
341
|
+
# empty tensors) or on the DRIVER (an extent past
|
|
342
|
+
# GL_MAX_3D_TEXTURE_SIZE, a volume larger than VRAM). Decide here,
|
|
343
|
+
# before any GL interaction: over-limit extents CLAMP to the
|
|
344
|
+
# displayable prefix (with a notice over the image), anything
|
|
345
|
+
# unrecoverable shows an error card in place of the 3-D view. The
|
|
346
|
+
# error path also flushes stale textures so a bad frame never
|
|
347
|
+
# keeps a previous volume alive under the message. ─────────────
|
|
348
|
+
# ── auto neural flow: if nf OFF, a displayed axis longer than the
|
|
349
|
+
# readability cap (Toggles.Voxels.auto_flow_extent) or the GL limit
|
|
350
|
+
# is wrapped HERE - effective nf_* locals for this render only (the
|
|
351
|
+
# user's params stay untouched; the labels below use the effective
|
|
352
|
+
# values and show e.g. "vocab % 180" or "batch - vocab"). ───────────
|
|
353
|
+
nf_pad = False
|
|
354
|
+
# Explicit OpenGL selection uploads the sliced tensor to a 3-D
|
|
355
|
+
# texture, including CUDA tensors. Automatic selection keeps CUDA
|
|
356
|
+
# sources in place; the CUDA renderer never silently falls back.
|
|
357
|
+
use_cuda = backend != "opengl" and bool(getattr(t, "is_cuda", False))
|
|
358
|
+
if use_cuda and not _cuda_march_ready():
|
|
359
|
+
_draw_voxel_error(draw_state, "CUDA tensor rendering requires meltygui[tensor]; "
|
|
360
|
+
"the tensor was not copied to the CPU.")
|
|
361
|
+
return False, None
|
|
362
|
+
if not nf_on:
|
|
363
|
+
_cap = int(Toggles.Voxels.auto_flow_extent)
|
|
364
|
+
if use_cuda:
|
|
365
|
+
# no 3-D texture → no GL extent limit; only the readability cap
|
|
366
|
+
_cap = _cap if _cap > 0 else 0
|
|
367
|
+
else:
|
|
368
|
+
_gl_max = int(gl_limits()["max_3d"])
|
|
369
|
+
_cap = min(_cap, _gl_max) if _cap > 0 else _gl_max
|
|
370
|
+
auto = auto_neural_flow(t.shape, dim_names, x_dim, y_dim, z_dim, _cap)
|
|
371
|
+
if auto is not None:
|
|
372
|
+
nf_on, nf_pad = True, True
|
|
373
|
+
nf_chop, nf_along, nf_chunk = (TensorDim(auto[0]),
|
|
374
|
+
TensorDim(auto[1]), auto[2])
|
|
375
|
+
# ── cache gate BEFORE any tensor work: slice_volume is pure but not
|
|
376
|
+
# free - on a multi-GB tensor its type coercion / permute-contiguous
|
|
377
|
+
# / neural-flow repack / normalize min-max are whole-tensor GPU
|
|
378
|
+
# passes, and running them every frame (the upload was already
|
|
379
|
+
# version-gated, the slice that feeds it was not) pinned an orbit
|
|
380
|
+
# at ~18 fps with the raymarcher all the way down. The key is
|
|
381
|
+
# the params that decide the volume (the effective nf_* after
|
|
382
|
+
# auto-flow); the volume-derived facts (mapping, source_shape,
|
|
383
|
+
# clamp_note) ride the cached texture. ─────────────────────────
|
|
384
|
+
vol_key = (source_identity(src), dim_names,
|
|
385
|
+
str(x_dim), str(y_dim), str(z_dim), slices, mean_dims,
|
|
386
|
+
int(sort_dim), bool(normalize), bool(nf_on), str(nf_chop),
|
|
387
|
+
str(nf_along), int(nf_chunk), nf_pad, use_cuda)
|
|
388
|
+
tex = _cached_volume_texture(gl_state, vol_key)
|
|
389
|
+
if tex is not None:
|
|
390
|
+
mapping, source_shape = tex.mapping, tex.source_shape
|
|
391
|
+
vol = None
|
|
392
|
+
if not isinstance(src, GLTexture) and tex is None and use_cuda:
|
|
393
|
+
try:
|
|
394
|
+
cv = slice_volume_view(
|
|
395
|
+
t, dim_names, x_dim, y_dim, z_dim, slices, mean_dims,
|
|
396
|
+
sort_dim, normalize, nf_on, nf_chop, nf_along, nf_chunk,
|
|
397
|
+
nf_pad=nf_pad)
|
|
398
|
+
except (ValueError, TypeError, RuntimeError, IndexError) as e:
|
|
399
|
+
_draw_voxel_error(draw_state, f"can't build a volume view from "
|
|
400
|
+
f"{_describe_tensor(t)}:\n{e}")
|
|
401
|
+
gl_state.drop("cuda_view")
|
|
402
|
+
return False, None
|
|
403
|
+
cv._vol_key = vol_key
|
|
404
|
+
cv.dim_names = dim_names
|
|
405
|
+
# The view is the cached "volume": gl_state holds it by the same key
|
|
406
|
+
# discipline as the textures (no GL - nothing to delete). The GL
|
|
407
|
+
# volume (possibly GBs of display-GPU VRAM) is released on the
|
|
408
|
+
# switch, and vice versa below.
|
|
409
|
+
gl_state.drop("cuda_view")
|
|
410
|
+
gl_state.get("cuda_view", lambda: cv, deps=vol_key)
|
|
411
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
412
|
+
tex, mapping, source_shape = cv, cv.mapping, cv.source_shape
|
|
413
|
+
if not isinstance(src, GLTexture) and tex is None:
|
|
414
|
+
gl_state.drop("cuda_view")
|
|
415
|
+
try:
|
|
416
|
+
vol, mapping, source_shape = slice_volume(
|
|
417
|
+
t, dim_names, x_dim, y_dim, z_dim, slices, mean_dims,
|
|
418
|
+
sort_dim, normalize, nf_on, nf_chop, nf_along, nf_chunk,
|
|
419
|
+
nf_pad=nf_pad)
|
|
420
|
+
except (ValueError, TypeError, RuntimeError, IndexError) as e:
|
|
421
|
+
_draw_voxel_error(draw_state, f"can't build a volume from "
|
|
422
|
+
f"{_describe_tensor(t)}:\n{e}")
|
|
423
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
424
|
+
return False, None
|
|
425
|
+
# Extents only here (max_bytes=inf): the VRAM budget is judged by
|
|
426
|
+
# the upload on a cache MISS (texture3d / tensor_to_texture create),
|
|
427
|
+
# since it's measured against FREE memory and a cached volume's GPU
|
|
428
|
+
# allocation must not fail its next frame. That refusal surfaces
|
|
429
|
+
# as the "GPU upload failed" card below.
|
|
430
|
+
clamped_shape, problems = texture3d_fit(vol.shape, vol.element_size(),
|
|
431
|
+
max_bytes=float("inf"))
|
|
432
|
+
if any("GL_MAX_3D_TEXTURE_SIZE" not in p for p in problems):
|
|
433
|
+
_draw_voxel_error(draw_state, f"{_describe_tensor(t)} → volume "
|
|
434
|
+
f"{tuple(int(s) for s in vol.shape)}:\n"
|
|
435
|
+
+ "\n".join(problems))
|
|
436
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
437
|
+
return False, None
|
|
438
|
+
clamp_note = None
|
|
439
|
+
if problems:
|
|
440
|
+
# Display the leading max-size block of each over-limit axis.
|
|
441
|
+
d3, h3, w3 = clamped_shape
|
|
442
|
+
vol = vol[:d3, :h3, :w3].contiguous()
|
|
443
|
+
clamp_note = "clamped: " + "; ".join(problems)
|
|
444
|
+
version = vol_key + (mapping, clamped_shape)
|
|
445
|
+
try:
|
|
446
|
+
if vol.is_cuda:
|
|
447
|
+
from meltygui.model.cuda_texture_model import tensor_to_texture
|
|
448
|
+
tex = tensor_to_texture(gl_state, "volume_cuda", vol,
|
|
449
|
+
version=version)
|
|
450
|
+
if tex is None:
|
|
451
|
+
tex = gl_state.texture3d("volume", vol.cpu().numpy(), version=version)
|
|
452
|
+
gl_state.drop("volume_cuda")
|
|
453
|
+
else:
|
|
454
|
+
gl_state.drop("volume")
|
|
455
|
+
except Exception as e:
|
|
456
|
+
# The driver refused something the pre-flight didn't predict
|
|
457
|
+
# (out of memory, unsupported format...). Show it, don't crash
|
|
458
|
+
# the render loop; the deps still ensure we retry only when the
|
|
459
|
+
# source or mapping change.
|
|
460
|
+
_draw_voxel_error(draw_state, f"GPU upload failed for volume "
|
|
461
|
+
f"{tuple(int(s) for s in vol.shape)} "
|
|
462
|
+
f"({vol.dtype}):\n{e}")
|
|
463
|
+
gl_state.drop("volume"); gl_state.drop("volume_cuda")
|
|
464
|
+
return False, None
|
|
465
|
+
tex.source_shape = source_shape # tensor metadata on the buffer
|
|
466
|
+
tex.source_ndim = len(source_shape)
|
|
467
|
+
tex.clamp_note = clamp_note
|
|
468
|
+
tex.mapping = mapping
|
|
469
|
+
tex.dim_names = dim_names
|
|
470
|
+
tex._vol_key = vol_key # the gate above uses this
|
|
471
|
+
vol = None # don't hold the temp volume
|
|
472
|
+
|
|
473
|
+
# Edge labels - the mapped dim's name (+ neural-flow decoration) and its
|
|
474
|
+
# DISPLAYED size, recomputed per frame from the params.
|
|
475
|
+
if mapping is not None:
|
|
476
|
+
zd, yd, xd = mapping
|
|
477
|
+
n = len(source_shape)
|
|
478
|
+
chop_d = _resolve_dim(dim_names, nf_chop, n)
|
|
479
|
+
along_d = _resolve_dim(dim_names, nf_along, n)
|
|
480
|
+
chop_d = xd if chop_d is None else chop_d
|
|
481
|
+
along_d = zd if along_d is None else along_d
|
|
482
|
+
display = []
|
|
483
|
+
for axis, dim in (("x", xd), ("y", yd), ("z", zd)):
|
|
484
|
+
label = dim_names[dim] if dim < len(dim_names) else f"dim{dim}"
|
|
485
|
+
if nf_on and dim == chop_d:
|
|
486
|
+
label = f"{label} % {int(nf_chunk)}" # chopped into chunks
|
|
487
|
+
elif nf_on and dim == along_d:
|
|
488
|
+
chop_name = (dim_names[chop_d]
|
|
489
|
+
if chop_d < len(dim_names) else f"dim{chop_d}")
|
|
490
|
+
label = f"{label} · {chop_name}" # along the blocks
|
|
491
|
+
display.append((label, int(tex.shape[_AXIS_POS[axis]])))
|
|
492
|
+
axis_display = tuple(display)
|
|
493
|
+
else:
|
|
494
|
+
d3, h3, w3 = (int(s) for s in tex.shape)
|
|
495
|
+
axis_display = ((dim_names[2] if len(dim_names) > 2 else "x", w3),
|
|
496
|
+
(dim_names[1] if len(dim_names) > 1 else "y", h3),
|
|
497
|
+
(dim_names[0] if dim_names else "z", d3))
|
|
498
|
+
|
|
499
|
+
# ── slice sliders: >3-dim tensors get one slider per UNMAPPED dim (not
|
|
500
|
+
# displayed, not averaged, extent > 1) along the bottom of the view to
|
|
501
|
+
# choose which slice is pinned. GLTexture inputs arrive pre-sliced
|
|
502
|
+
# (mapping is None) - nothing to scrub. ─────────────────────────────
|
|
503
|
+
slider_dims = []
|
|
504
|
+
if mapping is not None and len(source_shape) > 3:
|
|
505
|
+
slider_dims = [d for d in range(len(source_shape))
|
|
506
|
+
if d not in mapping and d not in mean_dims
|
|
507
|
+
and source_shape[d] > 1]
|
|
508
|
+
|
|
509
|
+
# Size from the OWNING WINDOW, not this view's own draw() - a nested
|
|
510
|
+
# view's height derives from what it rendered last frame (self-referential),
|
|
511
|
+
# while the window's height is the user-dragged size. Reserve room for the
|
|
512
|
+
# header + a line below the image.
|
|
513
|
+
# The owning window IS this draw_state when draw_voxels is itself a window
|
|
514
|
+
# (mode=WINDOW / closable); only fall back to the enclosing window for the
|
|
515
|
+
# non-window child case. Using draw_state.parent_window for a closable voxel
|
|
516
|
+
# grabbed an ANCESTOR (e.g. live_view_forward) that doesn't move with the
|
|
517
|
+
# voxel window, so the controls panel - parented to `win` below - followed
|
|
518
|
+
# the ancestor and stayed put while the voxel window was dragged.
|
|
519
|
+
win = draw_state if draw_state.closable else (draw_state.parent_window or draw_state)
|
|
520
|
+
width, height = _view_size(draw_state)
|
|
521
|
+
if slider_dims:
|
|
522
|
+
# The sliders live INSIDE the view's box - give them their rows by
|
|
523
|
+
# shrinking the image, not by growing past the window.
|
|
524
|
+
height = max(100, height - int(imgui.get_frame_height_with_spacing())
|
|
525
|
+
* len(slider_dims))
|
|
526
|
+
|
|
527
|
+
# ── in-flight locate values: a locate_* write to a SLOW source (e.g. a
|
|
528
|
+
# `# [cam_brightness=...]` comment) is deferred during drags and lands
|
|
529
|
+
# multi-frame after; until then the injected kwarg is stale. Re-read any
|
|
530
|
+
# camera param with a value set through locate_* (which also clears the
|
|
531
|
+
# entry once the trip lands) so drags accumulate off the latest state.
|
|
532
|
+
# _sa_precise rides the same way: a low-precision source has a 4dp
|
|
533
|
+
# rounding, so locate_* serves the full-precision overlay over it. ──
|
|
534
|
+
_pending = getattr(draw_state, "_sa_pending", None) or {}
|
|
535
|
+
_precise = getattr(draw_state, "_sa_precise", None) or {}
|
|
536
|
+
_in_flight = _pending.keys() | _precise.keys()
|
|
537
|
+
if _in_flight:
|
|
538
|
+
def _fly(n, cur):
|
|
539
|
+
if n not in _in_flight:
|
|
540
|
+
return cur
|
|
541
|
+
v = getattr(draw_state, "locate_" + n)
|
|
542
|
+
return cur if v is None else v
|
|
543
|
+
tilt, spin, cam_zoom = _fly("tilt", tilt), _fly("spin", spin), _fly("cam_zoom", cam_zoom)
|
|
544
|
+
roll = _fly("roll", roll)
|
|
545
|
+
pan_x, pan_y, pan_z = _fly("pan_x", pan_x), _fly("pan_y", pan_y), _fly("pan_z", pan_z)
|
|
546
|
+
cam_brightness = _fly("cam_brightness", cam_brightness)
|
|
547
|
+
cam_contrast = _fly("cam_contrast", cam_contrast)
|
|
548
|
+
ortho = _fly("ortho", ortho)
|
|
549
|
+
# EVERY display-affecting panel param rides the same gate, not just
|
|
550
|
+
# the camera: a panel edit whose driving source is slow (an override
|
|
551
|
+
# comment's parse->copy->hotkey trip) otherwise renders the STALE
|
|
552
|
+
# injected kwarg until the trip lands - "changes not always
|
|
553
|
+
# reflected", especially visible on the cuda path where transfer params
|
|
554
|
+
# also key the mip/floor bakes.
|
|
555
|
+
density, threshold = _fly("density", density), _fly("threshold", threshold)
|
|
556
|
+
step_size, max_steps = _fly("step_size", step_size), _fly("max_steps", max_steps)
|
|
557
|
+
nearest, centered = _fly("nearest", nearest), _fly("centered", centered)
|
|
558
|
+
draw_plane = _fly("draw_plane", draw_plane)
|
|
559
|
+
shadow_opacity = _fly("shadow_opacity", shadow_opacity)
|
|
560
|
+
shadow_softness = _fly("shadow_softness", shadow_softness)
|
|
561
|
+
draw_shading = _fly("draw_shading", draw_shading)
|
|
562
|
+
self_shading = _fly("self_shading", self_shading)
|
|
563
|
+
light_pos, light_tint = _fly("light_pos", light_pos), _fly("light_tint", light_tint)
|
|
564
|
+
light_brightness = _fly("light_brightness", light_brightness)
|
|
565
|
+
ambient_light = _fly("ambient_light", ambient_light)
|
|
566
|
+
shading_strength = _fly("shading_strength", shading_strength)
|
|
567
|
+
# # (Data-shaping params - slices/dims/nf/scale/normalize - are consumed
|
|
568
|
+
# # ABOVE this gate and they can't be re-read here, but the pump below
|
|
569
|
+
# # re-renders until the trip lands and the new tex_key rebuilds.)
|
|
570
|
+
# # And keep re-rendering until the slow write LANDS (locate_* clears
|
|
571
|
+
# # the entry): the landing itself doesn't invalidate this frame, so
|
|
572
|
+
# # without the pump the final value never rebuilds.
|
|
573
|
+
# draw_state.invalidate()
|
|
574
|
+
# request_render()
|
|
575
|
+
|
|
576
|
+
# ── gestures → draw_state params (auto-state: the caller diverges the
|
|
577
|
+
# param so it persists; events are hover-routed wrapper kwargs) ──────
|
|
578
|
+
if middle_mouse_drag is not None:
|
|
579
|
+
# Ortable chirality, latched per GESTURE: upside down (cos(tilt)<0,
|
|
580
|
+
# world-up pointing down the screen) a rightward move must spin the
|
|
581
|
+
# other way to keep tracking the cursor. Latching at drag start keeps
|
|
582
|
+
# the direction stable when a drag tilts across the pole mid-gesture;
|
|
583
|
+
# the latch clears on release so the next drag re-reads orientation.
|
|
584
|
+
spin_sign = getattr(draw_state, "_orbit_spin_sign", None)
|
|
585
|
+
if spin_sign is None:
|
|
586
|
+
spin_sign = -1.0 if math.cos(tilt) < 0.0 else 1.0
|
|
587
|
+
draw_state._orbit_spin_sign = spin_sign
|
|
588
|
+
if middle_mouse_drag.shift:
|
|
589
|
+
# Blender-style shift-d = pan: move the orbit target so the
|
|
590
|
+
# content tracks the cursor 1:1 at the target plane (world units
|
|
591
|
+
# per pixel at current cam_zoom, focal 1.7 - matches the ray gen).
|
|
592
|
+
wpp = 2.0 * cam_zoom / (1.7 * height)
|
|
593
|
+
st, ct = math.sin(tilt), math.cos(tilt)
|
|
594
|
+
cs, ss = math.cos(spin), math.sin(spin)
|
|
595
|
+
dx, dy = middle_mouse_drag.dx, middle_mouse_drag.dy
|
|
596
|
+
pan_x += (ss * dx - cs * st * dy) * wpp
|
|
597
|
+
pan_y += (-cs * dx - ss * st * dy) * wpp
|
|
598
|
+
pan_z += ct * dy * wpp
|
|
599
|
+
draw_state.locate_pan_x = pan_x
|
|
600
|
+
draw_state.locate_pan_y = pan_y
|
|
601
|
+
draw_state.locate_pan_z = pan_z
|
|
602
|
+
elif middle_mouse_drag.ctrl:
|
|
603
|
+
# the old viewer's ctrl-drag: vertical = dolly zoom, horizontal
|
|
604
|
+
# still orbits.
|
|
605
|
+
cam_zoom = min(137.6, max(0.0, cam_zoom * math.exp(0.005 * middle_mouse_drag.dy)))
|
|
606
|
+
spin -= middle_mouse_drag.dx * 0.008 * spin_sign
|
|
607
|
+
draw_state.locate_cam_zoom = cam_zoom
|
|
608
|
+
draw_state.locate_spin = spin
|
|
609
|
+
elif Toggles.Voxels.mouse_navigation == "trackball":
|
|
610
|
+
# Mouse drag as a rotation vector in VIEW space (dy pitches about
|
|
611
|
+
# screen-right, dx yaws about screen-up), through the same geometry
|
|
612
|
+
# as the 3D mouse's trackball - roll is the third angle.
|
|
613
|
+
tilt, spin, roll, _, _ = apply_space_mouse(
|
|
614
|
+
(0.0, 0.0, 0.0, middle_mouse_drag.dy * 0.008,
|
|
615
|
+
middle_mouse_drag.dx * 0.008, 0.0),
|
|
616
|
+
tilt, spin, roll, cam_zoom, (pan_x, pan_y, pan_z),
|
|
617
|
+
navigation="trackball", orbit_sensitivity=1.0,
|
|
618
|
+
pan_sensitivity=0.0, zoom_sensitivity=0.0)
|
|
619
|
+
draw_state.locate_spin = spin
|
|
620
|
+
draw_state.locate_tilt = tilt
|
|
621
|
+
draw_state.locate_roll = roll
|
|
622
|
+
else:
|
|
623
|
+
spin -= middle_mouse_drag.dx * 0.008 * spin_sign
|
|
624
|
+
# Tilt is UNRESTRICTED - orbit straight over the poles and keep
|
|
625
|
+
# going. remainder() re-wraps into [-pi, pi] (same orientation,
|
|
626
|
+
# cos/sin-continuous) so the stored angle never runs away.
|
|
627
|
+
tilt = math.remainder(tilt + middle_mouse_drag.dy * 0.008, math.tau)
|
|
628
|
+
draw_state.locate_spin = spin
|
|
629
|
+
draw_state.locate_tilt = tilt
|
|
630
|
+
else:
|
|
631
|
+
draw_state._orbit_spin_sign = None
|
|
632
|
+
if double_right_mouse_drag is not None:
|
|
633
|
+
# the old viewer's shading drag: now on a DOUBLE right-drag (the 2nd
|
|
634
|
+
# press of a double right-click, held and dragged): horizontal =
|
|
635
|
+
# brightness, vertical = contrast (up to increase). The plain right-
|
|
636
|
+
# click stays reserved for the context menu.
|
|
637
|
+
cam_brightness = min(4.0, max(0.0, cam_brightness + double_right_mouse_drag.dx * 0.01))
|
|
638
|
+
cam_contrast = min(5.0, max(0.01, cam_contrast - double_right_mouse_drag.dy * 0.008))
|
|
639
|
+
draw_state.locate_cam_brightness = cam_brightness
|
|
640
|
+
draw_state.locate_cam_contrast = cam_contrast
|
|
641
|
+
if scroll_y_changed is not None:
|
|
642
|
+
cam_zoom = min(135.5, max(0.0, cam_zoom * math.exp(-0.23 * scroll_y_changed.value)))
|
|
643
|
+
draw_state.locate_cam_zoom = cam_zoom
|
|
644
|
+
if space_mouse_changed is not None and space_mouse_changed.axes:
|
|
645
|
+
# 3D mouse (events/space_mouse.py → InputHandler.feed()): the six
|
|
646
|
+
# axes integrated over the frame. The mapping is voxel_camera's -
|
|
647
|
+
# turntable writes tilt / spin straight from the puck's pitch / yaw,
|
|
648
|
+
# trackball rotates the basis in view space and decomposes it back
|
|
649
|
+
# (roll is the third angle); pan tracks the screen plane scaled by
|
|
650
|
+
# the camera distance, push / pull is an e-fold dolly. Sensitivities
|
|
651
|
+
# and the mode are Toggles.SpaceMouse. Axes arrive in OBJECT terms
|
|
652
|
+
# (Blender's handiness folded in by space_mouse.normalize()) so this
|
|
653
|
+
# is the same math as the mouse.
|
|
654
|
+
tilt, spin, roll, cam_zoom, (pan_x, pan_y, pan_z) = apply_space_mouse(
|
|
655
|
+
space_mouse_changed.axes, tilt, spin, roll, cam_zoom, (pan_x, pan_y, pan_z),
|
|
656
|
+
navigation=Toggles.SpaceMouse.navigation,
|
|
657
|
+
orbit_sensitivity=float(Toggles.SpaceMouse.orbit_sensitivity),
|
|
658
|
+
pan_sensitivity=float(Toggles.SpaceMouse.pan_sensitivity),
|
|
659
|
+
zoom_sensitivity=float(Toggles.SpaceMouse.zoom_sensitivity),
|
|
660
|
+
pivot=Toggles.SpaceMouse.pivot)
|
|
661
|
+
draw_state.locate_tilt = tilt
|
|
662
|
+
draw_state.locate_spin = spin
|
|
663
|
+
draw_state.locate_roll = roll
|
|
664
|
+
draw_state.locate_cam_zoom = cam_zoom
|
|
665
|
+
draw_state.locate_pan_x = pan_x
|
|
666
|
+
draw_state.locate_pan_y = pan_y
|
|
667
|
+
draw_state.locate_pan_z = pan_z
|
|
668
|
+
|
|
669
|
+
# ── Blender-style numpad views (hover-routed key events): 7/1/3 = top/
|
|
670
|
+
# front/right, ctrl = the opposite side, 5 = ortho toggle, / (either
|
|
671
|
+
# slash, or numpad . like the old viewer) = recenter the pan on the
|
|
672
|
+
# origin. A focused text editor owns the keyboard, so keys are ignored
|
|
673
|
+
# while one is active. ────────────────────────────────────────────────
|
|
674
|
+
if keyboard_available:
|
|
675
|
+
if kp_7_pressed is not None:
|
|
676
|
+
spin, tilt = -(math.pi / 2), (-(math.pi / 2) if kp_7_pressed.ctrl else (math.pi / 2))
|
|
677
|
+
draw_state.locate_spin = spin
|
|
678
|
+
draw_state.locate_tilt = tilt
|
|
679
|
+
if kp_1_pressed is not None:
|
|
680
|
+
spin, tilt = ((math.pi / 2) if kp_1_pressed.ctrl else -(math.pi / 2)), 0.0
|
|
681
|
+
draw_state.locate_spin = spin
|
|
682
|
+
draw_state.locate_tilt = tilt
|
|
683
|
+
if kp_3_pressed is not None:
|
|
684
|
+
spin, tilt = (math.pi if kp_3_pressed.ctrl else 0.0), 0.0
|
|
685
|
+
draw_state.locate_spin = spin
|
|
686
|
+
draw_state.locate_tilt = tilt
|
|
687
|
+
if kp_5_pressed is not None:
|
|
688
|
+
ortho = not ortho
|
|
689
|
+
draw_state.locate_ortho = ortho
|
|
690
|
+
if (slash_pressed is not None or kp_divide_pressed is not None
|
|
691
|
+
or kp_decimal_pressed is not None):
|
|
692
|
+
pan_x = pan_y = pan_z = 0.0
|
|
693
|
+
draw_state.locate_pan_x = 0.0
|
|
694
|
+
draw_state.locate_pan_y = 0.0
|
|
695
|
+
draw_state.locate_pan_z = 0.0
|
|
696
|
+
# ...and level the horizon (a trackball session's habit).
|
|
697
|
+
roll = 0.0
|
|
698
|
+
draw_state.locate_roll = 0.0
|
|
699
|
+
|
|
700
|
+
# ── plane side latch: when the view is upside-down the target plane
|
|
701
|
+
# belongs on the box's OTHER face (the floor light stays fixed in world
|
|
702
|
+
# space; only the catcher's marches see a mirrored box so the flipped
|
|
703
|
+
# floor still catches a shadow). Same idea as _orbit_spin_sign: the side
|
|
704
|
+
# only re-reads orientation while NO drag is active - mid-drag the floor
|
|
705
|
+
# holds put, and the flip lands when you let go.
|
|
706
|
+
if middle_mouse_drag is None:
|
|
707
|
+
draw_state._plane_side = -1.0 if math.cos(tilt) < 0.0 else 1.0
|
|
708
|
+
plane_side = getattr(draw_state, "_plane_side", None) or (
|
|
709
|
+
-1.0 if math.cos(tilt) < 0.0 else 1.0)
|
|
710
|
+
|
|
711
|
+
# Filtering is sampler state on the texture, view-owned, applied per frame.
|
|
712
|
+
filt = gl.GL_NEAREST if nearest else gl.GL_LINEAR
|
|
713
|
+
if isinstance(tex, GLTexture):
|
|
714
|
+
gl.glBindTexture(tex.target, tex.texture_id)
|
|
715
|
+
gl.glTexParameteri(tex.target, gl.GL_TEXTURE_MIN_FILTER, filt)
|
|
716
|
+
gl.glTexParameteri(tex.target, gl.GL_TEXTURE_MAG_FILTER, filt)
|
|
717
|
+
gl.glBindTexture(tex.target, 0)
|
|
718
|
+
|
|
719
|
+
volume_scale = _volume_scale(tex.shape)
|
|
720
|
+
|
|
721
|
+
lut_tex = luts.texture(lut)
|
|
722
|
+
|
|
723
|
+
# ── axis coordinate positions: visible silhouette spans via the Python
|
|
724
|
+
# mirror of the shader camera, computed BEFORE the GL pass - the label
|
|
725
|
+
# billboards render INTO the voxel FBO with the volume's own camera ────
|
|
726
|
+
axis_edges = None
|
|
727
|
+
if axis_display:
|
|
728
|
+
axis_edges = _axis_edges(tilt, spin, cam_zoom,
|
|
729
|
+
width / height, width, height,
|
|
730
|
+
scale=volume_scale,
|
|
731
|
+
pan=(pan_x, pan_y, pan_z),
|
|
732
|
+
ortho=ortho, roll=roll)
|
|
733
|
+
|
|
734
|
+
# A dragged step size can cross zero (the number token has no floor). A
|
|
735
|
+
# non-positive step walks the GL path BACKWARDS out of the box on its
|
|
736
|
+
# first step, so every slice shows only its entry voxel - a flat plane
|
|
737
|
+
# cut along the data's edges (the cuda path only uses it as a sampling
|
|
738
|
+
# stride and shrugged it off, the "GL path looks broken" ticket on
|
|
739
|
+
# 09-08). Floor it here, once, for both paths.
|
|
740
|
+
step_size = max(float(step_size), 1e-5)
|
|
741
|
+
|
|
742
|
+
# ── GL pass: every resource tracked + lifecycle-managed by gl_state ──
|
|
743
|
+
fb = gl_state.fbo("target", width, height)
|
|
744
|
+
depth_was_on = gl.glIsEnabled(gl.GL_DEPTH_TEST)
|
|
745
|
+
with fb:
|
|
746
|
+
gl.glDisable(gl.GL_DEPTH_TEST)
|
|
747
|
+
gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
748
|
+
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
|
749
|
+
# Labels FIRST, so the volume pass composites OVER them - the floor
|
|
750
|
+
# shadow (and the floor itself) darkens the labels beneath it
|
|
751
|
+
# instead of the labels floating on top of the shadow.
|
|
752
|
+
if axis_edges and (name_size > 0 or num_size > 0):
|
|
753
|
+
# Labels as in-scene textured quads. A bake/render hiccup should
|
|
754
|
+
# not take down the view (or trigger the hotswap auto-revert) -
|
|
755
|
+
# log it and keep rendering the volume.
|
|
756
|
+
try:
|
|
757
|
+
specs = _billboard_specs(axis_edges, axis_display,
|
|
758
|
+
volume_scale, name_size, name_padding,
|
|
759
|
+
name_opacity, num_size, num_padding,
|
|
760
|
+
num_opacity, num_spacing, num_angle)
|
|
761
|
+
cam = {"tilt": tilt, "spin": spin, "roll": roll, "zoom": cam_zoom,
|
|
762
|
+
"pan_x": pan_x, "pan_y": pan_y, "pan_z": pan_z,
|
|
763
|
+
"ortho": ortho, "aspect": width / height}
|
|
764
|
+
_render_label_billboards(
|
|
765
|
+
gl_state, specs, cam, height,
|
|
766
|
+
font=font_manager.get(Font.JETBRAINS_MONO_30) if font_manager else None)
|
|
767
|
+
except Exception as e:
|
|
768
|
+
if not voxel_state.label_warned:
|
|
769
|
+
voxel_state.label_warned = True
|
|
770
|
+
import traceback
|
|
771
|
+
print(f"label billboards disabled: {e}")
|
|
772
|
+
traceback.print_exc()
|
|
773
|
+
# The volume shader outputs PREMULTIPLIED alpha (the shader
|
|
774
|
+
# composites with (1-a) weights), so it layers over the labels with
|
|
775
|
+
# ONE / ONE_MINUS_SRC_ALPHA - over label-free (transparent) pixels
|
|
776
|
+
# this is bit-identical to the old unblended write.
|
|
777
|
+
_blend_was = bool(gl.glIsEnabled(gl.GL_BLEND))
|
|
778
|
+
gl.glEnable(gl.GL_BLEND)
|
|
779
|
+
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
|
780
|
+
gl.glBlendFuncSeparate(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA,
|
|
781
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
782
|
+
# int() so a UI-dragged float never flips the uniform's inferred
|
|
783
|
+
# GLSL type (the loop bound must be an int).
|
|
784
|
+
if isinstance(tex, CudaVolumeView):
|
|
785
|
+
# The CUDA kernel already produced the premultiplied image (on
|
|
786
|
+
# the tensor's GPU, hopped to a display-GPU RGBA16F texture);
|
|
787
|
+
# blit it into the FBO under the same blend state so labels,
|
|
788
|
+
# outline and the rest of the view are untouched.
|
|
789
|
+
from meltygui.view import voxel_cuda_view
|
|
790
|
+
img_tex = _cuda_render(
|
|
791
|
+
gl_state, tex, width, height, voxel_state, lut=lut, tilt=tilt, spin=spin,
|
|
792
|
+
lut_texture=lut_tex,
|
|
793
|
+
roll=roll, zoom=cam_zoom, pan=(pan_x, pan_y, pan_z), ortho=bool(ortho),
|
|
794
|
+
volume_scale=volume_scale, step_size=float(step_size),
|
|
795
|
+
max_steps=int(max_steps), density=float(density),
|
|
796
|
+
threshold=float(threshold), brightness=float(cam_brightness),
|
|
797
|
+
contrast=float(cam_contrast), gamma=float(Toggles.Voxels.gamma),
|
|
798
|
+
centered=bool(centered),
|
|
799
|
+
shade=voxel_cuda_view.shade_params(
|
|
800
|
+
draw_plane=bool(draw_plane), shadow_opacity=float(shadow_opacity),
|
|
801
|
+
shadow_softness=float(shadow_softness),
|
|
802
|
+
shadow_tint=tuple(float(c) for c in Toggles.Voxels.floor_shadow_color)[:3],
|
|
803
|
+
plane_side=float(plane_side), draw_shading=bool(draw_shading),
|
|
804
|
+
self_shading=bool(self_shading),
|
|
805
|
+
light_pos=tuple(float(c) for c in light_pos),
|
|
806
|
+
light_tint=tuple(float(c) for c in light_tint),
|
|
807
|
+
light_brightness=float(light_brightness),
|
|
808
|
+
ambient_light=float(ambient_light),
|
|
809
|
+
shading_strength=float(shading_strength)))
|
|
810
|
+
if img_tex is not None:
|
|
811
|
+
image_blit_pass(gl_state, image=img_tex)
|
|
812
|
+
else:
|
|
813
|
+
voxel_pass(gl_state, volume=tex, volume_lin=tex, lut=lut_tex,
|
|
814
|
+
aspect=width / height,
|
|
815
|
+
volume_scale=volume_scale, step_size=step_size,
|
|
816
|
+
max_steps=int(max_steps), density=density,
|
|
817
|
+
threshold=threshold, tilt=tilt, spin=spin, roll=roll, zoom=cam_zoom,
|
|
818
|
+
pan_x=pan_x, pan_y=pan_y, pan_z=pan_z, ortho=ortho,
|
|
819
|
+
brightness=cam_brightness, contrast=cam_contrast,
|
|
820
|
+
gamma=float(Toggles.Voxels.gamma), centered=centered,
|
|
821
|
+
draw_plane=bool(draw_plane),
|
|
822
|
+
shadow_opacity=float(shadow_opacity),
|
|
823
|
+
shadow_softness=float(shadow_softness),
|
|
824
|
+
# The floor shadow's own colour (neutral grey), not
|
|
825
|
+
# the UI's blue-black compositor Toggles.floor_color.
|
|
826
|
+
shadow_tint=tuple(float(c) for c in Toggles.Voxels.floor_shadow_color)[:3],
|
|
827
|
+
draw_shading=bool(draw_shading),
|
|
828
|
+
self_shading=bool(self_shading),
|
|
829
|
+
plane_side=float(plane_side),
|
|
830
|
+
light_pos=tuple(float(c) for c in light_pos),
|
|
831
|
+
light_tint=tuple(float(c) for c in light_tint),
|
|
832
|
+
light_brightness=float(light_brightness),
|
|
833
|
+
ambient_light=float(ambient_light),
|
|
834
|
+
shading_strength=float(shading_strength))
|
|
835
|
+
if not _blend_was:
|
|
836
|
+
gl.glDisable(gl.GL_BLEND)
|
|
837
|
+
if depth_was_on:
|
|
838
|
+
gl.glEnable(gl.GL_DEPTH_TEST)
|
|
839
|
+
|
|
840
|
+
img_pos = imgui.get_cursor_screen_pos()
|
|
841
|
+
imgui.image(fb.texture_id, width, height, uv0=(0, 1), uv1=(1, 0))
|
|
842
|
+
clamp_note = getattr(tex, "clamp_note", None)
|
|
843
|
+
if clamp_note:
|
|
844
|
+
# The volume on screen is a PREFIX of the tensor; say so on the image
|
|
845
|
+
# (top-left, wrapped to the image width) rather than silently
|
|
846
|
+
# showing a truncated tensor.
|
|
847
|
+
_draw_image_notice(img_pos, width, clamp_note)
|
|
848
|
+
if not isinstance(src, GLTexture):
|
|
849
|
+
_draw_tensor_meta(img_pos, height, t)
|
|
850
|
+
|
|
851
|
+
# ── the outline stays 2-D imgui (crisp 1px outline over the volume) ────
|
|
852
|
+
if axis_edges:
|
|
853
|
+
_draw_axis_lines(imgui.get_window_draw_list(), img_pos, axis_edges)
|
|
854
|
+
|
|
855
|
+
# ── the slice sliders, one slider per unmapped dim under the volume. An
|
|
856
|
+
# edit writes the full-length slices tuple to draw_state (auto-state:
|
|
857
|
+
# it diverges the param, persists, and resets slice_volume's version so
|
|
858
|
+
# the volume re-slices + re-uploads on the next frame). ──────────────
|
|
859
|
+
_draw_slice_sliders(draw_state, slider_dims, dim_names, slices, source_shape, width)
|
|
860
|
+
|
|
861
|
+
# ── ALL controls live in a satellite panel opening to the RIGHT of
|
|
862
|
+
# the window: the renderer's full params, rendered automatically -
|
|
863
|
+
# draw_state.locate_params is a live dict over this signature, each row
|
|
864
|
+
# reads its framework-resolved value and an edit goes through
|
|
865
|
+
# set_anywhere (draw_state by default; a higher-pri source like an
|
|
866
|
+
# annotation comment claims the write when it drives the param).
|
|
867
|
+
# POPOVER window_pos is relative to the CURSOR at the call,
|
|
868
|
+
# so anchor at the window's right edge - the panel rides along if the
|
|
869
|
+
# window is dragged. Double-click the volume to show/hide; `closed` is
|
|
870
|
+
# only PASSED on init/toggle so the window's own X button works - the
|
|
871
|
+
# framework owns the state between toggles and we mirror it back (a
|
|
872
|
+
# forced closed= every call reopened the panel on the next pre-render,
|
|
873
|
+
# which is why the X appeared dead). ─────────────────────────────────
|
|
874
|
+
init = voxel_state.params_panel is None
|
|
875
|
+
if init:
|
|
876
|
+
# Adopt the same view's saved panel state during the migration.
|
|
877
|
+
voxel_state.params_panel = draw_state.misc.pop("params_panel", False)
|
|
878
|
+
toggled = False
|
|
879
|
+
if left_mouse_double_clicked is not None:
|
|
880
|
+
voxel_state.params_panel = not voxel_state.params_panel
|
|
881
|
+
toggled = True
|
|
882
|
+
draw_state.invalidate()
|
|
883
|
+
request_render()
|
|
884
|
+
panel_open = bool(voxel_state.params_panel)
|
|
885
|
+
# Anchor the panel at the window's RIGHT edge (+12px gap). Set every frame
|
|
886
|
+
# so left_offset/top_offset track the right edge and the panel rides along
|
|
887
|
+
# when the window is dragged; the panel's own drag accumulates into
|
|
888
|
+
# window_pos on top of that, so it stays draggable.
|
|
889
|
+
# Save/restore the flow cursor around the panel jump; nested in a scroll
|
|
890
|
+
# view, `win` is the ENCLOSING window, so this teleports the cursor far
|
|
891
|
+
# from this view's box - left unrestored, poisons the parent rect and
|
|
892
|
+
# the group measure (views popped in with a huge height, then the -30
|
|
893
|
+
# self-reference shrank them back 29px a frame).
|
|
894
|
+
panel_kwargs = {"closed": not panel_open} if (init or toggled) else {}
|
|
895
|
+
if (not middle_mouse_drag and not double_right_mouse_drag and scroll_y_changed is None
|
|
896
|
+
and space_mouse_changed is None):
|
|
897
|
+
_flow_cursor = imgui.get_cursor_screen_pos()
|
|
898
|
+
# Anchor y: the enclosing window's top for a window voxel, this ROW's
|
|
899
|
+
# top for a nested one. The panel call emits an inline item at the
|
|
900
|
+
# cursor, and that item is committed into THIS view's group - an
|
|
901
|
+
# anchor at win.abs_top made a nested view's rect span from the row to
|
|
902
|
+
# the window top, so committed heights scaled with scroll distance
|
|
903
|
+
# (the scrollbar jitter as rows crossed the viewport).
|
|
904
|
+
_anchor_y = win.abs_top if draw_state.closable else draw_state.abs_top
|
|
905
|
+
imgui.set_cursor_screen_pos((win.abs_left + (win.width or width) + 12, _anchor_y))
|
|
906
|
+
|
|
907
|
+
changed, _, panel_ds = draw_any(draw_state.locate_params,
|
|
908
|
+
name=f"controls##{draw_state.name}",
|
|
909
|
+
is_tree=False,
|
|
910
|
+
use_cache=False,
|
|
911
|
+
show_name=False,
|
|
912
|
+
layer_offset=7,
|
|
913
|
+
tint=draw_state._kwargs.get("tint", None),
|
|
914
|
+
swoosh_mode=SwooshMode.LINE,
|
|
915
|
+
mode=Modes.WINDOW_PARAMS, show_tint=False,
|
|
916
|
+
parent_window=win, auto_resize=True,
|
|
917
|
+
shadow=False, return_extras=True,
|
|
918
|
+
initial={"expanded": True},
|
|
919
|
+
**panel_kwargs)
|
|
920
|
+
imgui.set_cursor_screen_pos(_flow_cursor)
|
|
921
|
+
if panel_ds is not None:
|
|
922
|
+
voxel_state.params_panel = not panel_ds.closed
|
|
923
|
+
# The panel is cached and must NOT invalidate per drag frame - it
|
|
924
|
+
# rides its blit while a camera gesture writes the params, then
|
|
925
|
+
# catches up ONCE at the gesture edge.
|
|
926
|
+
if not panel_ds.closed:
|
|
927
|
+
if (not pointer_buttons_down and scroll_y_changed is None
|
|
928
|
+
and space_mouse_changed is None) and changed:
|
|
929
|
+
panel_ds.invalidate_up()
|
|
930
|
+
|
|
931
|
+
# ── status bar error surfacing only ────────────────────────────────────
|
|
932
|
+
if voxel_pass.last_error:
|
|
933
|
+
# imgui.set_cursor_screen_pos((draw_state.abs_left, draw_state.abs_top))
|
|
934
|
+
imgui.text_colored(voxel_pass.last_error.splitlines()[0], 1.0, 0.45, 0.40, 1.0)
|
|
935
|
+
if isinstance(tex, CudaVolumeView) and voxel_state.cuda_error:
|
|
936
|
+
imgui.text_colored(voxel_state.cuda_error.splitlines()[0], 1.0, 0.45, 0.40, 1.0)
|
|
937
|
+
|
|
938
|
+
if changed:
|
|
939
|
+
# UP, not self: this view is often nested (a live-value window,
|
|
940
|
+
# a collection row) and its pixels are baked into ancestor blit
|
|
941
|
+
# tiles - a self-only invalidate left the ancestor serving the
|
|
942
|
+
# stale image, so panel edits "didn't take" until something else
|
|
943
|
+
# repushed the ancestor.
|
|
944
|
+
draw_state.invalidate_up()
|
|
945
|
+
request_render()
|
|
946
|
+
return changed, input_value
|
|
947
|
+
|
|
948
|
+
return False, input_value
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
LABEL_VERT = """
|
|
952
|
+
#version 330 core
|
|
953
|
+
uniform float tilt, spin, roll, zoom, aspect;
|
|
954
|
+
uniform bool ortho;
|
|
955
|
+
uniform vec3 pan;
|
|
956
|
+
layout(location = 0) in vec3 a_anchor; // world point ON the edge
|
|
957
|
+
layout(location = 1) in vec3 a_u; // baseline dir (flipped for reading)
|
|
958
|
+
layout(location = 2) in vec3 a_v; // text-up dir
|
|
959
|
+
layout(location = 3) in vec3 a_out; // unflipped outward dir (placement)
|
|
960
|
+
layout(location = 4) in vec4 a_metrics; // half_w, half_h, offs (NDC), alpha
|
|
961
|
+
layout(location = 5) in vec4 a_uvrect; // u0, v0(bottom), u1, v1(top)
|
|
962
|
+
out vec2 uv;
|
|
963
|
+
out float v_alpha;
|
|
964
|
+
void main() {
|
|
965
|
+
// two-triangle quad from gl_VertexID: corners in {-1,+1}²
|
|
966
|
+
int id = gl_VertexID;
|
|
967
|
+
vec2 q = vec2((id == 1 || id == 2 || id == 4) ? 1.0 : -1.0,
|
|
968
|
+
(id == 2 || id == 4 || id == 5) ? 1.0 : -1.0);
|
|
969
|
+
uv = vec2(mix(a_uvrect.x, a_uvrect.z, q.x * 0.5 + 0.5),
|
|
970
|
+
mix(a_uvrect.y, a_uvrect.w, q.y * 0.5 + 0.5));
|
|
971
|
+
v_alpha = a_metrics.w;
|
|
972
|
+
float ct = cos(tilt);
|
|
973
|
+
vec3 fwd = -vec3(cos(spin) * ct, sin(spin) * ct, sin(tilt));
|
|
974
|
+
vec3 right0 = vec3(-sin(spin), cos(spin), 0.0);
|
|
975
|
+
// roll turns right toward up about the view axis (0 = level horizon,
|
|
976
|
+
// the turntable); the 3D mouse's trackball mode is what writes it.
|
|
977
|
+
vec3 right = right0 * cos(roll) + cross(right0, fwd) * sin(roll);
|
|
978
|
+
vec3 up = cross(right, fwd);
|
|
979
|
+
vec3 eye = pan - fwd * zoom;
|
|
980
|
+
// Screen-constant sizing: metrics arrive in NDC units. The world length
|
|
981
|
+
// that projects to one NDC unit at the ANCHOR's depth is depth/1.7
|
|
982
|
+
// (zoom/1.7 in ortho), so the label keeps its pixel size at any zoom
|
|
983
|
+
// while still anchoring to and foreshortening with the scene.
|
|
984
|
+
float ws = (ortho ? zoom : max(0.05, dot(a_anchor - eye, fwd))) / 1.7;
|
|
985
|
+
vec3 world = a_anchor + (a_out * a_metrics.z
|
|
986
|
+
+ a_u * (a_metrics.x * q.x) + a_v * (a_metrics.y * q.y)) * ws;
|
|
987
|
+
vec3 d = world - eye;
|
|
988
|
+
// The voxel ray gen, inverted (same math as _axis_edges): perspective
|
|
989
|
+
// keeps the depth in w for the divide, ortho is a plain scale.
|
|
990
|
+
if (ortho) {
|
|
991
|
+
float s = zoom / 1.7;
|
|
992
|
+
gl_Position = vec4(dot(d, right) / (s * aspect), dot(d, up) / s, 0.0, 1.0);
|
|
993
|
+
} else {
|
|
994
|
+
gl_Position = vec4(1.7 * dot(d, right) / aspect, 1.7 * dot(d, up),
|
|
995
|
+
0.0, dot(d, fwd));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
"""
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
LABEL_FRAG = """
|
|
1002
|
+
#version 330 core
|
|
1003
|
+
uniform sampler2D label;
|
|
1004
|
+
in vec2 uv;
|
|
1005
|
+
in float v_alpha;
|
|
1006
|
+
out vec4 FragColor;
|
|
1007
|
+
void main() {
|
|
1008
|
+
vec4 t = texture(label, uv);
|
|
1009
|
+
FragColor = vec4(t.rgb, t.a * v_alpha);
|
|
1010
|
+
}
|
|
1011
|
+
"""
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
_LABEL_UNIFORMS = ("tilt", "spin", "roll", "zoom", "aspect", "ortho", "pan", "label")
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
_LABEL_FLOATS = 20 # 4×vec3 + 2×vec4 per instance
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
_AXIS_NEAR = 0.05
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
_EDGE_SHORTEN_PX = 14.0
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _label_program(gl_state):
|
|
1027
|
+
"""The instanced label program + its uniform-location map, compiled once
|
|
1028
|
+
per GLState (re-created when the GLSL source changes, e.g. on hotswap)."""
|
|
1029
|
+
|
|
1030
|
+
def create():
|
|
1031
|
+
def compile_one(kind, source):
|
|
1032
|
+
s = gl.glCreateShader(kind)
|
|
1033
|
+
gl.glShaderSource(s, source)
|
|
1034
|
+
gl.glCompileShader(s)
|
|
1035
|
+
if gl.glGetShaderiv(s, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
|
|
1036
|
+
raise RuntimeError(gl.glGetShaderInfoLog(s).decode(errors="replace"))
|
|
1037
|
+
return s
|
|
1038
|
+
|
|
1039
|
+
vs = compile_one(gl.GL_VERTEX_SHADER, LABEL_VERT)
|
|
1040
|
+
fs = compile_one(gl.GL_FRAGMENT_SHADER, LABEL_FRAG)
|
|
1041
|
+
prog = gl.glCreateProgram()
|
|
1042
|
+
gl.glAttachShader(prog, vs)
|
|
1043
|
+
gl.glAttachShader(prog, fs)
|
|
1044
|
+
gl.glLinkProgram(prog)
|
|
1045
|
+
gl.glDeleteShader(vs)
|
|
1046
|
+
gl.glDeleteShader(fs)
|
|
1047
|
+
if gl.glGetProgramiv(prog, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
|
1048
|
+
raise RuntimeError(gl.glGetProgramInfoLog(prog).decode(errors="replace"))
|
|
1049
|
+
loc = {n: gl.glGetUniformLocation(prog, n) for n in _LABEL_UNIFORMS}
|
|
1050
|
+
return prog, loc
|
|
1051
|
+
|
|
1052
|
+
def delete(value):
|
|
1053
|
+
gl.glDeleteProgram(value[0])
|
|
1054
|
+
|
|
1055
|
+
return gl_state.get("label_prog", create, delete,
|
|
1056
|
+
deps=(hash(LABEL_VERT), hash(LABEL_FRAG)))
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _label_vao(gl_state):
|
|
1060
|
+
"""(vao, vbo): one interleaved per-instance buffer (divisor 1 on every
|
|
1061
|
+
attribute — the quad corners come from gl_VertexID, no vertex attribs)."""
|
|
1062
|
+
|
|
1063
|
+
def create():
|
|
1064
|
+
vao = gl.glGenVertexArrays(1)
|
|
1065
|
+
vbo = gl.glGenBuffers(1)
|
|
1066
|
+
gl.glBindVertexArray(vao)
|
|
1067
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
|
|
1068
|
+
stride = _LABEL_FLOATS * 4
|
|
1069
|
+
offset = 0
|
|
1070
|
+
for slot, n in ((0, 3), (1, 3), (2, 3), (3, 3), (4, 4), (5, 4)):
|
|
1071
|
+
gl.glEnableVertexAttribArray(slot)
|
|
1072
|
+
gl.glVertexAttribPointer(slot, n, gl.GL_FLOAT, gl.GL_FALSE, stride,
|
|
1073
|
+
ctypes.c_void_p(offset))
|
|
1074
|
+
gl.glVertexAttribDivisor(slot, 1)
|
|
1075
|
+
offset += n * 4
|
|
1076
|
+
gl.glBindVertexArray(0)
|
|
1077
|
+
return vao, vbo
|
|
1078
|
+
|
|
1079
|
+
def delete(value):
|
|
1080
|
+
vao, vbo = value
|
|
1081
|
+
gl.glDeleteBuffers(1, [vbo])
|
|
1082
|
+
gl.glDeleteVertexArrays(1, [vao])
|
|
1083
|
+
|
|
1084
|
+
return gl_state.get("label_vao", create, delete)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _label_atlas(gl_state, texts, font=None):
|
|
1088
|
+
"""The strip atlas for this view's label strings, cached until the
|
|
1089
|
+
string SET changes (tick sets only change at zoom thresholds, so
|
|
1090
|
+
re-bakes are rare). `texts` must be a sorted tuple."""
|
|
1091
|
+
|
|
1092
|
+
def create():
|
|
1093
|
+
return bake_texts(texts, gl_state=gl_state, font=font)
|
|
1094
|
+
|
|
1095
|
+
def delete(value):
|
|
1096
|
+
gl.glDeleteTextures([value[0].texture_id])
|
|
1097
|
+
|
|
1098
|
+
return gl_state.get("label_atlas", create, delete, deps=(texts, id(font)))
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _axis_edges(tilt, spin, zoom, aspect, width, height,
|
|
1102
|
+
scale=(1.0, 1.0, 1.0), pan=(0.0, 0.0, 0.0), ortho=False, roll=0.0):
|
|
1103
|
+
"""The volume box's silhouette edges, each clipped to its VISIBLE span —
|
|
1104
|
+
the Python mirror of the shader's orbit camera (extents = `scale`, the
|
|
1105
|
+
voxel-count-proportional volume_scale), so lines and labels land exactly
|
|
1106
|
+
on the rendered edges.
|
|
1107
|
+
|
|
1108
|
+
Face visibility is decided in WORLD space: front-facing iff the eye is
|
|
1109
|
+
outside the face's plane (ortho: iff the view direction looks against
|
|
1110
|
+
its normal) — never from projected corner geometry. The old test used
|
|
1111
|
+
the projected quad's shoelace area against an absolute px² threshold and
|
|
1112
|
+
needed all four corners in front of the near plane; on a wide-skinny
|
|
1113
|
+
volume (a (1, 96, 4096) slab is a 1.0 × 0.023 × 0.0002 box) any zoom that
|
|
1114
|
+
makes the data readable puts the camera INSIDE the box's long span, the
|
|
1115
|
+
near corners fell to the behind-camera cutoff, and every face and edge
|
|
1116
|
+
touching them vanished — the axis hid exactly when you zoomed in to
|
|
1117
|
+
read it, and orbiting changed which corners died.
|
|
1118
|
+
|
|
1119
|
+
An edge is on the silhouette iff exactly one adjacent face is front-
|
|
1120
|
+
facing (edge-on faces count as back-facing, so the camera-facing square
|
|
1121
|
+
contributes all four sides in an exact top view); eye inside the box —
|
|
1122
|
+
no face front-facing — keeps all 12, so the box stays outlined and
|
|
1123
|
+
labeled from the inside. Each edge then clips against the near plane in
|
|
1124
|
+
camera space and the image rect in screen space (screen params map back
|
|
1125
|
+
through the perspective-correct 1/z interpolation), so a partially-
|
|
1126
|
+
behind or partially-offscreen axis keeps its on-screen portion.
|
|
1127
|
+
|
|
1128
|
+
Returns [(a, b, pa, pb, t0, t1, z0, z1)]: the ±1 corner sign tuples, the
|
|
1129
|
+
screen endpoints of the visible span, its world-param range over a→b
|
|
1130
|
+
(exactly 0.0 / 1.0 when that end is the true corner), and the camera
|
|
1131
|
+
depths at the visible ends (equal under ortho) for perspective-correct
|
|
1132
|
+
tick placement downstream."""
|
|
1133
|
+
# The shader camera's basis (voxel_camera.basis), in numpy.
|
|
1134
|
+
fwd, right, up = (np.array(v, np.float64) for v in _cam_basis(tilt, spin, roll))
|
|
1135
|
+
eye = np.asarray(pan, np.float64) - fwd * zoom
|
|
1136
|
+
sc = np.asarray(scale, np.float64)
|
|
1137
|
+
# Inverse of the shader's ray gen (rd ∝ fwd*1.7 + right*ndc.x + up*ndc.y,
|
|
1138
|
+
# ndc.x pre-scaled by aspect): ndc = 1.7 * cam_xy / cam_z, x /= aspect.
|
|
1139
|
+
# Ortho divides by the fixed frame half-size (zoom/1.7) instead of the
|
|
1140
|
+
# point's own depth.
|
|
1141
|
+
ortho_denom = max(zoom, 1e-6) / 1.7
|
|
1142
|
+
|
|
1143
|
+
def to_screen(cx, cy, cz):
|
|
1144
|
+
denom = ortho_denom if ortho else cz / 1.7
|
|
1145
|
+
ndx = (cx / denom) / aspect
|
|
1146
|
+
ndy = cy / denom
|
|
1147
|
+
return ((ndx * 0.5 + 0.5) * width, (1.0 - (ndy * 0.5 + 0.5)) * height)
|
|
1148
|
+
|
|
1149
|
+
def face_visible(k, s):
|
|
1150
|
+
# The box is centered on the ORIGIN (pan is the camera target).
|
|
1151
|
+
return (-s * fwd[k] > 1e-12) if ortho else (s * eye[k] > sc[k])
|
|
1152
|
+
|
|
1153
|
+
vis = {(k, s): face_visible(k, s) for k in range(3) for s in (-1, 1)}
|
|
1154
|
+
any_vis = any(vis.values())
|
|
1155
|
+
|
|
1156
|
+
def clip(a, b):
|
|
1157
|
+
# World → camera space (right/up/depth) at both corners.
|
|
1158
|
+
da = np.asarray(a, np.float64) * sc - eye
|
|
1159
|
+
db = np.asarray(b, np.float64) * sc - eye
|
|
1160
|
+
az, bz = float(da @ fwd), float(db @ fwd)
|
|
1161
|
+
if az < _AXIS_NEAR and bz < _AXIS_NEAR:
|
|
1162
|
+
return None
|
|
1163
|
+
t0, t1 = 0.0, 1.0
|
|
1164
|
+
if az < _AXIS_NEAR:
|
|
1165
|
+
t0 = (_AXIS_NEAR - az) / (bz - az)
|
|
1166
|
+
elif bz < _AXIS_NEAR:
|
|
1167
|
+
t1 = (_AXIS_NEAR - az) / (bz - az)
|
|
1168
|
+
ax, ay = float(da @ right), float(da @ up)
|
|
1169
|
+
bx, by = float(db @ right), float(db @ up)
|
|
1170
|
+
cx0, cy0, cz0 = ax + (bx - ax) * t0, ay + (by - ay) * t0, az + (bz - az) * t0
|
|
1171
|
+
cx1, cy1, cz1 = ax + (bx - ax) * t1, ay + (by - ay) * t1, az + (bz - az) * t1
|
|
1172
|
+
pa, pb = to_screen(cx0, cy0, cz0), to_screen(cx1, cy1, cz1)
|
|
1173
|
+
# Liang-Barsky against the image rect.
|
|
1174
|
+
s0, s1 = 0.0, 1.0
|
|
1175
|
+
dx, dy = pb[0] - pa[0], pb[1] - pa[1]
|
|
1176
|
+
for p, q in ((-dx, pa[0]), (dx, width - pa[0]),
|
|
1177
|
+
(-dy, pa[1]), (dy, height - pa[1])):
|
|
1178
|
+
if abs(p) < 1e-9:
|
|
1179
|
+
if q < 0.0:
|
|
1180
|
+
return None
|
|
1181
|
+
continue
|
|
1182
|
+
r = q / p
|
|
1183
|
+
if p < 0.0:
|
|
1184
|
+
if r > s1:
|
|
1185
|
+
return None
|
|
1186
|
+
if r > s0:
|
|
1187
|
+
s0 = r
|
|
1188
|
+
else:
|
|
1189
|
+
if r < s0:
|
|
1190
|
+
return None
|
|
1191
|
+
if r < s1:
|
|
1192
|
+
s1 = r
|
|
1193
|
+
|
|
1194
|
+
def world_u(s):
|
|
1195
|
+
# Screen param → world param over the near-clipped span: 1/z
|
|
1196
|
+
# interpolates linearly in screen space, so u = s-z0/(z1+s-(z0-z1));
|
|
1197
|
+
# ortho z is affine (u = s).
|
|
1198
|
+
return s if ortho else s * cz0 / (cz1 + s * (cz0 - cz1))
|
|
1199
|
+
|
|
1200
|
+
u0, u1 = world_u(s0), world_u(s1)
|
|
1201
|
+
return (a, b,
|
|
1202
|
+
(pa[0] + dx * s0, pa[1] + dy * s0),
|
|
1203
|
+
(pa[0] + dx * s1, pa[1] + dy * s1),
|
|
1204
|
+
t0 + (t1 - t0) * u0, t0 + (t1 - t0) * u1,
|
|
1205
|
+
cz0 + (cz1 - cz0) * u0, cz0 + (cz1 - cz0) * u1)
|
|
1206
|
+
|
|
1207
|
+
edges = []
|
|
1208
|
+
for k in range(3):
|
|
1209
|
+
i, j = (k + 1) % 3, (k + 2) % 3
|
|
1210
|
+
for si in (-1, 1):
|
|
1211
|
+
for sj in (-1, 1):
|
|
1212
|
+
if any_vis and vis[(i, si)] == vis[(j, sj)]:
|
|
1213
|
+
continue # the edge's two faces agree → not visible
|
|
1214
|
+
a, b = [0, 0, 0], [0, 0, 0]
|
|
1215
|
+
a[k], b[k] = -1, 1
|
|
1216
|
+
a[i] = b[i] = si
|
|
1217
|
+
a[j] = b[j] = sj
|
|
1218
|
+
rec = clip(tuple(a), tuple(b))
|
|
1219
|
+
if rec is not None:
|
|
1220
|
+
edges.append(rec)
|
|
1221
|
+
return edges
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _draw_axis_lines(draw_list, img_pos, edges):
|
|
1225
|
+
"""The visible silhouette spans as thin imgui lines, shortened near true
|
|
1226
|
+
CORNERS (the original fixed_shorten look); a clipped end (near plane /
|
|
1227
|
+
screen border) runs to its cut, since the edge continues past it. Labels
|
|
1228
|
+
are NOT drawn here any more — they're textured billboards in the voxel
|
|
1229
|
+
FBO (_billboard_specs + _render_label_billboards), so they live in the
|
|
1230
|
+
3-D scene."""
|
|
1231
|
+
line_col = pack_color(0.9, 0.9, 1.0, 0.5)
|
|
1232
|
+
for a, b, pa, pb, t0, t1, z0, z1 in edges:
|
|
1233
|
+
dx, dy = pb[0] - pa[0], pb[1] - pa[1]
|
|
1234
|
+
length = math.hypot(dx, dy)
|
|
1235
|
+
if length < 0.5:
|
|
1236
|
+
continue # zero-area edge: nothing to draw, skip the div
|
|
1237
|
+
# Short edges shorten proportionally instead of vanishing - the
|
|
1238
|
+
# outline only ever skips sub-2px degenerates.
|
|
1239
|
+
shorten = min(_EDGE_SHORTEN_PX, length * 0.25)
|
|
1240
|
+
sh_a = shorten if t0 == 0.0 else 0.0
|
|
1241
|
+
sh_b = shorten if t1 == 1.0 else 0.0
|
|
1242
|
+
ux, uy = dx / length, dy / length
|
|
1243
|
+
draw_list.add_line(img_pos[0] + pa[0] + ux * sh_a, img_pos[1] + pa[1] + uy * sh_a,
|
|
1244
|
+
img_pos[0] + pb[0] - ux * sh_b, img_pos[1] + pb[1] - uy * sh_b,
|
|
1245
|
+
line_col, 1.0)
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _billboard_specs(edges, axis_display, volume_scale,
|
|
1249
|
+
name_size=24.0, name_padding=34.0, name_opacity=1.0,
|
|
1250
|
+
num_size=16.0, num_padding=11.0, num_opacity=1.0,
|
|
1251
|
+
num_spacing=1.6, num_angle=0.0):
|
|
1252
|
+
"""[(text, anchor3, u_dir3, v_dir3, out_dir3, px_h, off_px, alpha)] for
|
|
1253
|
+
every visible silhouette span — the dim name beside the SPAN's midpoint
|
|
1254
|
+
(always on screen, unlike a clipped edge's full midpoint, which can sit
|
|
1255
|
+
behind the camera) plus integer ticks (_tick_values) at their TRUE
|
|
1256
|
+
positions along the edge; a clipped edge labels only its on-screen index
|
|
1257
|
+
range, so a zoomed-in wide volume reads like a scrolled ruler. Anchors
|
|
1258
|
+
are volume-box WORLD points ON the edge. All metrics are screen PIXELS,
|
|
1259
|
+
held at any zoom (the shader depth-converts at each anchor): `*_size` is
|
|
1260
|
+
the label height (0 hides that label type), `*_padding` the GAP between
|
|
1261
|
+
the line and the label's near edge (independent of size), `*_opacity`
|
|
1262
|
+
the tint alpha. u runs along the edge and v outward from the box
|
|
1263
|
+
("angled perpendicular to the line"); both are flipped for readability —
|
|
1264
|
+
the up-axis flips when the quad shows its back (un-mirrors without
|
|
1265
|
+
reversing the reading direction), then a 180° spin makes text read
|
|
1266
|
+
left-to-right, or bottom-to-top on near-vertical edges. The offset
|
|
1267
|
+
always rides the UNFLIPPED outward direction, so labels never land
|
|
1268
|
+
inside the box. Nothing hides by projected size any more — the
|
|
1269
|
+
face-visibility silhouette already culls truly invisible edges, and
|
|
1270
|
+
_tick_values degrades to just the end values on short edges."""
|
|
1271
|
+
name_off = name_padding + name_size * 0.5 # anchor -> label CENTER
|
|
1272
|
+
num_off = num_padding + num_size * 0.5
|
|
1273
|
+
# tick label slant (optional, not the label plane - matplotlib-style)
|
|
1274
|
+
ca, sa = math.cos(math.radians(num_angle)), math.sin(math.radians(num_angle))
|
|
1275
|
+
pts = [p for e in edges for p in (e[2], e[3])]
|
|
1276
|
+
if not pts:
|
|
1277
|
+
return []
|
|
1278
|
+
scx = sum(p[0] for p in pts) / len(pts) # silhouette's screen centroid
|
|
1279
|
+
scy = sum(p[1] for p in pts) / len(pts)
|
|
1280
|
+
|
|
1281
|
+
specs = []
|
|
1282
|
+
for a, b, pa, pb, t0, t1, z0, z1 in edges:
|
|
1283
|
+
k = next(i for i in range(3) if a[i] != b[i]) # the axis it runs along
|
|
1284
|
+
if a[k] > b[k]: # a = the texcoord-0 end (visible span flips with it)
|
|
1285
|
+
a, b = b, a
|
|
1286
|
+
pa, pb = pb, pa
|
|
1287
|
+
t0, t1 = 1.0 - t1, 1.0 - t0
|
|
1288
|
+
z0, z1 = z1, z0
|
|
1289
|
+
px_len = math.hypot(pb[0] - pa[0], pb[1] - pa[1])
|
|
1290
|
+
if px_len < 0.5:
|
|
1291
|
+
continue # zero-area edge: direction math requires a length
|
|
1292
|
+
name, size = axis_display[k]
|
|
1293
|
+
a3 = tuple(a[i] * volume_scale[i] for i in range(3))
|
|
1294
|
+
b3 = tuple(b[i] * volume_scale[i] for i in range(3))
|
|
1295
|
+
length = math.sqrt(sum((b3[i] - a3[i]) ** 2 for i in range(3))) or 1.0
|
|
1296
|
+
w = tuple((b3[i] - a3[i]) / length for i in range(3)) # a → b, for placement
|
|
1297
|
+
mid_full = tuple((a3[i] + b3[i]) * 0.5 for i in range(3))
|
|
1298
|
+
m_len = math.sqrt(sum(c * c for c in mid_full)) or 1.0
|
|
1299
|
+
out = tuple(c / m_len for c in mid_full) # outward, ⊥ the edge (mid-w = 0)
|
|
1300
|
+
# World endpoints + midpoint of the VISIBLE span (the label anchors).
|
|
1301
|
+
va = tuple(a3[i] + (b3[i] - a3[i]) * t0 for i in range(3))
|
|
1302
|
+
vb = tuple(a3[i] + (b3[i] - a3[i]) * t1 for i in range(3))
|
|
1303
|
+
mid = tuple((va[i] + vb[i]) * 0.5 for i in range(3))
|
|
1304
|
+
|
|
1305
|
+
# TRUE screen directions, not the camera-basis approximation (which
|
|
1306
|
+
# skews under perspective for off-center edges and mirrors oblique
|
|
1307
|
+
# labels): the baseline from the edge's mean projected points, the
|
|
1308
|
+
# outward axis as its perpendicular pointing away from the
|
|
1309
|
+
# silhouette's screen centroid (the world `out` projects into that
|
|
1310
|
+
# half-space for any silhouette edge, so the signs match).
|
|
1311
|
+
u_s = ((pb[0] - pa[0]) / px_len, (pb[1] - pa[1]) / px_len)
|
|
1312
|
+
mxs, mys = (pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5
|
|
1313
|
+
ox, oy = mxs - scx, mys - scy
|
|
1314
|
+
along = ox * u_s[0] + oy * u_s[1]
|
|
1315
|
+
nx, ny = ox - along * u_s[0], oy - along * u_s[1]
|
|
1316
|
+
nl = math.hypot(nx, ny) or 1.0
|
|
1317
|
+
v_s = (nx / nl, ny / nl)
|
|
1318
|
+
|
|
1319
|
+
u, v = w, out
|
|
1320
|
+
# Chirality - readable text needs cross(u_s, v_s) < 0 on a y-down
|
|
1321
|
+
# screen. When the quad shows its back, flip the UP axis - that
|
|
1322
|
+
# un-mirrors top/bottom without reversing the reading direction.
|
|
1323
|
+
if u_s[0] * v_s[1] - u_s[1] * v_s[0] > 0:
|
|
1324
|
+
v = tuple(-c for c in v)
|
|
1325
|
+
# 180° flipping (chirality-preserving): read left-to-right, or
|
|
1326
|
+
# bottom-to-top when the baseline is near-vertical on screen.
|
|
1327
|
+
if u_s[0] < -0.2 * abs(u_s[1]) or (
|
|
1328
|
+
abs(u_s[0]) <= 0.2 * abs(u_s[1]) and u_s[1] > 0):
|
|
1329
|
+
u = tuple(-c for c in u)
|
|
1330
|
+
v = tuple(-c for c in v)
|
|
1331
|
+
|
|
1332
|
+
if name_size > 0:
|
|
1333
|
+
specs.append((name, mid, u, v, out, name_size, name_off, name_opacity))
|
|
1334
|
+
i0, i1 = t0 * size, t1 * size # the visible index range
|
|
1335
|
+
if num_size > 0 and size > 0 and i1 - i0 > 1e-9:
|
|
1336
|
+
# Ticks compress into the DRAWN line span (true-corner ends draw
|
|
1337
|
+
# shortened; clipped ends need to be cut), and the end labels
|
|
1338
|
+
# sit at the visible line ends. Screen px and world params go
|
|
1339
|
+
# through the perspective-correct 1/z map (affine when z0 == z1,
|
|
1340
|
+
# i.e. ortho or an edge parallel to the screen).
|
|
1341
|
+
def u_of(s):
|
|
1342
|
+
return s if z0 == z1 else s * z0 / (z1 + s * (z0 - z1))
|
|
1343
|
+
|
|
1344
|
+
def s_of(up):
|
|
1345
|
+
return up if z0 == z1 else up * z1 / (z0 + up * (z1 - z0))
|
|
1346
|
+
|
|
1347
|
+
inset_px = min(_EDGE_SHORTEN_PX, px_len * 0.25)
|
|
1348
|
+
u_lo = u_of(inset_px / px_len if t0 == 0.0 else 0.0)
|
|
1349
|
+
u_hi = u_of(1.0 - (inset_px / px_len if t1 == 1.0 else 0.0))
|
|
1350
|
+
if num_angle:
|
|
1351
|
+
ut = tuple(ca * u[i] + sa * v[i] for i in range(3))
|
|
1352
|
+
vt = tuple(ca * v[i] - sa * u[i] for i in range(3))
|
|
1353
|
+
else:
|
|
1354
|
+
ut, vt = u, v
|
|
1355
|
+
# Step from the span's AVERAGE screen density; perspective
|
|
1356
|
+
# compresses the far end, so greedily skip interior ticks whose
|
|
1357
|
+
# SCREEN positions crowd the previous one or the end label.
|
|
1358
|
+
ticks = _tick_values(i0, i1, px_len / (i1 - i0), num_size,
|
|
1359
|
+
num_spacing) # [] on an integer-free sliver
|
|
1360
|
+
min_gap = max(1, len(str(ticks[-1] if ticks else 0))) \
|
|
1361
|
+
* 0.62 * num_size * num_spacing
|
|
1362
|
+
placed = []
|
|
1363
|
+
for n, idx in enumerate(ticks):
|
|
1364
|
+
up = u_lo + (u_hi - u_lo) * ((idx - i0) / (i1 - i0))
|
|
1365
|
+
s_px = s_of(up) * px_len
|
|
1366
|
+
if 0 < n < len(ticks) - 1 and placed and (
|
|
1367
|
+
s_px - placed[-1] < min_gap
|
|
1368
|
+
or s_of(u_hi) * px_len - s_px < min_gap):
|
|
1369
|
+
continue
|
|
1370
|
+
placed.append(s_px)
|
|
1371
|
+
p = tuple(va[i] + (vb[i] - va[i]) * up for i in range(3))
|
|
1372
|
+
specs.append((str(idx), p, ut, vt, out, num_size, num_off, num_opacity))
|
|
1373
|
+
return specs
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _render_label_billboards(gl_state, specs, cam, height, font=None):
|
|
1377
|
+
"""Draw every label spec into the CURRENT FBO with the volume's camera
|
|
1378
|
+
(`cam` = the camera uniform kwargs) in ONE instanced draw: assemble the
|
|
1379
|
+
per-instance buffer (anchor/axes/metrics/uv-rect per label), upload,
|
|
1380
|
+
glDrawArraysInstanced. Pixel sizes/offsets convert to NDC units against
|
|
1381
|
+
the viewport height (`height`); the shader depth-scales them at each
|
|
1382
|
+
anchor for screen-constant labels."""
|
|
1383
|
+
if not specs:
|
|
1384
|
+
return
|
|
1385
|
+
texts = tuple(sorted({s[0] for s in specs}))
|
|
1386
|
+
atlas, rects = _label_atlas(gl_state, texts, font=font)
|
|
1387
|
+
prog, loc = _label_program(gl_state)
|
|
1388
|
+
vao, vbo = _label_vao(gl_state)
|
|
1389
|
+
|
|
1390
|
+
ndc_per_px = 2.0 / max(1.0, float(height))
|
|
1391
|
+
data = np.empty((len(specs), _LABEL_FLOATS), np.float32)
|
|
1392
|
+
for i, (text, anchor, u, v, out, px_h, off_px, alpha) in enumerate(specs):
|
|
1393
|
+
u0, v0, u1, v1, tw, th = rects[text]
|
|
1394
|
+
half_h = (px_h * 0.5) * ndc_per_px
|
|
1395
|
+
row = data[i]
|
|
1396
|
+
row[0:3] = anchor
|
|
1397
|
+
row[3:6] = u
|
|
1398
|
+
row[6:9] = v
|
|
1399
|
+
row[9:12] = out
|
|
1400
|
+
row[12] = half_h * (tw / max(1, th))
|
|
1401
|
+
row[13] = half_h
|
|
1402
|
+
row[14] = off_px * ndc_per_px
|
|
1403
|
+
row[15] = alpha
|
|
1404
|
+
row[16:20] = (u0, v0, u1, v1)
|
|
1405
|
+
|
|
1406
|
+
blend_was = bool(gl.glIsEnabled(gl.GL_BLEND))
|
|
1407
|
+
prev_prog = gl.glGetIntegerv(gl.GL_CURRENT_PROGRAM)
|
|
1408
|
+
gl.glEnable(gl.GL_BLEND)
|
|
1409
|
+
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
|
1410
|
+
gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
|
1411
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
1412
|
+
gl.glUseProgram(prog)
|
|
1413
|
+
gl.glUniform1f(loc["tilt"], cam["tilt"])
|
|
1414
|
+
gl.glUniform1f(loc["spin"], cam["spin"])
|
|
1415
|
+
gl.glUniform1f(loc["roll"], cam["roll"])
|
|
1416
|
+
gl.glUniform1f(loc["zoom"], cam["zoom"])
|
|
1417
|
+
gl.glUniform1f(loc["aspect"], cam["aspect"])
|
|
1418
|
+
gl.glUniform1i(loc["ortho"], 1 if cam["ortho"] else 0)
|
|
1419
|
+
gl.glUniform3f(loc["pan"], cam["pan_x"], cam["pan_y"], cam["pan_z"])
|
|
1420
|
+
gl.glUniform1i(loc["label"], 0)
|
|
1421
|
+
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
1422
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, atlas.texture_id)
|
|
1423
|
+
gl.glBindVertexArray(vao)
|
|
1424
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
|
|
1425
|
+
gl.glBufferData(gl.GL_ARRAY_BUFFER, data.nbytes, data, gl.GL_STREAM_DRAW)
|
|
1426
|
+
gl.glDrawArraysInstanced(gl.GL_TRIANGLES, 0, 6, len(specs))
|
|
1427
|
+
gl.glBindVertexArray(0)
|
|
1428
|
+
gl.glUseProgram(prev_prog)
|
|
1429
|
+
if not blend_was:
|
|
1430
|
+
gl.glDisable(gl.GL_BLEND)
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
VOXEL_FRAG = """
|
|
1434
|
+
#version 330 core
|
|
1435
|
+
in vec2 uv;
|
|
1436
|
+
out vec4 FragColor;
|
|
1437
|
+
|
|
1438
|
+
// Slab intersection with the box [-bounds, +bounds]: (t_enter, t_exit).
|
|
1439
|
+
vec2 rayBox(vec3 ro, vec3 rd, vec3 bounds) {
|
|
1440
|
+
vec3 inv = 1.0 / rd;
|
|
1441
|
+
vec3 t0 = (-bounds - ro) * inv;
|
|
1442
|
+
vec3 t1 = ( bounds - ro) * inv;
|
|
1443
|
+
vec3 lo = min(t0, t1);
|
|
1444
|
+
vec3 hi = max(t0, t1);
|
|
1445
|
+
return vec2(max(max(lo.x, lo.y), lo.z), min(min(hi.x, hi.y), hi.z));
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ── shared transfer function: raw volume sample at texcoord p →
|
|
1449
|
+
// (v: remapped LUT coordinate, m: opacity drive). The old viewer's value
|
|
1450
|
+
// pipeline verbatim — contrast about mid-grey, then brightness, on the
|
|
1451
|
+
// GREYSCALE value; `centered` maps signed data so raw 0 sits at the LUT
|
|
1452
|
+
// middle (pair with a diverging LUT) and opacity keys on MAGNITUDE, so
|
|
1453
|
+
// negatives render as strongly as positives. Takes the sampler to read
|
|
1454
|
+
// through: `volume` (the user's nearest/linear toggle) for the color march,
|
|
1455
|
+
// `volume_lin` (always linear — same texture, its own sampler object) for
|
|
1456
|
+
// every shading read, where smooth beats blocky regardless of the toggle.
|
|
1457
|
+
vec2 remapValue(sampler3D vol, vec3 p) {
|
|
1458
|
+
float v = texture(vol, p).r;
|
|
1459
|
+
if (centered) { v = v * 0.5 + 0.5; } // signed [-1,1] -> [0,1]
|
|
1460
|
+
v = (v - 0.5) * contrast + 0.5;
|
|
1461
|
+
float m;
|
|
1462
|
+
if (centered) {
|
|
1463
|
+
v = 0.5 + (v - 0.5) * brightness;
|
|
1464
|
+
v = clamp(v, 0.0, 1.0);
|
|
1465
|
+
m = abs(v - 0.5) * 2.0;
|
|
1466
|
+
} else {
|
|
1467
|
+
v *= brightness;
|
|
1468
|
+
v = clamp(v, 0.0, 1.0);
|
|
1469
|
+
m = v;
|
|
1470
|
+
}
|
|
1471
|
+
return vec2(v, m);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Extended-sRGB decode (hdr_color.py's convention): the sRGB curve
|
|
1475
|
+
// mirrored for negatives, no ceiling — a LUT entry above 1 is brighter than
|
|
1476
|
+
// the desktop's white, a negative one is outside the sRGB gamut (P3). A
|
|
1477
|
+
// plain pow() turned negatives into NaN.
|
|
1478
|
+
vec3 decodeSrgb(vec3 c) { return sign(c) * pow(abs(c), vec3(2.2)); }
|
|
1479
|
+
|
|
1480
|
+
// The old viewer's opacity ramp: values at/above the gate (1 - threshold)
|
|
1481
|
+
// are FULLY opaque — a hard isosurface — and below it opacity falls off as
|
|
1482
|
+
// (m/gate)^4, scaled by density and the volume_scale-NORMALIZED segment
|
|
1483
|
+
// length, so optical depth is a function of the FRACTION of the volume
|
|
1484
|
+
// traversed, not the world path length — a 4096-voxel axis viewed end-on
|
|
1485
|
+
// accumulates the same opacity as an 8-voxel one.
|
|
1486
|
+
float alphaFor(float m, float seg_n) {
|
|
1487
|
+
float gate = 1.0 - clamp(threshold, 0.0, 0.999);
|
|
1488
|
+
if (m >= gate) return 1.0;
|
|
1489
|
+
return clamp(pow(m / gate, 4.0) * density * seg_n * 50.0, 0.0, 1.0);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Transmittance from a world point toward the light: a COARSE fixed-count
|
|
1493
|
+
// march of the SAME transfer function (shadows are low-frequency — fat
|
|
1494
|
+
// steps read clean where the primary ray needs thousands), multiplying out
|
|
1495
|
+
// per-step opacity. The result is graded the way the render is: haze dims
|
|
1496
|
+
// the light, the opaque core blocks it. `steps` sets the quality tier
|
|
1497
|
+
// (the plane's cast shadow affords more than per-sample self-shading) and
|
|
1498
|
+
// `max_dist` optionally caps the march to NEAR-FIELD occluders — for
|
|
1499
|
+
// self-shading, what's right next to a sample is most of its shadow.
|
|
1500
|
+
// Always reads through volume_lin: shading wants smooth fields.
|
|
1501
|
+
// The Info variant also reports WHERE occlusion happened: (T, t_occ) with
|
|
1502
|
+
// t_occ the distance along the light ray at which transmittance first
|
|
1503
|
+
// dropped below 0.5 — the occluder height that drives the ground-space
|
|
1504
|
+
// penumbra radius. Rays that never occlude report the mid-chord of their
|
|
1505
|
+
// box span instead, so pixels just OUTSIDE the umbra blur with the same
|
|
1506
|
+
// radius as their shadowed neighbors (the penumbra widens on BOTH sides
|
|
1507
|
+
// of the hard edge); rays that miss the box entirely report 0.
|
|
1508
|
+
vec2 lightVisibilityInfo(vec3 p, vec3 lp, int steps, float max_dist) {
|
|
1509
|
+
vec3 ld = normalize(lp - p);
|
|
1510
|
+
vec2 span = rayBox(p, ld, volume_scale);
|
|
1511
|
+
float t0 = max(span.x, 0.0);
|
|
1512
|
+
float t1 = min(min(span.y, length(lp - p)), max_dist);
|
|
1513
|
+
if (t0 >= t1) return vec2(1.0, 0.0);
|
|
1514
|
+
float ss = (t1 - t0) / float(steps);
|
|
1515
|
+
float seg_n = ss * length(ld / volume_scale);
|
|
1516
|
+
float T = 1.0;
|
|
1517
|
+
float t_occ = 0.0;
|
|
1518
|
+
float t = t0 + ss * 0.5;
|
|
1519
|
+
for (int i = 0; i < steps; i++) {
|
|
1520
|
+
vec3 q = (p + ld * t) / volume_scale * 0.5 + 0.5;
|
|
1521
|
+
T *= 1.0 - alphaFor(remapValue(volume_lin, q).y, seg_n);
|
|
1522
|
+
if (t_occ == 0.0 && T < 0.5) { t_occ = t; }
|
|
1523
|
+
if (T < 0.02) break; // fully shadowed — stop early
|
|
1524
|
+
t += ss;
|
|
1525
|
+
}
|
|
1526
|
+
if (t_occ == 0.0) { t_occ = 0.5 * (t0 + t1); }
|
|
1527
|
+
return vec2(T, t_occ);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
float lightVisibility(vec3 p, vec3 lp, int steps, float max_dist) {
|
|
1531
|
+
return lightVisibilityInfo(p, lp, steps, max_dist).x;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Gradient normal at texcoord p (central differences, one voxel apart),
|
|
1535
|
+
// mapped to WORLD space (anisotropic boxes bend gradients), plus a shading
|
|
1536
|
+
// weight (w) that fades to 0 where the gradient is too weak to trust —
|
|
1537
|
+
// uniform haze keeps its flat unshaded look instead of picking up noise.
|
|
1538
|
+
vec4 volumeNormal(vec3 p) {
|
|
1539
|
+
// 1.75-voxel stencil: with the linear sampler this is a genuine lowpass
|
|
1540
|
+
// on the normal field, so hard binary edges (a 0/1 mask's staircase)
|
|
1541
|
+
// shade as smooth ramps instead of per-voxel facets.
|
|
1542
|
+
vec3 e = 1.75 / vec3(textureSize(volume_lin, 0));
|
|
1543
|
+
vec3 g = vec3(
|
|
1544
|
+
texture(volume_lin, p + vec3(e.x, 0, 0)).r - texture(volume_lin, p - vec3(e.x, 0, 0)).r,
|
|
1545
|
+
texture(volume_lin, p + vec3(0, e.y, 0)).r - texture(volume_lin, p - vec3(0, e.y, 0)).r,
|
|
1546
|
+
texture(volume_lin, p + vec3(0, 0, e.z)).r - texture(volume_lin, p - vec3(0, 0, e.z)).r);
|
|
1547
|
+
if (centered) { g *= sign(texture(volume_lin, p).r); } // shade |v|'s surface
|
|
1548
|
+
vec3 gw = g / volume_scale; // texcoord gradient → world
|
|
1549
|
+
float len = length(gw);
|
|
1550
|
+
// The normal points from dense toward empty — that's MINUS the gradient.
|
|
1551
|
+
return vec4(len > 1e-6 ? -gw / len : vec3(0.0, 0.0, 1.0),
|
|
1552
|
+
clamp(length(g) * 6.0, 0.0, 1.0));
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
void main() {
|
|
1556
|
+
// Z-up orbit camera built straight from injected uniforms — tilt, spin, roll,
|
|
1557
|
+
// zoom, pan and ortho arrive as plain Python kwargs, no matrices anywhere.
|
|
1558
|
+
// The basis is analytic in spin/tilt (not cross(fwd, world-up)) so the
|
|
1559
|
+
// numpad top/bottom presets (tilt = ±π/2) stay well-defined; it matches
|
|
1560
|
+
// the old construction everywhere else.
|
|
1561
|
+
float ct = cos(tilt);
|
|
1562
|
+
vec3 fwd = -vec3(cos(spin) * ct, sin(spin) * ct, sin(tilt));
|
|
1563
|
+
vec3 right0 = vec3(-sin(spin), cos(spin), 0.0);
|
|
1564
|
+
// roll turns right toward up about the view axis (0 = level horizon,
|
|
1565
|
+
// the turntable); the 3D mouse's trackball mode is what writes it.
|
|
1566
|
+
vec3 right = right0 * cos(roll) + cross(right0, fwd) * sin(roll);
|
|
1567
|
+
vec3 up = cross(right, fwd);
|
|
1568
|
+
vec3 eye = vec3(pan_x, pan_y, pan_z) - fwd * zoom;
|
|
1569
|
+
vec2 ndc = (uv * 2.0 - 1.0) * vec2(aspect, 1.0);
|
|
1570
|
+
// Perspective rays fan out from the eye; ortho rays march parallel from
|
|
1571
|
+
// a plane through it, sized to match the perspective frame at the target.
|
|
1572
|
+
vec3 ro = ortho ? eye + (right * ndc.x + up * ndc.y) * (zoom / 1.7) : eye;
|
|
1573
|
+
vec3 rd = ortho ? fwd : normalize(fwd * 1.7 + right * ndc.x + up * ndc.y);
|
|
1574
|
+
// Accumulate optical depth per unit of VIEW DEPTH, not per unit of ray
|
|
1575
|
+
// arc length. In perspective, edge rays cross the volume at a steeper
|
|
1576
|
+
// angle and a step's world length (seg) is ~1/cos(theta) longer than a
|
|
1577
|
+
// center ray's, so a thin slab reads denser toward the screen edges (a
|
|
1578
|
+
// screenspace radial artifact — hidden on cubes only because they saturate
|
|
1579
|
+
// the alpha break). cos(angle to fwd) cancels the extra path. Ortho rays
|
|
1580
|
+
// have rd == fwd, so view_cos == 1 and this is a no-op there.
|
|
1581
|
+
float view_cos = dot(rd, fwd);
|
|
1582
|
+
|
|
1583
|
+
// ── shadow catcher: the plane the box rests on (z = -volume_scale.z)
|
|
1584
|
+
// is itself INVISIBLE — its only contribution is the shadow the volume
|
|
1585
|
+
// casts onto it, composited as a darkening with alpha = blocked light.
|
|
1586
|
+
// One-sided (backface-culled analytically): a hit only counts for rays
|
|
1587
|
+
// striking the TOP face — from underneath there's no shadow at all.
|
|
1588
|
+
float plane_t = -1.0;
|
|
1589
|
+
float plane_a = 0.0;
|
|
1590
|
+
// The caught shadow darkens toward shadow_tint — a neutral grey-black
|
|
1591
|
+
// (Toggles.Voxels.floor_shadow_color), decoupled from the blue-black
|
|
1592
|
+
// the rest of the studio's compositor shadows carry.
|
|
1593
|
+
vec3 plane_c = shadow_tint;
|
|
1594
|
+
// plane_side mirrors the catcher to the box's OTHER face when the view
|
|
1595
|
+
// is upside-down (+1 = floor at -z, -1 = at +z; latched between drags
|
|
1596
|
+
// on the Python side). The conditions are the normal ones written in
|
|
1597
|
+
// z' = z * plane_side; the catcher's light is mirrored to match below.
|
|
1598
|
+
if (draw_plane && draw_shading && rd.z * plane_side < -1e-6
|
|
1599
|
+
&& ro.z * plane_side > -volume_scale.z) {
|
|
1600
|
+
plane_t = (-volume_scale.z * plane_side - ro.z) / rd.z;
|
|
1601
|
+
vec3 pw = ro + rd * plane_t;
|
|
1602
|
+
// Blocked light via the same transmittance march as the rest of the
|
|
1603
|
+
// shading, lifted by the ambient floor (ambient_light raises this
|
|
1604
|
+
// shadow like every other). The exponential radial fade bounds the
|
|
1605
|
+
// catcher so the darkening dies off instead of cutting.
|
|
1606
|
+
float ext = max(volume_scale.x, volume_scale.y);
|
|
1607
|
+
float r = max(length(pw.xy) - ext * 1.1, 0.0);
|
|
1608
|
+
// Center march gathers (visibility, occluder distance); the blur
|
|
1609
|
+
// then happens in FLOOR coordinates — 4 extra visibility taps on a
|
|
1610
|
+
// ring around the hit point, radius = shadow_softness × occluder
|
|
1611
|
+
// distance (higher occluders throw softer shadows), averaged with
|
|
1612
|
+
// the center. Skipped when the center ray misses the box (t_occ 0
|
|
1613
|
+
// — open floor, nothing to soften).
|
|
1614
|
+
// The catcher's marches use a PLANE-LOCAL light: light_pos with its
|
|
1615
|
+
// z mirrored to the plane's side. The real light stays fixed in
|
|
1616
|
+
// world space (the volume's shading uses it untouched) — this
|
|
1617
|
+
// mirror only makes the flipped floor catch the same silhouette
|
|
1618
|
+
// the bottom floor would, instead of a ceiling catching nothing.
|
|
1619
|
+
vec3 pl_light = vec3(light_pos.xy, light_pos.z * plane_side);
|
|
1620
|
+
vec2 vi = lightVisibilityInfo(pw, pl_light, 24, 1e8);
|
|
1621
|
+
float vis = vi.x;
|
|
1622
|
+
float blur_r = shadow_softness * vi.y;
|
|
1623
|
+
if (blur_r > 1e-4) {
|
|
1624
|
+
float acc_v = vis;
|
|
1625
|
+
for (int k = 0; k < 4; k++) {
|
|
1626
|
+
float ang = float(k) * 1.5707963 + 0.7853982;
|
|
1627
|
+
vec3 op = pw + vec3(cos(ang), sin(ang), 0.0) * blur_r;
|
|
1628
|
+
acc_v += lightVisibility(op, pl_light, 10, 1e8);
|
|
1629
|
+
}
|
|
1630
|
+
vis = acc_v / 5.0;
|
|
1631
|
+
}
|
|
1632
|
+
float shadow = (1.0 - ambient_light) * (1.0 - vis);
|
|
1633
|
+
plane_a = clamp(shadow * shadow_opacity, 0.0, 1.0) * exp(-1.5 * r / ext);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// volume_scale: box extents per axis, voxel-count-proportional — so each
|
|
1637
|
+
// VOXEL is a cube and the tensor keeps its true shape.
|
|
1638
|
+
vec2 hit = rayBox(ro, rd, volume_scale);
|
|
1639
|
+
bool box_hit = !(hit.x > hit.y || hit.y < 0.0);
|
|
1640
|
+
if (!box_hit && plane_t <= 0.0) { FragColor = vec4(0.0); return; }
|
|
1641
|
+
|
|
1642
|
+
vec4 acc = vec4(0.0);
|
|
1643
|
+
// Plane in FRONT of the volume (looking down at foreground floor past
|
|
1644
|
+
// the box — possible since the fade extends beyond it): composite it
|
|
1645
|
+
// first. The box's bottom face lies IN the plane, so a ray never
|
|
1646
|
+
// crosses the plane mid-march — it's strictly before or after the box.
|
|
1647
|
+
if (plane_t > 0.0 && box_hit && plane_t <= max(hit.x, 0.0)) {
|
|
1648
|
+
acc = vec4(plane_c * plane_a, plane_a);
|
|
1649
|
+
plane_t = -1.0;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
float t = max(hit.x, 0.0);
|
|
1653
|
+
// max_steps is a watchdog: the break on hit.y is what normally ends the
|
|
1654
|
+
// march. The whole box is covered only while max_steps * step_size
|
|
1655
|
+
// exceeds the worst-case chord (2*sqrt(3) ≈ 3.46 units) — a granular
|
|
1656
|
+
// step_size needs a higher cap or the far side of the volume clips away.
|
|
1657
|
+
for (int i = 0; i < max_steps; i++) {
|
|
1658
|
+
if (t >= hit.y || acc.a > 0.98) break;
|
|
1659
|
+
// Weight each sample by the segment it actually covers (the tail is
|
|
1660
|
+
// partial) and sample at the segment MIDPOINT: a slab thinner than
|
|
1661
|
+
// one step then accumulates opacity proportional to its true path
|
|
1662
|
+
// length instead of jumping by whole steps as the sample count
|
|
1663
|
+
// changes with view angle — the concentric-ring artifact on thin
|
|
1664
|
+
// tensors.
|
|
1665
|
+
float seg = min(step_size, hit.y - t);
|
|
1666
|
+
vec3 p = (ro + rd * (t + seg * 0.5)) / volume_scale * 0.5 + 0.5;
|
|
1667
|
+
// Value pipeline + opacity ramp live in remapValue/alphaFor (shared
|
|
1668
|
+
// with the shadow march). `lut` is a 1-D texture the LUT host baked
|
|
1669
|
+
// from a flat [r,g,b,...] float list.
|
|
1670
|
+
vec2 vm = remapValue(volume, p);
|
|
1671
|
+
float seg_n = seg * length(rd / volume_scale);
|
|
1672
|
+
float a = alphaFor(vm.y, seg_n * view_cos);
|
|
1673
|
+
if (a > 0.0) {
|
|
1674
|
+
// Composite in LINEAR light: the LUT tables are display-referred
|
|
1675
|
+
// sRGB, so decode each sample before accumulating (encode once at
|
|
1676
|
+
// the end). Blending in sRGB space skews mixes toward the more
|
|
1677
|
+
// saturated component — the old harsh/garish translucency.
|
|
1678
|
+
vec3 c = decodeSrgb(texture(lut, vm.x).rgb);
|
|
1679
|
+
if (draw_shading) {
|
|
1680
|
+
// Gradient-normal Lambert, weighted by gradient strength so
|
|
1681
|
+
// flat haze keeps its unshaded look; self_shading adds a
|
|
1682
|
+
// FAST near-field transmittance march toward the light —
|
|
1683
|
+
// 6 fat linear-filtered steps capped close to the sample,
|
|
1684
|
+
// since nearby occluders are most of a sample's shadow.
|
|
1685
|
+
// Both the normal taps and the march are skipped where they
|
|
1686
|
+
// can't show: sub-1% alpha samples, and (for the march)
|
|
1687
|
+
// gradient weight ≈ 0 — the mix would erase it anyway.
|
|
1688
|
+
float shade = 1.0;
|
|
1689
|
+
if (a > 0.01) {
|
|
1690
|
+
vec4 nw = volumeNormal(p);
|
|
1691
|
+
if (nw.w > 0.01) {
|
|
1692
|
+
vec3 wp = ro + rd * (t + seg * 0.5);
|
|
1693
|
+
// Half-Lambert wrap: (n·l/2 + 1/2)² instead of the
|
|
1694
|
+
// hard max(n·l, 0). Faces pointing away from the
|
|
1695
|
+
// light dim gently rather than clamping to the
|
|
1696
|
+
// ambient floor — on binary data the hard clamp
|
|
1697
|
+
// turned every off-facing step facet into the same
|
|
1698
|
+
// flat dark block.
|
|
1699
|
+
float ndl = dot(nw.xyz, normalize(light_pos - wp)) * 0.5 + 0.5;
|
|
1700
|
+
float vis = self_shading
|
|
1701
|
+
? lightVisibility(wp, light_pos, 6, 0.7) : 1.0;
|
|
1702
|
+
shade = mix(1.0, ambient_light
|
|
1703
|
+
+ (1.0 - ambient_light) * ndl * ndl * vis,
|
|
1704
|
+
nw.w * shading_strength);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
c *= decodeSrgb(light_tint) * light_brightness * shade;
|
|
1708
|
+
}
|
|
1709
|
+
acc.rgb += (1.0 - acc.a) * a * c;
|
|
1710
|
+
acc.a += (1.0 - acc.a) * a;
|
|
1711
|
+
}
|
|
1712
|
+
t += step_size;
|
|
1713
|
+
}
|
|
1714
|
+
// Plane BEHIND the volume (the usual case): composite it under
|
|
1715
|
+
// whatever the march accumulated.
|
|
1716
|
+
if (plane_t > 0.0) {
|
|
1717
|
+
acc.rgb += (1.0 - acc.a) * plane_c * plane_a;
|
|
1718
|
+
acc.a += (1.0 - acc.a) * plane_a;
|
|
1719
|
+
}
|
|
1720
|
+
// The target is the linear fp16 scene (hdr_color.py): no sRGB encode
|
|
1721
|
+
// here, the presentation pass does that once. `gamma` is an artistic
|
|
1722
|
+
// curve on the linear image, 1.0 = untouched (the colorimetric result),
|
|
1723
|
+
// above 1 darkens the mids against the studio's dark UI. Mirrored for
|
|
1724
|
+
// negatives (P3 rides as negative scRGB). No dither: the fp16 target
|
|
1725
|
+
// doesn't band.
|
|
1726
|
+
FragColor = vec4(sign(acc.rgb) * pow(abs(acc.rgb), vec3(gamma)), acc.a);
|
|
1727
|
+
}
|
|
1728
|
+
"""
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@shader_func(fragment=VOXEL_FRAG)
|
|
1732
|
+
def voxel_pass(gl_state: GLState = None, tilt=0.5, spin=0.8, roll=0.0, zoom=3.4,
|
|
1733
|
+
pan_x=0.0, pan_y=0.0, pan_z=0.0, ortho=False,
|
|
1734
|
+
aspect=1.0, brightness=1.0, contrast=1.0, density=1.0, gamma=1.6,
|
|
1735
|
+
threshold=0.1, step_size=0.0015, max_steps=4096, centered=False,
|
|
1736
|
+
volume=None, volume_lin=None, lut=None,
|
|
1737
|
+
draw_plane=True, shadow_opacity=1.0, shadow_softness=0.15,
|
|
1738
|
+
shadow_tint=(0.0, 0.02, 0.05), plane_side=1.0,
|
|
1739
|
+
draw_shading=True, self_shading=True,
|
|
1740
|
+
light_pos=(90.0, -90.0, 200.0), light_tint=(1.0, 1.0, 1.0),
|
|
1741
|
+
light_brightness=1.622, ambient_light=0.3, shading_strength=0.7,
|
|
1742
|
+
volume_scale=(1.0, 1.0, 1.0), program=None, **kwargs):
|
|
1743
|
+
# Program bound, uniforms set. volume_lin is the SAME texture as volume
|
|
1744
|
+
# on its own unit; a GL sampler object forces LINEAR filtering on that
|
|
1745
|
+
# unit so the shading reads a smooth field, while the core march keeps
|
|
1746
|
+
# the user's nearest/linear choice (filtering is texture-object state -
|
|
1747
|
+
# a bound sampler object is the rare GL mechanism that overrides it
|
|
1748
|
+
# per unit).
|
|
1749
|
+
def create():
|
|
1750
|
+
s = int(gl.glGenSamplers(1))
|
|
1751
|
+
gl.glSamplerParameteri(s, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
|
1752
|
+
gl.glSamplerParameteri(s, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
|
1753
|
+
for w in (gl.GL_TEXTURE_WRAP_S, gl.GL_TEXTURE_WRAP_T,
|
|
1754
|
+
gl.GL_TEXTURE_WRAP_R):
|
|
1755
|
+
gl.glSamplerParameteri(s, w, gl.GL_CLAMP_TO_EDGE) # = texture3d's
|
|
1756
|
+
return s
|
|
1757
|
+
sampler = gl_state.get("volume_lin_sampler", create,
|
|
1758
|
+
lambda v: gl.glDeleteSamplers(1, [int(v)]))
|
|
1759
|
+
unit = -1
|
|
1760
|
+
loc = gl.glGetUniformLocation(program, "volume_lin")
|
|
1761
|
+
if loc >= 0:
|
|
1762
|
+
buf = np.zeros(1, np.int32)
|
|
1763
|
+
gl.glGetUniformiv(program, loc, buf)
|
|
1764
|
+
unit = int(buf[0])
|
|
1765
|
+
gl.glBindSampler(unit, sampler)
|
|
1766
|
+
gl.glBindVertexArray(gl_state.vao("fs_triangle"))
|
|
1767
|
+
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
|
1768
|
+
if unit >= 0:
|
|
1769
|
+
gl.glBindSampler(unit, 0) # sampler bindings outlive the draw call
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
def _cuda_march_ready():
|
|
1773
|
+
try:
|
|
1774
|
+
from meltygui.core.graphics.cuda_kernel_core import available
|
|
1775
|
+
return available()
|
|
1776
|
+
except Exception:
|
|
1777
|
+
return False
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _cuda_render(gl_state, cv, width, height, voxel_state: VoxelState, lut="jet", shade=None,
|
|
1781
|
+
lut_texture=None, **cam):
|
|
1782
|
+
"""Run the CUDA raymarcher over `cv` (CudaVolumeView) at width×height
|
|
1783
|
+
and return a display-GPU RGBA16F GLTexture holding the premultiplied
|
|
1784
|
+
LINEAR image (HDR headroom and P3 negatives intact, hdr_color.py) — or
|
|
1785
|
+
None (error recorded in voxel_state.cuda_error, drawn as status).
|
|
1786
|
+
The shared LUT and per-view output image live on the TENSOR's device;
|
|
1787
|
+
a pinned host buffer carries the image over, and `cuda_image`
|
|
1788
|
+
is the GL texture it lands in (all re-made only when size/device/LUT
|
|
1789
|
+
change)."""
|
|
1790
|
+
import torch
|
|
1791
|
+
from meltygui.view import voxel_cuda_view
|
|
1792
|
+
dev = cv.view.device
|
|
1793
|
+
W, H = int(width), int(height)
|
|
1794
|
+
try:
|
|
1795
|
+
out = gl_state.get("cuda_out",
|
|
1796
|
+
lambda: torch.empty(H, W, 4, dtype=torch.float16, device=dev),
|
|
1797
|
+
deps=(W, H, str(dev)))
|
|
1798
|
+
if lut_texture is None:
|
|
1799
|
+
from meltygui.model.lut_model import LutTexture, make_luts, lut_values
|
|
1800
|
+
lut_texture = gl_state.get(('cuda_lut_proxy', str(lut)),
|
|
1801
|
+
lambda: LutTexture(lut_values(make_luts(), lut)))
|
|
1802
|
+
lut_t = lut_texture.cuda(dev)
|
|
1803
|
+
# shading params ride one small device array, re-uploaded only when
|
|
1804
|
+
# a value changes (deps = the values themselves)
|
|
1805
|
+
shade_list = list(shade) if shade is not None else voxel_cuda_view.shade_params()
|
|
1806
|
+
shade_t = gl_state.get("cuda_shade",
|
|
1807
|
+
lambda: torch.tensor(shade_list, dtype=torch.float32, device=dev),
|
|
1808
|
+
deps=(tuple(shade_list), str(dev)))
|
|
1809
|
+
# Shading mip: baked once per volume version (one full volume read),
|
|
1810
|
+
# then every shading tap reads the few-MB dense copy instead of the
|
|
1811
|
+
# strided source - keeping shading cost independent of tensor size.
|
|
1812
|
+
_transfer = (float(cam["threshold"]), float(cam["density"]),
|
|
1813
|
+
float(cam["brightness"]), float(cam["contrast"]),
|
|
1814
|
+
bool(cam["centered"]))
|
|
1815
|
+
# The (j, k) mip is the colour march's TRAVERSAL grid now (two-level
|
|
1816
|
+
# DDA skips empty cells through it) as well as self-shading's light
|
|
1817
|
+
# field; it bakes OPACITY under the current transfer, so the key
|
|
1818
|
+
# includes it. Always baked on the cuda path.
|
|
1819
|
+
mip = gl_state.get(
|
|
1820
|
+
"cuda_mip",
|
|
1821
|
+
lambda: voxel_cuda_view.build_mip(
|
|
1822
|
+
cv.view, display_shape=cv.shape, nf=cv.nf, norm=cv.norm,
|
|
1823
|
+
threshold=_transfer[0], density=_transfer[1],
|
|
1824
|
+
brightness=_transfer[2], contrast=_transfer[3],
|
|
1825
|
+
centered=_transfer[4]),
|
|
1826
|
+
deps=(cv._vol_key, cv.shape, str(dev), _transfer))
|
|
1827
|
+
# Floor map is a BAKED full-res map - the plane's shadow depends on
|
|
1828
|
+
# the volume/light/transfer, never the camera, so it re-bakes only
|
|
1829
|
+
# when those change (slice slider, tensor version, light or
|
|
1830
|
+
# brightness edit), and orbiting reads it for free. Softness happens
|
|
1831
|
+
# live (the blur is map taps at render time).
|
|
1832
|
+
floor_map, floor_R = None, (0.0, 0.0)
|
|
1833
|
+
if shade_list[0] > 0.5 and shade_list[7] > 0.5: # draw_floor
|
|
1834
|
+
_light = (tuple(shade_list[9:12]), float(shade_list[6])) # pos, side
|
|
1835
|
+
vsc = cam["volume_scale"]
|
|
1836
|
+
floor_R = voxel_cuda_view.floor_map_extent(vsc)
|
|
1837
|
+
floor_map = gl_state.get(
|
|
1838
|
+
"cuda_floor",
|
|
1839
|
+
lambda: voxel_cuda_view.build_floor_map(
|
|
1840
|
+
cv.view, display_shape=cv.shape, volume_scale=vsc,
|
|
1841
|
+
nf=cv.nf, norm=cv.norm,
|
|
1842
|
+
threshold=_transfer[0], density=_transfer[1],
|
|
1843
|
+
brightness=_transfer[2], contrast=_transfer[3],
|
|
1844
|
+
centered=_transfer[4],
|
|
1845
|
+
light_pos=_light[0], plane_side=_light[1]),
|
|
1846
|
+
deps=(cv._vol_key, cv.shape, str(dev), _transfer, _light,
|
|
1847
|
+
tuple(round(float(v), 5) for v in vsc)))
|
|
1848
|
+
voxel_cuda_view.march(cv.view, out, lut_t, display_shape=cv.shape, nf=cv.nf,
|
|
1849
|
+
norm=cv.norm, aspect=W / H, shade=shade_t, mip=mip,
|
|
1850
|
+
floor_map=floor_map, floor_extent=floor_R, **cam)
|
|
1851
|
+
img = _upload_cuda_image(gl_state, out)
|
|
1852
|
+
voxel_state.cuda_error = None
|
|
1853
|
+
return img
|
|
1854
|
+
except Exception as e:
|
|
1855
|
+
msg = f"cuda_march failed: {e}"
|
|
1856
|
+
if msg != voxel_state.cuda_error:
|
|
1857
|
+
print(f"[voxels] {msg}")
|
|
1858
|
+
print_stack_trace()
|
|
1859
|
+
voxel_state.cuda_error = msg
|
|
1860
|
+
return None
|