Merge pull request #10 from ToontownSuperForeverMVP/feature/outdoor-lighting-and-procedural-quests
Graphics & Quests: Port updated OutdoorLighting system and procedural taskline
This commit is contained in:
commit
3dc99bc40c
63
GEMINI.md
63
GEMINI.md
|
|
@ -6,9 +6,9 @@ This repository contains **Toontown Super Forever MVP** (TOONTOWN-BEST-THE-NEW-)
|
|||
|
||||
# Codebase Architecture & Structure
|
||||
- **`toontown/`**: Main Toontown client logic, including game states, hoods, quest systems, shaders, GUI, and base files.
|
||||
- `toontown/hood/`: Outdoor lighting and procedural sky features.
|
||||
- `toontown/hood/`: Outdoor lighting (`OutdoorLighting.py`) and procedural sky (`ProceduralSky.py`) features.
|
||||
- `toontown/launcher/`: Local server management, launcher routines, user options.
|
||||
- `toontown/quest/`: Quest automation, `AutoerManager`, `TaskAutoer`.
|
||||
- `toontown/quest/`: Quest system (`Quests.py`, `LegacyQuestDict.py`, `QuestParser.py`), `AutoerManager`, `TaskAutoer`.
|
||||
- `toontown/safezone/`: Crane sandboxes (`DistributedTTCCraneSandbox`, AI variant).
|
||||
- `toontown/shaders/`: Custom visual shaders (water, sky, sunrays).
|
||||
- `toontown/shtiker/`: Custom Shtiker Book pages (`AutoerPage`).
|
||||
|
|
@ -19,21 +19,41 @@ This repository contains **Toontown Super Forever MVP** (TOONTOWN-BEST-THE-NEW-)
|
|||
- **`tools/`**: Development, maintenance, and asset management scripts.
|
||||
- **`win32/`, `linux/`, `darwin/`**: Engine launcher scripts and platform-specific binaries/tools.
|
||||
|
||||
# Git Status Analysis
|
||||
- **Current Working Branch**: `feature/modern-loading-system`
|
||||
- **Tracked & Staged Features**:
|
||||
- Full Modern Loading System (`ModernLoadingScreen.py`, `LoadingAssetPreview.py`, `ZonePrefetchCatalog.py`)
|
||||
- Integration with `ToontownLoader.py`, `ToontownLoadingScreen.py`, and `AssetCache.py`
|
||||
# Porting Plan: OutdoorLighting System & Procedural Taskline/Quests
|
||||
|
||||
# Open Pull Requests
|
||||
- **PR #9**: `UI & Performance: Full modern loading system with 3D asset preview and async prefetching` (`feature/modern-loading-system`)
|
||||
- **PR #7**: `Core: Codebase fixes, Astron compatibility, and widescreen offsets` (`feature/core-codebase-fixes`)
|
||||
- **PR #6**: `Automation: Add bot scripts, quest autoers, and custom shtiker page` (`feature/automation-bots`)
|
||||
- **PR #5**: `Graphics: Custom shaders (water, sky, sunrays) and outdoor lighting` (`feature/graphics-shaders`)
|
||||
- **PR #4**: `Performance: Add animation interpolation, async prefetch, and event flood prevention` (`feature/performance-optimizations`)
|
||||
- **PR #3**: `GUI: Add Modern Loading Screen and Asset Preview Overlay` (`feature/modern-loading-screen`)
|
||||
- **PR #2**: `Launcher: Improve Windows startup scripts and add LocalServerManager` (`feature/launcher-and-servers`)
|
||||
- **PR #1**: `Update from task f885637e-e2a0-4db6-b2cf-e8297682242a` (`optimizing-guis-for-widescreen-2242a`)
|
||||
Source: `C:\Users\Shadow\Desktop\scfo\supercfo`
|
||||
Target: `C:\Users\Shadow\Desktop\ttbtn`
|
||||
|
||||
## Phase 1: Fully Updated OutdoorLighting System Port
|
||||
- **Source Modules**:
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\OutdoorLighting.py` (212 KB full engine)
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\ProceduralSky.py` (19.7 KB procedural sky)
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\shaders\` (`sky.vert/frag.glsl`, `sunrays.vert/frag.glsl`, `water.vert/frag.glsl`)
|
||||
- **Key Enhancements to Integrate into `toontown/hood/OutdoorLighting.py` & `toontown/hood/ProceduralSky.py`**:
|
||||
- Multithreaded Cull/Draw render-state safety (`_supportsBasicShaders()` GSG query & thread guards).
|
||||
- Dynamic shadow map depth-pass preservation (`ColorWriteAttrib` composition via `addAttrib()`).
|
||||
- `DepthOffsetAttrib` non-destructive application preventing Z-fighting on static geometry.
|
||||
- Soft light rig destruction preserving `_prevShadowCasterState` to eliminate day/night cycle rebuild blinks.
|
||||
- In-place video settings updates without freezing or window property deadlocks.
|
||||
- Sync GLSL shader assets to `toontown/shaders/`.
|
||||
|
||||
## Phase 2: Procedural Taskline & Quests System Port
|
||||
- **Source Modules**:
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\toontown\quest\LegacyQuestDict.py` (15,743 lines of procedural quest chains)
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\toontown\quest\Quests.py` (Updated quest management & reward dict generators)
|
||||
- `C:\Users\Shadow\Desktop\scfo\supercfo\toontown\quest\QuestParser.py` (Dialogue script tokenizer/parser)
|
||||
- **Key Enhancements to Integrate into `toontown/quest/`**:
|
||||
- Port `LegacyQuestDict.py` into `toontown/quest/LegacyQuestDict.py`.
|
||||
- Add `WANT_LEGACY_QUESTS` flag & dictionary merger in `toontown/quest/Quests.py`.
|
||||
- Update quest tier constants (`TT_TIER`, `DD_TIER`, `DG_TIER`, `MM_TIER`, `BR_TIER`, `DL_TIER`, `LAWBOT_HQ_TIER`, `BOSSBOT_HQ_TIER`, `ELDER_TIER`).
|
||||
- Add `questExists` compatibility helper for AI call sites.
|
||||
- Integrate `QuestParser.py` for tokenized dialogue scripts.
|
||||
- Ensure compatibility with `TaskAutoer.py` and `AutoerManager.py` quest automation engines.
|
||||
|
||||
## Phase 3: Integration & Verification
|
||||
- Test client startup with `OutdoorLighting` and `ProceduralSky` enabled.
|
||||
- Verify zone switching (TTC, DD, DG, MM, BR, DL, Cog HQs) under multi-threaded rendering mode.
|
||||
- Verify quest assignment and completion in AI server repository (`QuestManagerAI.py`).
|
||||
|
||||
# Completed Work
|
||||
- [x] Implemented `ModernLoadingScreen.py`: Full-screen launcher/game load screen with animated gradient background, pulsing lighting, progress bar, asset captions, tip dict support, and smooth outro fade sequence.
|
||||
|
|
@ -41,9 +61,14 @@ This repository contains **Toontown Super Forever MVP** (TOONTOWN-BEST-THE-NEW-)
|
|||
- [x] Implemented `ZonePrefetchCatalog.py`: Catalog mapping all hood zone IDs (TTC, DD, DG, MML, TB, DD, Speedway, Cog HQs, Estate) with async background preloading (`ZonePrefetchManager`).
|
||||
- [x] Integrated `ToontownLoader.py` & `ToontownLoadingScreen.py`: Automatic asset callback forwarding, heartbeat keepalive, and fallback support.
|
||||
- [x] Expanded `AssetCache.py`: Instant preloading of common Phase 3, 3.5, 4, 5 GUI models, chatboxes, shadows, and logos.
|
||||
- [x] Submitted Pull Request #9 to GitHub repository (`feature/modern-loading-system`).
|
||||
- [x] Ported updated `OutdoorLighting.py` (212 KB engine) and `ProceduralSky.py` into `toontown/hood/` with multi-threading guards and GSG depth-pass safety.
|
||||
- [x] Ported GLSL shaders (`sky`, `sunrays`, `water`) into `toontown/shaders/` and added dynamic `_SHADER_DIR` resolution.
|
||||
- [x] Ported `LegacyQuestDict.py` (15,29 procedural quest chains) and updated `toontown/quest/Quests.py` with `WANT_LEGACY_QUESTS` flag and `questExists()` helper.
|
||||
- [x] Ported `QuestParser.py` into `toontown/quest/` and integrated `libotp` nametag/speech library dependencies.
|
||||
- [x] Submitted Pull Request #9 (`feature/modern-loading-system`) and Pull Request #10 (`feature/outdoor-lighting-and-procedural-quests`) to GitHub repository.
|
||||
|
||||
# TODO List for Future AI Instances
|
||||
- [ ] Merge and review PR #9 into `develop`.
|
||||
- [ ] Merge and review PR #9 and PR #10 into `develop`.
|
||||
- [ ] Verify `TaskAutoer.py` quest automation compatibility with the procedural quest dict during live server sessions.
|
||||
- [ ] Validate server stability with Astron local launcher under heavy zone switching.
|
||||
- [ ] Test full client load times across low-end and high-end configurations.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
from .nametag import NametagGlobals
|
||||
from .nametag.ChatBalloon import ChatBalloon
|
||||
from .nametag.ClickablePopup import ClickablePopup
|
||||
from .nametag.MarginManager import MarginManager
|
||||
from .nametag.MarginPopup import MarginPopup
|
||||
from .nametag.Nametag import Nametag
|
||||
from .nametag.Nametag2d import Nametag2d
|
||||
from .nametag.Nametag3d import Nametag3d
|
||||
from .nametag.NametagFloat2d import NametagFloat2d
|
||||
from .nametag.NametagFloat3d import NametagFloat3d
|
||||
from .nametag.NametagGroup import NametagGroup
|
||||
from .nametag.WhisperPopup import WhisperPopup
|
||||
from .nametag._constants import *
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
from direct.directnotify import DirectNotifyGlobal
|
||||
from panda3d.core import *
|
||||
|
||||
from . import NametagGlobals
|
||||
|
||||
|
||||
class ChatBalloon:
|
||||
notify = DirectNotifyGlobal.directNotify.newCategory('ChatBalloon')
|
||||
|
||||
def __init__(self, node=None):
|
||||
self.m_copy_node = None
|
||||
self.m_top_node = None
|
||||
self.m_top_mat = None
|
||||
self.m_middle_node = None
|
||||
self.m_middle_mat = None
|
||||
self.m_bottom_node = None
|
||||
self.m_bottom_mat = None
|
||||
|
||||
self.m_hscale = 0
|
||||
self.m_text_height = 0
|
||||
self.m_text_frame = Vec4(0)
|
||||
|
||||
self.scan(node)
|
||||
|
||||
@staticmethod
|
||||
def find_geom_node(node):
|
||||
if node.isGeomNode():
|
||||
return node
|
||||
|
||||
for i in range(node.getNumChildren()):
|
||||
n = ChatBalloon.find_geom_node(node.getChild(i))
|
||||
if n:
|
||||
return n
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_middle_geom(node):
|
||||
if not node.getNumChildren():
|
||||
return None
|
||||
|
||||
child = None
|
||||
for i in range(node.getNumChildren()):
|
||||
child = node.getChild(i)
|
||||
if child.getName() == 'middle':
|
||||
return n
|
||||
|
||||
n = ChatBalloon.find_middle_geom(child)
|
||||
if n:
|
||||
return n
|
||||
|
||||
return ChatBalloon.find_geom_node(child)
|
||||
|
||||
def scan(self, node):
|
||||
if node.getName() == 'chatBalloon':
|
||||
return self.scan_balloon(node)
|
||||
|
||||
for i in range(node.getNumChildren()):
|
||||
if self.scan(node.getChild(i)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def scan_balloon(self, node):
|
||||
self.m_copy_node = node.copySubgraph()
|
||||
|
||||
for i in range(node.getNumChildren()):
|
||||
child = node.getChild(i)
|
||||
if child.getName() == 'top':
|
||||
self.m_top_node = child
|
||||
self.m_top_mat = child.getTransform().getMat()
|
||||
|
||||
elif child.getName() == 'middle':
|
||||
self.m_middle_node = child
|
||||
self.m_middle_mat = child.getTransform().getMat()
|
||||
|
||||
elif child.getName() == 'bottom':
|
||||
self.m_bottom_node = child
|
||||
self.m_bottom_mat = child.getTransform().getMat()
|
||||
|
||||
if self.m_top_node and self.m_middle_node and self.m_bottom_node:
|
||||
return True
|
||||
|
||||
else:
|
||||
self.notify.warning('ChatBalloon geometry does not include top, middle, and bottom nodes.')
|
||||
return False
|
||||
|
||||
def generate(self, text, font, wordwrap, text_color, balloon_color, for_3d,
|
||||
has_draw_order, draw_order, page_button, space_for_button,
|
||||
reversed, new_button): # new_button is a pointer, let's use a list hack here
|
||||
chat_node = PandaNode('chat')
|
||||
chat_node.setAttrib(CullFaceAttrib.make(0))
|
||||
text_node = NametagGlobals.getTextNode()
|
||||
text_node.setFont(font)
|
||||
text_node.setWordwrap(wordwrap)
|
||||
text_node.setAlign(TextNode.ALeft)
|
||||
text_node.setText(text)
|
||||
|
||||
v116 = NametagGlobals._balloon_text_origin[0]
|
||||
if reversed:
|
||||
v116 = v116 + 9.0
|
||||
|
||||
v27 = (text_node.getRight() - text_node.getLeft()) * 0.11111111
|
||||
self.m_hscale = v27
|
||||
|
||||
if v27 < 0.25:
|
||||
self.m_hscale = 0.25
|
||||
text_node.setAlign(TextNode.ACenter)
|
||||
|
||||
v29 = v116
|
||||
if not reversed:
|
||||
v116 = v29 + 4.5
|
||||
|
||||
else:
|
||||
v116 = v29 - 4.5
|
||||
|
||||
elif reversed:
|
||||
self.m_hscale = -self.m_hscale
|
||||
|
||||
self.m_text_frame = text_node.getCardActual()
|
||||
_space = 0.2 if space_for_button else 0.0
|
||||
num_rows = max(1, text_node.getNumRows())
|
||||
_num_rows = num_rows
|
||||
line_h = text_node.getFont().getLineHeight()
|
||||
|
||||
num_rows_minus_1 = num_rows - 1
|
||||
subgraph_copy_mat = Mat4(self.m_hscale, 0, 0, 0,
|
||||
0, 1.0, 0, 0,
|
||||
0, 0, 1.0, 0,
|
||||
0, 0, 0, 1.0)
|
||||
text_h = _num_rows * line_h + _space
|
||||
self.m_text_height = text_h
|
||||
|
||||
v132 = num_rows_minus_1 * line_h + _space
|
||||
|
||||
middle_mat = Mat4(1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, text_h, 0,
|
||||
0, 0, 0, 1) * self.m_middle_mat
|
||||
top_mat = Mat4(1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, text_h - 1.0, 1) * self.m_top_mat
|
||||
|
||||
v137 = v116 * self.m_hscale
|
||||
v138 = 0.0
|
||||
v139 = NametagGlobals._balloon_text_origin[2] + v132 + 0.2
|
||||
|
||||
self.m_text_frame += Vec4(v137, v137, v139, v139)
|
||||
|
||||
'''
|
||||
Correct code:
|
||||
Python won't let us edit this transform, we'll have to copy it all
|
||||
|
||||
if self.m_top_node:
|
||||
self.m_top_node.setTransform(TransformState.makeMat(top_mat))
|
||||
|
||||
if self.m_middle_node:
|
||||
self.m_middle_node.setTransform(TransformState.makeMat(middle_mat))
|
||||
|
||||
subgraph_copy = self.m_copy_node.copySubgraph()
|
||||
chat_node.addChild(subgraph_copy)
|
||||
subgraph_copy.setTransform(TransformState.makeMat(subgraph_copy_mat))
|
||||
'''
|
||||
|
||||
# BEGIN PYTHON CODE
|
||||
subgraph_copy = self.m_copy_node.copySubgraph()
|
||||
NodePath.anyPath(subgraph_copy).find('**/top').node().setTransform(TransformState.makeMat(top_mat))
|
||||
NodePath.anyPath(subgraph_copy).find('**/middle').node().setTransform(TransformState.makeMat(middle_mat))
|
||||
|
||||
chat_node.addChild(subgraph_copy)
|
||||
subgraph_copy.setTransform(TransformState.makeMat(subgraph_copy_mat))
|
||||
# END PYTHON CODE
|
||||
|
||||
if has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
subgraph_copy.setAttrib(CullBinAttrib.make(bin, draw_order))
|
||||
|
||||
subgraph_copy.setAttrib(ColorAttrib.makeFlat(balloon_color))
|
||||
if balloon_color[3] != 1.0:
|
||||
subgraph_copy.setAttrib(TransparencyAttrib.make(1))
|
||||
|
||||
reducer = SceneGraphReducer()
|
||||
reducer.applyAttribs(subgraph_copy)
|
||||
reducer.flatten(chat_node, -1)
|
||||
|
||||
generated_text = text_node.generate()
|
||||
if for_3d:
|
||||
v86 = ChatBalloon.find_middle_geom(chat_node)
|
||||
if not v86:
|
||||
v86 = ChatBalloon.find_geom_node(chat_node)
|
||||
|
||||
v86.setEffect(DecalEffect.make())
|
||||
|
||||
else:
|
||||
v86 = chat_node
|
||||
if has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
generated_text.setAttrib(CullBinAttrib.make(bin, draw_order + 1))
|
||||
|
||||
np = NodePath.anyPath(v86)
|
||||
v144 = np.attachNewNode(generated_text)
|
||||
v144.setPos((v137, v138, v139))
|
||||
v144.setColor(text_color)
|
||||
v144.setY(-0.01) # Panda3D 1.10 hack to prevent z-fighting.
|
||||
if text_color[3] != 1.0:
|
||||
v144.setTransparency(1)
|
||||
|
||||
if page_button:
|
||||
v116 = ModelNode('button')
|
||||
new_button[0] = np.attachNewNode(v116)
|
||||
button_copy = page_button.copyTo(new_button[0])
|
||||
if reversed:
|
||||
button_copy.setPos(self.m_hscale * 1.7, 0, 1.8)
|
||||
|
||||
else:
|
||||
button_copy.setPos(self.m_hscale * 9.0, 0, 1.8)
|
||||
|
||||
button_copy.setScale(8.0, 8.0, 8.0)
|
||||
button_copy.setY(-0.01) # Panda3D 1.10 hack to prevent z-fighting.
|
||||
|
||||
reducer = SceneGraphReducer()
|
||||
reducer.applyAttribs(generated_text)
|
||||
reducer.flatten(chat_node, 1)
|
||||
|
||||
return chat_node
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
from direct.showbase.DirectObject import DirectObject
|
||||
from panda3d.core import *
|
||||
|
||||
from . import NametagGlobals
|
||||
from direct.gui.DirectGuiGlobals import PGButton
|
||||
|
||||
|
||||
class PopupMouseWatcherRegion(MouseWatcherRegion):
|
||||
"""
|
||||
This is an ultra hacky class!
|
||||
The correct implementation of PopupMouseWatcherRegion cannot be done in Python.
|
||||
This also assumes that m_mouse_watcher is NametagGlobals::_mouse_watcher.
|
||||
"""
|
||||
|
||||
class _Param:
|
||||
def __init__(self, outside=False):
|
||||
self.outside = outside
|
||||
|
||||
def isOutside(self):
|
||||
return self.outside
|
||||
|
||||
def getButton(self):
|
||||
return MouseButton.one()
|
||||
|
||||
MOUSE_WATCHER_SETUP = False
|
||||
|
||||
def __init__(self, obj, name, frame):
|
||||
MouseWatcherRegion.__init__(self, '%s-%s' % (name, id(self)), frame)
|
||||
|
||||
self.obj = obj
|
||||
self.__inside = False
|
||||
|
||||
if not self.MOUSE_WATCHER_SETUP:
|
||||
NametagGlobals._mouse_watcher.setEnterPattern('mouse-enter-%r')
|
||||
NametagGlobals._mouse_watcher.setLeavePattern('mouse-leave-%r')
|
||||
NametagGlobals._mouse_watcher.setButtonDownPattern('button-down-%r')
|
||||
NametagGlobals._mouse_watcher.setButtonUpPattern('button-up-%r')
|
||||
self.MOUSE_WATCHER_SETUP = True
|
||||
|
||||
self.slaveObject = DirectObject()
|
||||
self.slaveObject.accept(self.__getEvent(NametagGlobals._mouse_watcher.getEnterPattern()), self.__mouseEnter)
|
||||
self.slaveObject.accept(self.__getEvent(NametagGlobals._mouse_watcher.getLeavePattern()), self.__mouseLeave)
|
||||
self.slaveObject.accept(self.__getEvent(NametagGlobals._mouse_watcher.getButtonDownPattern()),
|
||||
self.__buttonDown)
|
||||
self.slaveObject.accept(self.__getEvent(NametagGlobals._mouse_watcher.getButtonUpPattern()), self.__buttonUp)
|
||||
|
||||
def __mouseEnter(self, region, extra):
|
||||
self.__inside = True
|
||||
self.obj.enterRegion(None)
|
||||
|
||||
def __mouseLeave(self, region, extra):
|
||||
self.__inside = False
|
||||
self.obj.exitRegion(None)
|
||||
|
||||
def __buttonDown(self, region, button):
|
||||
if button == 'mouse1':
|
||||
self.obj.press(PopupMouseWatcherRegion._Param())
|
||||
|
||||
def __buttonUp(self, region, button):
|
||||
if button == 'mouse1':
|
||||
self.obj.release(PopupMouseWatcherRegion._Param(not self.__inside))
|
||||
|
||||
def __getEvent(self, pattern):
|
||||
return pattern.replace('%r', self.getName())
|
||||
|
||||
|
||||
class ClickablePopup:
|
||||
def __init__(self):
|
||||
self.m_state = PGButton.SReady
|
||||
|
||||
def setState(self, state):
|
||||
if state != self.m_state:
|
||||
self.m_state = state
|
||||
self.updateContents()
|
||||
|
||||
def enterRegion(self, arg):
|
||||
if NametagGlobals._rollover_sound:
|
||||
NametagGlobals._rollover_sound.play()
|
||||
|
||||
self.setState(PGButton.SRollover)
|
||||
|
||||
def exitRegion(self, arg):
|
||||
self.setState(PGButton.SReady)
|
||||
|
||||
def press(self, arg):
|
||||
if arg.getButton() == MouseButton.one():
|
||||
if NametagGlobals._click_sound:
|
||||
NametagGlobals._click_sound.play()
|
||||
self.setState(PGButton.SDepressed)
|
||||
|
||||
def release(self, arg):
|
||||
if arg.getButton() == MouseButton.one():
|
||||
if arg.isOutside():
|
||||
self.setState(PGButton.SReady)
|
||||
|
||||
else:
|
||||
self.setState(PGButton.SRollover)
|
||||
self.click()
|
||||
|
||||
def _createRegion(self, frame):
|
||||
name = '%s-%s' % (self.__class__.__name__, self.getName())
|
||||
return PopupMouseWatcherRegion(self, name, frame)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from panda3d.core import Vec4
|
||||
from direct.gui.DirectGuiGlobals import PGButton
|
||||
|
||||
class ColorProfile:
|
||||
def __init__(self, clickable=Vec4(0, 0, 0, 1), hover=Vec4(0, 0, 0, 1), pressed=Vec4(0, 0, 0, 1), disabled=Vec4(0, 0, 0, 1)):
|
||||
self.clickable = Vec4(clickable)
|
||||
self.hover = Vec4(hover)
|
||||
self.pressed = Vec4(pressed)
|
||||
self.disabled = Vec4(disabled)
|
||||
|
||||
def copy(self):
|
||||
return ColorProfile(self.clickable, self.hover, self.pressed, self.disabled)
|
||||
|
||||
def getColorFromState(self, state):
|
||||
if state == PGButton.SReady:
|
||||
return Vec4(self.clickable)
|
||||
elif state == PGButton.SDepressed:
|
||||
return Vec4(self.pressed)
|
||||
elif state == PGButton.SRollover:
|
||||
return Vec4(self.hover)
|
||||
elif state == PGButton.SInactive:
|
||||
return Vec4(self.disabled)
|
||||
return Vec4(self.clickable)
|
||||
|
||||
GRAY = ColorProfile(Vec4(0.5, 0.5, 0.5, 1), Vec4(0.6, 0.6, 0.6, 1), Vec4(0.4, 0.4, 0.4, 1), Vec4(0.3, 0.3, 0.3, 1))
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
from panda3d.core import *
|
||||
|
||||
|
||||
class PopupHandle:
|
||||
def __init__(self, popup):
|
||||
self.m_popup = popup # 12
|
||||
self.m_cell = -1 # 16
|
||||
self.m_wants_visible = False # 20
|
||||
self.m_score = 0 # 24
|
||||
self.m_objcode = id(self) # 28
|
||||
popup.setObjectCode(self.m_objcode)
|
||||
|
||||
|
||||
class MarginCell:
|
||||
def __init__(self):
|
||||
self.m_mat = Mat4() # 0
|
||||
self.m_cell_width = 0 # 64
|
||||
self.m_popup = None # 68
|
||||
self.m_np = None # 72
|
||||
self.m_visible = False # 84
|
||||
self.m_objcode = 0 # 88
|
||||
self.m_time = 0 # 96
|
||||
|
||||
|
||||
class MarginManager(PandaNode):
|
||||
def __init__(self):
|
||||
PandaNode.__init__(self, 'popups')
|
||||
|
||||
# self.cbNode = CallbackNode(self.getName() + '-cbNode')
|
||||
# self.cbNode.setCullCallback(PythonCallbackObject(self.cullCallback))
|
||||
# self.addChild(self.cbNode)
|
||||
|
||||
self.m_cells = []
|
||||
self.m_popups = {} # MarginPopup*: PopupHandle
|
||||
self.m_code_map = {} # code: MarginPopup*
|
||||
self.m_num_available = 0
|
||||
|
||||
def addGridCell(self, a2, a3, a4, a5, a6, a7, newParent, newPos):
|
||||
v7 = (a5 - a4) * 0.16666667
|
||||
v8 = (a7 - a6) * 0.16666667
|
||||
v15 = v7 * a2 + a4
|
||||
v9 = a3 * v8 + a6
|
||||
v10 = v9 + v8 - 0.01
|
||||
v11 = v9 + 0.01
|
||||
v12 = v15 + v7 - 0.01
|
||||
v13 = v15 + 0.01
|
||||
return self.addCell(v13, v12, v11, v10, newParent, newPos)
|
||||
|
||||
def addCell(self, left, right, bottom, top, newParent, newPos):
|
||||
v5 = (top - bottom) * 0.5
|
||||
v19 = Vec3(v5, 0, 0)
|
||||
scale = Vec3(v5, v5, v5)
|
||||
shear = Vec3(0, 0, 0)
|
||||
trans = Vec3((left + right) * 0.5, 0, (bottom + top) * 0.5)
|
||||
|
||||
v18 = len(self.m_cells)
|
||||
v9 = MarginCell()
|
||||
self.m_cells.append((v9, newParent, newPos))
|
||||
v9.m_available = True
|
||||
|
||||
mat3 = Mat3()
|
||||
composeMatrix(mat3, scale, shear, Vec3(0, 0, 0), 0)
|
||||
v9.m_mat = Mat4(mat3, trans)
|
||||
|
||||
v9.m_cell_width = (right - left) * 0.5 / v19[0]
|
||||
v9.m_np = None
|
||||
v9.m_popup = None
|
||||
v9.m_objcode = 0
|
||||
v9.m_time = 0.0
|
||||
|
||||
self.m_num_available += 1
|
||||
return v18
|
||||
|
||||
def setCellAvailable(self, a2, a3):
|
||||
v5 = self.m_cells[a2][0]
|
||||
if v5.m_available:
|
||||
self.m_num_available -= 1
|
||||
|
||||
v5.m_available = a3
|
||||
if v5.m_available:
|
||||
self.m_num_available += 1
|
||||
|
||||
if v5.m_np:
|
||||
self.hide(a2)
|
||||
v5.m_popup = None
|
||||
v5.m_objcode = 0
|
||||
|
||||
def getCellAvailable(self, a2):
|
||||
return self.m_cells[a2][0].m_available
|
||||
|
||||
def cullCallback(self, *args):
|
||||
# Under Cull/Draw threading models, executing update() inside the Cull thread
|
||||
# causes concurrent scene graph modifications (attachNewNode, reparentTo) which
|
||||
# result in fatal C++ assertions (e.g. reparented at nodePath.cxx).
|
||||
# We rely entirely on the App-thread task in ToonBase.py to update margins.
|
||||
pass
|
||||
|
||||
def managePopup(self, a2):
|
||||
a2.setManaged(True)
|
||||
self.m_popups[a2] = PopupHandle(a2)
|
||||
self.m_code_map[a2.getObjectCode()] = a2
|
||||
|
||||
def unmanagePopup(self, a2):
|
||||
v9 = self.m_popups.get(a2)
|
||||
if v9:
|
||||
if v9.m_cell >= 0:
|
||||
self.hide(v9.m_cell)
|
||||
v9.m_cell = -1
|
||||
|
||||
a2.setManaged(False)
|
||||
del self.m_popups[a2]
|
||||
del self.m_code_map[v9.m_objcode]
|
||||
|
||||
def hide(self, a2):
|
||||
cell = self.m_cells[a2][0]
|
||||
cell.m_np.removeNode()
|
||||
cell.m_time = globalClock.getFrameTime()
|
||||
if cell.m_popup:
|
||||
cell.m_popup.setVisible(False)
|
||||
|
||||
def show(self, popup, cell_index):
|
||||
v12 = self.m_cells[cell_index][0]
|
||||
v12.m_popup = popup
|
||||
v12.m_objcode = popup.getObjectCode()
|
||||
v12.m_np = NodePath.anyPath(self).attachNewNode(popup)
|
||||
v12.m_np.setMat(v12.m_mat)
|
||||
v12.m_np.reparentTo(self.m_cells[cell_index][1])
|
||||
v12.m_np.setPos(self.m_cells[cell_index][2])
|
||||
self.m_popups[popup].m_cell = cell_index
|
||||
popup.m_cell_width = v12.m_cell_width
|
||||
popup.setVisible(True)
|
||||
|
||||
def chooseCell(self, a2, a3):
|
||||
now = globalClock.getFrameTime()
|
||||
objcode = a2.getObjectCode()
|
||||
|
||||
for cell in a3:
|
||||
v7 = self.m_cells[cell][0]
|
||||
if (v7.m_popup == a2 or v7.m_objcode == objcode) and (now - v7.m_time) <= 30.0:
|
||||
result = cell
|
||||
break
|
||||
|
||||
else:
|
||||
for cell in a3[::-1][1:]: # Iterate backwards, skip last item
|
||||
v10 = self.m_cells[cell][0]
|
||||
if (not v10.m_popup) or (now - v10.m_time) > 30.0:
|
||||
result = cell
|
||||
break
|
||||
|
||||
else:
|
||||
result = a3[-1]
|
||||
|
||||
a3.remove(result)
|
||||
return result
|
||||
|
||||
def showVisibleNoConflict(self):
|
||||
cells = []
|
||||
for i, cell in enumerate(self.m_cells):
|
||||
if cell[0].m_available and not cell[0].m_np:
|
||||
cells.append(i)
|
||||
|
||||
for handle in self.m_popups.values():
|
||||
v7 = handle.m_popup
|
||||
if handle.m_wants_visible and not v7.isVisible():
|
||||
v8 = self.chooseCell(v7, cells)
|
||||
self.show(v7, v8)
|
||||
|
||||
def showVisibleResolveConflict(self):
|
||||
v4 = []
|
||||
|
||||
for handle in self.m_popups.values():
|
||||
score = 0
|
||||
if handle.m_wants_visible:
|
||||
score = handle.m_score
|
||||
|
||||
v4.append((handle, -score))
|
||||
|
||||
v4 = sorted(v4, key=lambda a: a[-1])
|
||||
for handle in v4[self.m_num_available:]:
|
||||
if handle[0].m_popup.isVisible():
|
||||
self.hide(handle[0].m_cell)
|
||||
handle[0].m_cell = -1
|
||||
|
||||
cells = []
|
||||
for i, cell in enumerate(self.m_cells):
|
||||
if cell[0].m_available and not cell[0].m_np:
|
||||
cells.append(i)
|
||||
|
||||
for handle in v4[:self.m_num_available]:
|
||||
v7 = handle[0].m_popup
|
||||
if handle[0].m_wants_visible and not v7.isVisible():
|
||||
v8 = self.chooseCell(v7, cells)
|
||||
self.show(v7, v8)
|
||||
|
||||
def update(self):
|
||||
num_want_visible = 0
|
||||
|
||||
for handle in list(self.m_popups.values()):
|
||||
popup = handle.m_popup
|
||||
handle.m_wants_visible = popup.considerVisible()
|
||||
if handle.m_wants_visible and handle.m_objcode:
|
||||
handle.m_score = popup.getScore()
|
||||
num_want_visible += 1
|
||||
|
||||
elif popup.isVisible():
|
||||
self.hide(handle.m_cell)
|
||||
handle.m_cell = -1
|
||||
|
||||
if num_want_visible > self.m_num_available:
|
||||
self.showVisibleResolveConflict()
|
||||
|
||||
else:
|
||||
self.showVisibleNoConflict()
|
||||
|
||||
for popup in self.m_popups.keys():
|
||||
popup.frameCallback()
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
from panda3d.core import *
|
||||
|
||||
from . import NametagGlobals
|
||||
|
||||
|
||||
class MarginPopup(PandaNode):
|
||||
def __init__(self):
|
||||
PandaNode.__init__(self, 'MarginPopup')
|
||||
|
||||
self.m_managed = False
|
||||
self.m_visible = False
|
||||
self.m_np = None
|
||||
self.m_cell_width = 1.0
|
||||
self.m_seq = NametagGlobals._margin_prop_seq
|
||||
|
||||
def getCellWidth(self):
|
||||
return self.m_cell_width
|
||||
|
||||
def setManaged(self, value):
|
||||
self.m_managed = value
|
||||
if value:
|
||||
self.m_np = NodePath.anyPath(self)
|
||||
|
||||
else:
|
||||
self.m_np = None
|
||||
|
||||
def isManaged(self):
|
||||
return self.m_managed
|
||||
|
||||
def setVisible(self, value):
|
||||
self.m_visible = value
|
||||
|
||||
def isVisible(self):
|
||||
return self.m_visible
|
||||
|
||||
def getScore(self):
|
||||
return 0.0
|
||||
|
||||
def getObjectCode(self):
|
||||
return 0
|
||||
|
||||
def considerVisible(self):
|
||||
if self.m_seq != NametagGlobals._margin_prop_seq:
|
||||
self.m_seq = NametagGlobals._margin_prop_seq
|
||||
self.updateContents()
|
||||
|
||||
def updateContents(self):
|
||||
pass
|
||||
|
||||
def frameCallback(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
from .ClickablePopup import *
|
||||
from ._constants import *
|
||||
from .ColorProfile import ColorProfile, GRAY
|
||||
|
||||
|
||||
class Nametag(ClickablePopup):
|
||||
CName = 1
|
||||
CSpeech = 2
|
||||
CThought = 4
|
||||
|
||||
def __init__(self, wordwrap):
|
||||
ClickablePopup.__init__(self)
|
||||
|
||||
self.m_avatar = None
|
||||
self.m_ival = None
|
||||
self.m_popup_region = None
|
||||
self.m_seq = 0
|
||||
self.m_mouse_watcher = None
|
||||
self.m_draw_order = 0
|
||||
self.m_has_draw_order = False
|
||||
|
||||
self.m_contents = CFSpeech | CFThought | CFQuicktalker
|
||||
self.m_active = True
|
||||
self.m_field_12 = 0
|
||||
self.m_group = None
|
||||
self.m_wordwrap = wordwrap
|
||||
self.m_has_region = False
|
||||
self.m_ival_name = 'flash-%d' % id(self)
|
||||
self._colorProfile: ColorProfile = GRAY
|
||||
self._useColorProfile: bool = False
|
||||
|
||||
# Optional AP-teams style color profile support.
|
||||
# Some forks call these methods from Nametag2d; provide safe defaults.
|
||||
def usingColorProfile(self) -> bool:
|
||||
return bool(getattr(self, '_useColorProfile', False))
|
||||
|
||||
def setUsingColorProfile(self, use: bool):
|
||||
self._useColorProfile = bool(use)
|
||||
self.updateContents()
|
||||
|
||||
def setColorProfile(self, profile: ColorProfile):
|
||||
self._colorProfile = profile if profile is not None else GRAY
|
||||
self._useColorProfile = True
|
||||
self.updateContents()
|
||||
|
||||
def getColorProfile(self) -> ColorProfile:
|
||||
return getattr(self, '_colorProfile', GRAY)
|
||||
|
||||
def clearAvatar(self):
|
||||
self.m_avatar = None
|
||||
|
||||
def clearDrawOrder(self):
|
||||
self.m_has_draw_order = False
|
||||
self.updateContents()
|
||||
|
||||
def click(self):
|
||||
if self.m_group:
|
||||
self.m_group.click()
|
||||
|
||||
def deactivate(self):
|
||||
if self.m_has_region:
|
||||
if self.m_mouse_watcher:
|
||||
self.m_mouse_watcher.removeRegion(self.m_popup_region)
|
||||
self.m_mouse_watcher = None
|
||||
|
||||
self.m_has_region = None
|
||||
|
||||
self.m_seq = 0
|
||||
|
||||
def determineContents(self):
|
||||
if self.m_group and self.m_group.isManaged():
|
||||
v3 = self.m_contents & self.m_group.getContents()
|
||||
v4 = self.m_group.m_chat_flags
|
||||
|
||||
if v4 & CFSpeech:
|
||||
if v3 & Nametag.CSpeech:
|
||||
return Nametag.CSpeech
|
||||
|
||||
elif v4 & CFThought and v3 & Nametag.CThought:
|
||||
return Nametag.CThought
|
||||
|
||||
if v3 & Nametag.CName and self.m_group.getName() and NametagGlobals._master_nametags_visible:
|
||||
return Nametag.CName
|
||||
|
||||
return 0
|
||||
|
||||
def displayAsActive(self):
|
||||
if not self.m_active:
|
||||
return 0
|
||||
|
||||
if self.m_group:
|
||||
return self.m_group.displayAsActive()
|
||||
|
||||
else:
|
||||
return NametagGlobals._master_nametags_active
|
||||
|
||||
def setAvatar(self, avatar):
|
||||
self.m_avatar = avatar
|
||||
|
||||
def getAvatar(self):
|
||||
return self.m_avatar
|
||||
|
||||
def setChatWordwrap(self, wordwrap):
|
||||
self.m_wordwrap = wordwrap
|
||||
|
||||
def getChatWordwrap(self):
|
||||
return self.m_wordwrap
|
||||
|
||||
def getGroup(self):
|
||||
return self.m_group
|
||||
|
||||
def getState(self):
|
||||
if self.m_group:
|
||||
if not (self.m_active and self.m_group.displayAsActive()):
|
||||
return PGButton.SInactive
|
||||
|
||||
elif not (self.m_active and NametagGlobals._master_nametags_active):
|
||||
return PGButton.SInactive
|
||||
|
||||
return self.m_state
|
||||
|
||||
def hasGroup(self):
|
||||
return self.m_group is not None
|
||||
|
||||
def setActive(self, active):
|
||||
self.m_active = active
|
||||
self.updateContents()
|
||||
|
||||
def isActive(self):
|
||||
return self.m_active
|
||||
|
||||
def isGroupManaged(self):
|
||||
return self.m_group and self.m_group.isManaged()
|
||||
|
||||
def keepRegion(self):
|
||||
if self.m_popup_region:
|
||||
self.m_seq = self.m_group.getRegionSeq()
|
||||
|
||||
def manage(self, manager):
|
||||
self.updateContents()
|
||||
|
||||
def unmanage(self, manager):
|
||||
self.updateContents()
|
||||
self.deactivate()
|
||||
|
||||
def setContents(self, contents):
|
||||
self.m_contents = contents
|
||||
self.updateContents()
|
||||
|
||||
def setDrawOrder(self, draw_order):
|
||||
self.m_draw_order = draw_order
|
||||
self.m_has_draw_order = True
|
||||
self.updateContents()
|
||||
|
||||
def setRegion(self, frame, sort):
|
||||
if self.m_popup_region:
|
||||
self.m_popup_region.setFrame(frame)
|
||||
|
||||
else:
|
||||
self.m_popup_region = self._createRegion(frame)
|
||||
|
||||
self.m_popup_region.setSort(int(sort))
|
||||
self.m_seq = self.m_group.getRegionSeq()
|
||||
|
||||
def startFlash(self, np):
|
||||
self.stopFlash()
|
||||
self.m_ival = Sequence(
|
||||
np.colorInterval(0.5, Vec4(1.0, 1.0, 1.0, 0.5), startColor=Vec4(1.0, 1.0, 1.0, 1.0), blendType='easeOut'),
|
||||
np.colorInterval(0.5, Vec4(1.0, 1.0, 1.0, 1.0), startColor=Vec4(1.0, 1.0, 1.0, 0.5), blendType='easeIn'))
|
||||
self.m_ival.loop()
|
||||
|
||||
def stopFlash(self):
|
||||
if self.m_ival:
|
||||
self.m_ival.finish()
|
||||
self.m_ival = None
|
||||
|
||||
def updateRegion(self, seq):
|
||||
if seq == self.m_seq:
|
||||
is_active = self.displayAsActive()
|
||||
|
||||
else:
|
||||
is_active = False
|
||||
|
||||
if self.m_has_region:
|
||||
if self.m_mouse_watcher != NametagGlobals._mouse_watcher:
|
||||
if self.m_mouse_watcher:
|
||||
self.m_mouse_watcher.removeRegion(self.m_popup_region)
|
||||
|
||||
self.m_has_region = False
|
||||
self.setState(PGButton.SReady)
|
||||
|
||||
if is_active:
|
||||
if (not self.m_has_region) and self.m_popup_region:
|
||||
if self.m_mouse_watcher != NametagGlobals._mouse_watcher:
|
||||
self.m_mouse_watcher = NametagGlobals._mouse_watcher
|
||||
|
||||
if self.m_mouse_watcher:
|
||||
self.m_mouse_watcher.addRegion(self.m_popup_region)
|
||||
|
||||
self.m_has_region = True
|
||||
|
||||
elif self.m_has_region:
|
||||
if self.m_mouse_watcher and self.m_popup_region:
|
||||
self.m_mouse_watcher.removeRegion(self.m_popup_region)
|
||||
|
||||
self.m_has_region = False
|
||||
self.m_mouse_watcher = None
|
||||
self.setState(PGButton.SReady)
|
||||
|
||||
def upcastToPandaNode(self):
|
||||
return self
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
import math
|
||||
|
||||
from panda3d.core import *
|
||||
|
||||
from . import NametagGlobals
|
||||
from .MarginPopup import MarginPopup
|
||||
from .Nametag import Nametag
|
||||
from ._constants import *
|
||||
|
||||
|
||||
class Nametag2d(Nametag, MarginPopup):
|
||||
def __init__(self):
|
||||
Nametag.__init__(self, 8.075)
|
||||
MarginPopup.__init__(self)
|
||||
|
||||
self.m_copied_np = None
|
||||
self.m_attached_np = None
|
||||
self.m_arrow = None
|
||||
self.m_unknown_np = None
|
||||
|
||||
# self.cbNode = CallbackNode(self.getName() + '-cbNode')
|
||||
# self.cbNode.setCullCallback(PythonCallbackObject(self.cullCallback))
|
||||
# self.addChild(self.cbNode)
|
||||
NametagGlobals._active_nametags_2d.add(self)
|
||||
|
||||
self.setName('unnamed')
|
||||
|
||||
self.m_contents = 3
|
||||
self.m_chat_contents = 0
|
||||
self.updateContents()
|
||||
self.m_on = NametagGlobals._master_arrows_on
|
||||
self.m_seq2d = 0
|
||||
|
||||
self.m_trans_vec = Vec3(0, 0, 0)
|
||||
|
||||
def setVisible(self, value):
|
||||
self.m_visible = value
|
||||
self.updateContents()
|
||||
|
||||
def manage(self, manager):
|
||||
self.updateContents()
|
||||
manager.managePopup(self)
|
||||
|
||||
def unmanage(self, manager):
|
||||
Nametag.unmanage(self, manager)
|
||||
manager.unmanagePopup(self)
|
||||
|
||||
def setObjectCode(self, objcode):
|
||||
if self.m_group:
|
||||
self.m_group.setObjectCode(objcode)
|
||||
|
||||
def getObjectCode(self):
|
||||
if self.m_group:
|
||||
return self.m_group.getObjectCode()
|
||||
|
||||
return 0
|
||||
|
||||
def getScore(self):
|
||||
if self.m_group:
|
||||
return 1000 - self.getDistance2()
|
||||
|
||||
return 0
|
||||
|
||||
def getDistance2(self):
|
||||
if self.m_avatar:
|
||||
np = self.m_avatar
|
||||
|
||||
else:
|
||||
np = self.m_group.getAvatar()
|
||||
|
||||
if np.isEmpty():
|
||||
return 0
|
||||
|
||||
return np.getPos(NametagGlobals._toon).lengthSquared()
|
||||
|
||||
def considerVisible(self):
|
||||
from .NametagGroup import NametagGroup
|
||||
|
||||
v2 = 0
|
||||
do_update = True
|
||||
if self.m_on != NametagGlobals._master_arrows_on:
|
||||
self.m_on = NametagGlobals._master_arrows_on
|
||||
v2 = 1
|
||||
|
||||
if self.m_seq2d == NametagGlobals._margin_prop_seq:
|
||||
if not v2:
|
||||
do_update = False
|
||||
|
||||
else:
|
||||
self.m_seq2d = NametagGlobals._margin_prop_seq
|
||||
|
||||
if do_update:
|
||||
self.updateContents()
|
||||
|
||||
if not self.m_chat_contents:
|
||||
return 0
|
||||
|
||||
result = self.m_group.m_nametag3d_flag != 2
|
||||
if NametagGlobals._onscreen_chat_forced and self.m_chat_contents & (Nametag.CSpeech | Nametag.CThought):
|
||||
result = 1
|
||||
|
||||
self.m_group.setNametag3dFlag(0)
|
||||
if result and self.m_group.getColorCode() in (NametagGroup.CCToonBuilding,
|
||||
NametagGroup.CCSuitBuilding,
|
||||
NametagGroup.CCHouseBuilding):
|
||||
return self.getDistance2() < 1600
|
||||
|
||||
return result
|
||||
|
||||
def updateContents(self):
|
||||
self.stopFlash()
|
||||
if self.m_group:
|
||||
self.setName(self.m_group.getName())
|
||||
|
||||
else:
|
||||
self.setName('unnamed')
|
||||
|
||||
if self.m_copied_np:
|
||||
self.m_copied_np.removeNode()
|
||||
|
||||
if self.m_attached_np:
|
||||
self.m_attached_np.removeNode()
|
||||
|
||||
if self.m_arrow:
|
||||
self.m_arrow.removeNode()
|
||||
|
||||
if self.m_unknown_np:
|
||||
self.m_unknown_np.removeNode()
|
||||
|
||||
self.m_chat_contents = self.determineContents()
|
||||
if not NametagGlobals._master_arrows_on:
|
||||
self.m_chat_contents = self.m_chat_contents & ~1
|
||||
|
||||
if self.m_visible and self.isGroupManaged():
|
||||
v10 = self.m_chat_contents
|
||||
if v10 & Nametag.CSpeech:
|
||||
self.generateChat(NametagGlobals._speech_balloon_2d)
|
||||
|
||||
elif v10 & Nametag.CThought:
|
||||
self.generateChat(NametagGlobals._thought_balloon_2d)
|
||||
|
||||
elif v10 & Nametag.CName:
|
||||
self.generateName()
|
||||
|
||||
def frameCallback(self):
|
||||
if self.m_visible and self.m_popup_region:
|
||||
self.m_seq = self.m_group.m_region_seq
|
||||
|
||||
if self.m_group:
|
||||
self.m_group.updateRegions()
|
||||
|
||||
def rotateArrow(self):
|
||||
if not self.m_arrow:
|
||||
return
|
||||
|
||||
if self.m_avatar:
|
||||
np = self.m_avatar
|
||||
|
||||
else:
|
||||
np = self.m_group.getAvatar()
|
||||
|
||||
if not np:
|
||||
return
|
||||
|
||||
relpos = np.getPos(NametagGlobals._camera) - NametagGlobals._toon.getPos(NametagGlobals._camera)
|
||||
hpr = Vec3(0, 0, -math.atan2(relpos[1], relpos[0]) * 180 / math.pi)
|
||||
scale = Vec3(0.5, 0.5, 0.5)
|
||||
shear = Vec3(0, 0, 0)
|
||||
|
||||
temp_mat_3 = Mat3()
|
||||
composeMatrix(temp_mat_3, scale, shear, hpr)
|
||||
arrow_mat = Mat4(temp_mat_3, self.m_trans_vec)
|
||||
self.m_arrow.setMat(arrow_mat)
|
||||
|
||||
def generateName(self):
|
||||
v4 = self.getState()
|
||||
v84 = Vec4(NametagGlobals.getNameFg(self.m_group.getColorCode(), v4))
|
||||
|
||||
# AP Teams Nametag hook. If we have an override for a color profile use it.
|
||||
if self.usingColorProfile():
|
||||
v84 = self.getColorProfile().getColorFromState(self.getState())
|
||||
|
||||
v75 = Vec4(NametagGlobals.getNameBg(self.m_group.getColorCode(), v4))
|
||||
v75[3] = max(v75[3], NametagGlobals._min_2d_alpha)
|
||||
v75[3] = min(v75[3], NametagGlobals._max_2d_alpha)
|
||||
|
||||
v67 = NametagGlobals._card_pad[3] + self.m_group.m_name_frame[3]
|
||||
v68 = self.m_group.m_name_frame[2] - NametagGlobals._card_pad[2]
|
||||
|
||||
wordwrap = self.m_group.getNameWordwrap()
|
||||
v17 = self.m_cell_width / wordwrap * 2.0
|
||||
v66 = 0.333 * (1.0 / v17) - (v68 + v67) * 0.5
|
||||
v18 = min(1.0 / v17 - v67, v66)
|
||||
|
||||
v69 = Mat4(v17, 0, 0, 0,
|
||||
0, v17, 0, 0,
|
||||
0, 0, v17, 0,
|
||||
0, 0, v18 * v17, 1.0)
|
||||
a3 = v69
|
||||
|
||||
if v75[3] != 0.0:
|
||||
card = CardMaker('nametag')
|
||||
card.setFrame(self.m_group.m_name_frame[0] - NametagGlobals._card_pad[0],
|
||||
self.m_group.m_name_frame[1] + NametagGlobals._card_pad[1],
|
||||
v68, v67)
|
||||
card.setColor(v75)
|
||||
if NametagGlobals._nametag_card:
|
||||
card.setSourceGeometry(NametagGlobals._nametag_card.node(),
|
||||
NametagGlobals._nametag_card_frame)
|
||||
|
||||
self.m_attached_np = self.m_np.attachNewNode(card.generate())
|
||||
self.m_attached_np.setMat(v69)
|
||||
if v75[3] != 1.0:
|
||||
self.m_attached_np.setTransparency(1)
|
||||
|
||||
if self.m_has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
self.m_attached_np.setBin(bin, self.m_draw_order)
|
||||
|
||||
self.m_copied_np = self.m_group.copyNameTo(self.m_np)
|
||||
self.m_copied_np.setMat(a3)
|
||||
if self.m_has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
self.m_copied_np.setBin(bin, self.m_draw_order)
|
||||
|
||||
self.m_copied_np.setColor(v84)
|
||||
if v84[3] != 1.0:
|
||||
self.m_copied_np.setTransparency(1)
|
||||
|
||||
reducer = SceneGraphReducer()
|
||||
reducer.applyAttribs(self.m_copied_np.node())
|
||||
reducer.applyAttribs(self.m_attached_np.node())
|
||||
|
||||
if NametagGlobals._arrow_model:
|
||||
self.m_arrow = NametagGlobals._arrow_model.copyTo(self.m_np)
|
||||
if self.m_has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
self.m_arrow.setBin(bin, self.m_draw_order)
|
||||
|
||||
self.m_trans_vec = a3.xformPoint(Point3(0, 0, v68 - 1.0))
|
||||
|
||||
color = Vec4(NametagGlobals.getArrowColor(self.m_group.getColorCode()))
|
||||
|
||||
# AP Teams Nametag hook. If we have an override for a color profile use it.
|
||||
if self.usingColorProfile():
|
||||
color = self.getColorProfile().getColorFromState(PGButton.SInactive)
|
||||
|
||||
self.m_arrow.setColor(color)
|
||||
if color[3] != 1.0:
|
||||
self.m_arrow.setTransparency(1)
|
||||
|
||||
self.rotateArrow()
|
||||
|
||||
elif self.m_arrow:
|
||||
self.m_arrow.removeNode()
|
||||
|
||||
v69 = self.m_np.getNetTransform().getMat()
|
||||
v69 = a3 * v69
|
||||
|
||||
v77 = v69.xformPoint(Point3(self.m_group.m_name_frame[0] - NametagGlobals._card_pad[0], 0, v68))
|
||||
v80 = v69.xformPoint(Point3(self.m_group.m_name_frame[1] + NametagGlobals._card_pad[1], 0, v67))
|
||||
|
||||
frame = Vec4(v77[0], v80[0], v77[2], v80[2])
|
||||
self.setRegion(frame, 0)
|
||||
|
||||
def generateChat(self, balloon):
|
||||
v5 = self.getState()
|
||||
text_color = Vec4(NametagGlobals.getChatFg(self.m_group.getColorCode(), v5))
|
||||
balloon_color = Vec4(NametagGlobals.getChatBg(self.m_group.getColorCode(), v5))
|
||||
|
||||
if self.m_group.m_chat_flags & CFQuicktalker:
|
||||
balloon_color = Vec4(self.m_group.getQtColor())
|
||||
|
||||
balloon_color[3] = max(balloon_color[3], NametagGlobals._min_2d_alpha)
|
||||
balloon_color[3] = min(balloon_color[3], NametagGlobals._max_2d_alpha)
|
||||
|
||||
text = self.m_group.getChat()
|
||||
if self.m_group.m_name:
|
||||
text = '%s: %s' % (self.m_group.m_name, text)
|
||||
|
||||
has_page_button = False
|
||||
has_quit_button = False
|
||||
if not self.m_group.m_has_timeout:
|
||||
has_page_button = self.m_group.m_chat_flags & CFPageButton
|
||||
if self.m_group.getPageNumber() >= self.m_group.getNumChatPages() - 1:
|
||||
if self.m_group.m_chat_flags & CFQuitButton:
|
||||
has_page_button = False
|
||||
has_quit_button = True
|
||||
|
||||
page_button = None
|
||||
if has_page_button:
|
||||
page_button = NametagGlobals.getPageButton(v5)
|
||||
|
||||
elif has_quit_button:
|
||||
page_button = NametagGlobals.getQuitButton(v5)
|
||||
|
||||
reversed = self.m_group.m_chat_flags & CFReversed
|
||||
new_button = [None]
|
||||
balloon_result = balloon.generate(text, self.m_group.getChatFont(), self.m_wordwrap,
|
||||
text_color, balloon_color, False,
|
||||
self.m_has_draw_order, self.m_draw_order,
|
||||
page_button, self.m_group.willHaveButton(),
|
||||
reversed, new_button)
|
||||
|
||||
self.m_unknown_np = self.m_np.attachNewNode(balloon_result)
|
||||
|
||||
v88 = 8.0 # XXX THIS IS A GUESS
|
||||
v49 = 2 * self.m_cell_width
|
||||
a6 = v49 / (v88 + 1.0)
|
||||
v50 = balloon.m_text_height * balloon.m_hscale
|
||||
v85 = balloon.m_hscale * 5.0
|
||||
v88 = v50 * 0.5
|
||||
v113 = -(balloon.m_hscale * 0.5 + v85)
|
||||
v51 = -(NametagGlobals._balloon_text_origin[2] + v88)
|
||||
v118 = Mat4(a6, 0, 0, 0,
|
||||
0, a6, 0, 0,
|
||||
0, 0, a6, 0,
|
||||
v113 * a6, 0, v51 * a6, 1.0)
|
||||
|
||||
self.m_unknown_np.setMat(v118)
|
||||
|
||||
reducer = SceneGraphReducer()
|
||||
reducer.applyAttribs(self.m_unknown_np.node())
|
||||
|
||||
v66 = self.m_np.getNetTransform().getMat()
|
||||
|
||||
# XXX THE LINES BELOW ARE A GUESS
|
||||
v67 = v113 * a6
|
||||
v68 = v51 * a6
|
||||
v94 = v66.xformPoint(Point3(v67, 0.0, v68))
|
||||
v97 = v66.xformPoint(Point3(-v67, 0.0, -v68))
|
||||
|
||||
frame = Vec4(v94[0], v97[0], v94[2], v97[2])
|
||||
self.setRegion(frame, 0)
|
||||
|
||||
def cullCallback(self, *args):
|
||||
self.rotateArrow()
|
||||
if self.m_visible and self.m_popup_region:
|
||||
self.m_seq = self.m_group.getRegionSeq()
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
import math
|
||||
|
||||
from panda3d.core import *
|
||||
|
||||
from . import NametagGlobals
|
||||
from .Nametag import Nametag
|
||||
from ._constants import *
|
||||
|
||||
|
||||
class Nametag3d(Nametag, PandaNode):
|
||||
def __init__(self):
|
||||
Nametag.__init__(self, 10.5)
|
||||
PandaNode.__init__(self, 'unnamed')
|
||||
|
||||
self.m_np_360 = None
|
||||
self.m_np_372 = None
|
||||
self.m_np_balloon = None
|
||||
|
||||
# self.cbNode = CallbackNode(self.getName() + '-cbNode')
|
||||
# self.cbNode.setCullCallback(PythonCallbackObject(self.cullCallback))
|
||||
# self.addChild(self.cbNode)
|
||||
NametagGlobals._active_nametags_3d.add(self)
|
||||
|
||||
self.m_billboard_offset = 3.0
|
||||
|
||||
self.m_np_top = NodePath.anyPath(PandaNode('top'))
|
||||
self.m_is_3d = 1
|
||||
self.m_field_396 = 0
|
||||
self.m_name_frame = Vec4(0, 0, 0, 0)
|
||||
self.m_chat_contents = None
|
||||
|
||||
self.setBounds(BoundingSphere((0, 0, 0), 2.0))
|
||||
|
||||
def setBillboardOffset(self, billboard_offset):
|
||||
self.m_billboard_offset = billboard_offset
|
||||
|
||||
def getBillboardOffset(self):
|
||||
return self.m_billboard_offset
|
||||
|
||||
def cullCallback(self, traverse_data):
|
||||
if self.isGroupManaged():
|
||||
# sort = CullBinManager.getGlobalPtr().getBinSort(traverse_data._state.getBinIndex())
|
||||
# np = traverse_data._node_path.getNodePath()
|
||||
traverser = traverse_data.getTrav()
|
||||
np = NodePath.anyPath(self)
|
||||
sort = CullBinManager.getGlobalPtr().getBinSort(traverser.getInitialState().getBinIndex())
|
||||
self.adjustToCamera(np, sort)
|
||||
|
||||
def manage(self, manager):
|
||||
self.m_np_top.reparentTo(NodePath.anyPath(self))
|
||||
# Nametag/chat geometry is emissive — never lit by scene lights.
|
||||
# Priority 101 overrides render.setLight(_, 100) from OutdoorLighting.
|
||||
self.m_np_top.setLightOff(101)
|
||||
self.updateContents()
|
||||
|
||||
def unmanage(self, manager):
|
||||
self.m_np_top.detachNode()
|
||||
Nametag.unmanage(self, manager)
|
||||
|
||||
def updateContents(self):
|
||||
self.stopFlash()
|
||||
|
||||
if self.m_has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
self.m_np_top.setBin(bin, self.m_draw_order)
|
||||
|
||||
else:
|
||||
self.m_np_top.clearBin()
|
||||
|
||||
if self.m_group:
|
||||
self.setName(self.m_group.getName())
|
||||
|
||||
else:
|
||||
self.setName('unnamed')
|
||||
|
||||
if self.m_np_360:
|
||||
self.m_np_360.removeNode()
|
||||
|
||||
if self.m_np_372:
|
||||
self.m_np_372.removeNode()
|
||||
|
||||
if self.m_np_balloon:
|
||||
self.m_np_balloon.removeNode()
|
||||
|
||||
self.m_np_top.node().removeAllChildren()
|
||||
|
||||
self.m_chat_contents = self.determineContents()
|
||||
if self.isGroupManaged():
|
||||
if self.m_chat_contents & 2:
|
||||
self.generateChat(NametagGlobals._speech_balloon_3d)
|
||||
|
||||
elif self.m_chat_contents & 4:
|
||||
self.generateChat(NametagGlobals._thought_balloon_3d)
|
||||
|
||||
elif self.m_chat_contents & 1:
|
||||
self.generateName()
|
||||
|
||||
def release(self, arg):
|
||||
if arg.getButton() == MouseButton.one():
|
||||
self.setState(PGButton.SRollover)
|
||||
if self.m_group:
|
||||
self.m_group.click()
|
||||
|
||||
def generateChat(self, balloon):
|
||||
v5 = self.getState()
|
||||
text_color = Vec4(NametagGlobals.getChatFg(self.m_group.getColorCode(), v5))
|
||||
balloon_color = Vec4(NametagGlobals.getChatBg(self.m_group.getColorCode(), v5))
|
||||
|
||||
if self.m_group.m_chat_flags & CFQuicktalker:
|
||||
balloon_color = Vec4(self.m_group.getQtColor())
|
||||
|
||||
text = self.m_group.getChat()
|
||||
has_page_button = False
|
||||
has_quit_button = False
|
||||
if not self.m_group.m_has_timeout:
|
||||
has_page_button = self.m_group.m_chat_flags & CFPageButton
|
||||
if self.m_group.getPageNumber() >= self.m_group.getNumChatPages() - 1:
|
||||
if self.m_group.m_chat_flags & CFQuitButton:
|
||||
has_page_button = False
|
||||
has_quit_button = True
|
||||
|
||||
page_button = None
|
||||
if has_page_button:
|
||||
page_button = NametagGlobals.getPageButton(v5)
|
||||
|
||||
elif has_quit_button:
|
||||
page_button = NametagGlobals.getQuitButton(v5)
|
||||
|
||||
reversed = self.m_group.m_chat_flags & CFReversed
|
||||
new_button = [None]
|
||||
balloon_result = balloon.generate(text, self.m_group.getChatFont(), self.m_wordwrap,
|
||||
text_color, balloon_color, self.m_is_3d,
|
||||
self.m_has_draw_order, self.m_draw_order,
|
||||
page_button, self.m_group.willHaveButton(),
|
||||
reversed, new_button)
|
||||
|
||||
self.m_np_balloon = self.m_np_top.attachNewNode(balloon_result)
|
||||
if new_button[0]:
|
||||
self.startFlash(new_button[0])
|
||||
|
||||
self.m_name_frame = balloon.m_text_frame
|
||||
self.m_field_396 = 1
|
||||
|
||||
def generateName(self):
|
||||
v4 = self.getState()
|
||||
v56 = Vec4(NametagGlobals.getNameFg(self.m_group.getColorCode(), v4))
|
||||
|
||||
# Archipelago Teams Nametag integration removed
|
||||
|
||||
v54 = Vec4(NametagGlobals.getNameBg(self.m_group.getColorCode(), v4))
|
||||
|
||||
|
||||
self.m_name_frame = Vec4(*self.m_group.getNameFrame())
|
||||
self.m_name_frame[0] -= NametagGlobals._card_pad[0]
|
||||
self.m_name_frame[1] += NametagGlobals._card_pad[1]
|
||||
self.m_name_frame[2] -= NametagGlobals._card_pad[2]
|
||||
self.m_name_frame[3] += NametagGlobals._card_pad[3]
|
||||
self.m_field_396 = 1
|
||||
|
||||
v47 = None
|
||||
if v54[3] != 0.0:
|
||||
card = CardMaker('nametag')
|
||||
card.setFrame(self.m_name_frame)
|
||||
card.setColor(v54)
|
||||
if NametagGlobals._nametag_card:
|
||||
card.setSourceGeometry(NametagGlobals._nametag_card.node(),
|
||||
NametagGlobals._nametag_card_frame)
|
||||
|
||||
self.m_np_372 = self.m_np_top.attachNewNode(card.generate())
|
||||
self.m_np_372.setTransparency(1)
|
||||
v47 = self.m_np_372.find('**/+GeomNode')
|
||||
|
||||
label86 = False
|
||||
if self.m_is_3d:
|
||||
if self.m_group.m_name_icon:
|
||||
self.m_group.m_name_icon.instanceTo(self.m_np_top)
|
||||
|
||||
if v47:
|
||||
self.m_np_360 = self.m_group.copyNameTo(v47)
|
||||
self.m_np_360.setDepthWrite(0)
|
||||
self.m_np_360.setY(-0.01) # Panda3D 1.10 hack to prevent z-fighting.
|
||||
v47.node().setEffect(DecalEffect.make())
|
||||
|
||||
else:
|
||||
label86 = True
|
||||
|
||||
else:
|
||||
label86 = True
|
||||
|
||||
if label86:
|
||||
self.m_np_360 = self.m_group.copyNameTo(self.m_np_top)
|
||||
if self.m_has_draw_order:
|
||||
bin = config.GetString('nametag-fixed-bin', 'fixed')
|
||||
self.m_name_icon.setBin(bin, self.m_draw_order + 1)
|
||||
self.m_np_360.setBin(bin, self.m_draw_order + 2)
|
||||
|
||||
self.m_np_360.setColor(v56)
|
||||
if v56[3] != 1.0:
|
||||
self.m_np_360.setTransparency(1)
|
||||
|
||||
def adjustToCamera(self, np, sort):
|
||||
if self.m_is_3d:
|
||||
lens = NametagGlobals._camera.node().getLens(0)
|
||||
if self.m_avatar or not self.m_group:
|
||||
v131 = self.m_avatar
|
||||
|
||||
else:
|
||||
v131 = self.m_group.m_avatar
|
||||
|
||||
v130 = NametagGlobals._camera.getTransform(np)
|
||||
v25 = v130.getMat()
|
||||
v204 = v25.xformVec(Vec3.up())
|
||||
v203 = v25.xformVec(Vec3.forward())
|
||||
|
||||
v193 = Mat3()
|
||||
lookAt(v193, Vec3(v203), Vec3(v204))
|
||||
v177 = Mat4(v193)
|
||||
v177_3_0 = v177[3][0]
|
||||
v177_3_1 = v177[3][1]
|
||||
|
||||
a3 = np.getTransform(NametagGlobals._camera).getMat()
|
||||
v122 = a3[3][1]
|
||||
v30 = max(v122, 0.1)
|
||||
v31 = v30 * 0.02
|
||||
v121 = (v31 ** 0.5) * NametagGlobals.getGlobalNametagScale() * 0.56
|
||||
if self.m_billboard_offset == 0.0:
|
||||
v42 = 0
|
||||
|
||||
else:
|
||||
v32 = v25[0][1]
|
||||
v33 = v25[0][0]
|
||||
v136 = v25[0][2]
|
||||
v134 = v33
|
||||
v127 = self.m_billboard_offset
|
||||
v144 = math.sqrt(v136 * v136 + v32 * v32 + v134 * v134)
|
||||
v129 = self.m_billboard_offset / v144
|
||||
if v122 > 0.0:
|
||||
if isinstance(lens, PerspectiveLens):
|
||||
v37 = lens.getNear() + 0.001
|
||||
if v122 - v129 < v37:
|
||||
v129 = v122 - v37
|
||||
v127 = v129 * v144
|
||||
|
||||
v121 = (v122 - v129) / v122 * v121
|
||||
|
||||
v38 = v25[3][0]
|
||||
v39 = v25[3][1]
|
||||
v126 = v25[3][2]
|
||||
v125 = v39
|
||||
v129 = v126 * v126 + v39 * v39 + v38 * v38
|
||||
if v129 == 0.0:
|
||||
v125 = 0.0
|
||||
v38 = 0.0
|
||||
v126 = 0.0
|
||||
|
||||
else:
|
||||
v40 = v129 - 1.0
|
||||
if abs(v40) > 0.0: # if ( v40 >= 1.0e-12 || v40 <= -1.0e-12 ) NOT ALMOST ZERO
|
||||
v41 = 1.0 / math.sqrt(v129)
|
||||
v38 *= v41
|
||||
v125 *= v41
|
||||
v126 *= v41
|
||||
|
||||
v126 *= v127
|
||||
v177_3_0 = v38 * v127
|
||||
v177_3_1 = v125 * v127
|
||||
v42 = v126
|
||||
|
||||
v205 = Mat4(v177[0][0] * v121, v177[0][1] * v121, v177[0][2] * v121, v177[0][3] * v121,
|
||||
v177[1][0] * v121, v177[1][1] * v121, v177[1][2] * v121, v177[1][3] * v121,
|
||||
v177[2][0] * v121, v177[2][1] * v121, v177[2][2] * v121, v177[2][3] * v121,
|
||||
v177_3_0, v177_3_1, v42, v177[3][3])
|
||||
self.m_np_top.setMat(v205)
|
||||
|
||||
v51 = 0
|
||||
if self.displayAsActive():
|
||||
if not self.m_chat_contents & (2 | 4):
|
||||
v51 = 1
|
||||
|
||||
elif not self.m_group:
|
||||
v51 = 1
|
||||
|
||||
elif self.m_group.m_has_timeout:
|
||||
v51 = 1
|
||||
|
||||
elif not self.m_group.willHaveButton():
|
||||
v51 = 1
|
||||
|
||||
elif self.m_group.getPageNumber() >= self.m_group.getNumChatPages() - 1:
|
||||
v51 = 1
|
||||
|
||||
v123 = 0
|
||||
sorta = 0
|
||||
frame = Vec4(0, 0, 0, 0)
|
||||
v150 = lens.getProjectionMat()
|
||||
|
||||
if v51:
|
||||
v138, v139, v140 = v205.xformVec(Vec3(-2.5, 0.0, 1.0))
|
||||
v124, v125, v126 = v205.xformVec(Vec3(2.5, 0.0, 1.0))
|
||||
|
||||
v121 = np.getTransform(v131)
|
||||
if v121.isInvalid():
|
||||
return
|
||||
|
||||
v64 = v121.getMat()
|
||||
v138, v139, v140 = v64.xformPoint(Point3(v138, v139, v140))
|
||||
v124, v125, v126 = v64.xformPoint(Point3(v124, v125, v126))
|
||||
|
||||
v122 = v131.getTransform(NametagGlobals._camera)
|
||||
if v122.isInvalid():
|
||||
return
|
||||
|
||||
v124, v125, v126 = v122.getMat().xformPoint(Point3(v124, v125, v126))
|
||||
v134, v135, v136 = v122.getMat().xformPoint(Point3(v138, v139, 0.0))
|
||||
|
||||
a2 = v150.xform(Vec4(v124, v125, v126, 1.0))
|
||||
frame = v150.xform(Vec4(v134, v135, v136, 1.0))
|
||||
|
||||
if a2[3] <= 0.0 or frame[3] <= 0.0:
|
||||
self.m_group.m_nametag3d_flag &= (self.m_group.m_nametag3d_flag <= 0) - 1
|
||||
self.deactivate()
|
||||
return
|
||||
|
||||
v123 = 1
|
||||
v133 = 1.0 / frame[3]
|
||||
v128 = 1.0 / a2[3]
|
||||
frame = Vec4(frame[0] * v133, a2[0] * v128, frame[1] * v133, a2[1] * v128)
|
||||
|
||||
v89 = 0
|
||||
if self.m_field_396:
|
||||
v124, v125, v126 = v205.xformPoint(Point3(self.m_name_frame[0] - 0.5, 0.0, self.m_name_frame[2] - 1.0))
|
||||
v138, v139, v140 = v205.xformPoint(Point3(self.m_name_frame[1] + 0.5, 0.0, self.m_name_frame[3] + 1.0))
|
||||
|
||||
v124, v125, v126 = a3.xformPoint(Point3(v124, v125, v126))
|
||||
v138, v139, v140 = a3.xformPoint(Point3(v138, v139, v140))
|
||||
|
||||
a2 = v150.xform(Vec4(v138, v139, v140, 1.0))
|
||||
v134, v135, v136, v137 = v150.xform(Vec4(v124, v125, v126, 1.0))
|
||||
|
||||
if v137 <= 0.0 or a2[3] <= 0.0:
|
||||
v89 = 1
|
||||
|
||||
else:
|
||||
v133 = 1.0 / v137
|
||||
v109 = 1.0 / a2[3]
|
||||
v110 = v109 * a2[1]
|
||||
v127 = v133 * v135
|
||||
v131 = v109 * a2[0]
|
||||
v111 = v133 * v134
|
||||
v146 = v111
|
||||
if v111 < -1.0 or v131 > 1.0 or v127 < -1.0 or v110 > 1.0:
|
||||
v89 = 1
|
||||
|
||||
if v123:
|
||||
if frame[3] > v110:
|
||||
v110 = frame[3]
|
||||
if frame[2] >= v127:
|
||||
v115 = v127
|
||||
else:
|
||||
v115 = frame[2]
|
||||
if frame[1] <= v131:
|
||||
v116 = v131
|
||||
else:
|
||||
v116 = frame[1]
|
||||
if frame[0] >= v146:
|
||||
frame[0] = v146
|
||||
|
||||
frame[1] = v116
|
||||
frame[2] = v115
|
||||
|
||||
else:
|
||||
frame[0] = v146
|
||||
frame[1] = v131
|
||||
frame[2] = v127
|
||||
v123 = 1
|
||||
|
||||
frame[3] = v110
|
||||
sorta = int(v125 * -100.0)
|
||||
|
||||
if v123 and self.displayAsActive():
|
||||
self.setRegion(frame, sorta)
|
||||
|
||||
v118 = self.m_group.m_nametag3d_flag
|
||||
if v89:
|
||||
v118 = max(v118, 1)
|
||||
|
||||
elif v118 <= 2:
|
||||
v118 = 2
|
||||
|
||||
self.m_group.setNametag3dFlag(v118)
|
||||
return
|
||||
|
||||
self.m_group.incrementNametag3dFlag(2)
|
||||
if not self.displayAsActive():
|
||||
return
|
||||
|
||||
if not self.m_field_396:
|
||||
return
|
||||
|
||||
v12 = np.getNetTransform().getMat()
|
||||
v124 = v12.xformPoint(Point3(self.m_name_frame[0] - 0.5, 0, self.m_name_frame[2] - 1.0))
|
||||
v16 = v12.xformPoint(Point3(self.m_name_frame[1] + 0.5, 0, self.m_name_frame[3] + 1.0))
|
||||
|
||||
v131 = Vec4(v124[0], v16[0], v124[2], v16[2])
|
||||
self.setRegion(v131, sort)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from .Nametag3d import Nametag3d
|
||||
|
||||
|
||||
class NametagFloat2d(Nametag3d):
|
||||
def __init__(self):
|
||||
Nametag3d.__init__(self)
|
||||
self.m_is_3d = False
|
||||
self.updateContents()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .Nametag3d import Nametag3d
|
||||
|
||||
|
||||
class NametagFloat3d(Nametag3d):
|
||||
def __init__(self):
|
||||
Nametag3d.__init__(self)
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
from panda3d.core import *
|
||||
|
||||
_speech_balloon_3d = None
|
||||
|
||||
|
||||
def setSpeechBalloon3d(speech_balloon_3d):
|
||||
global _speech_balloon_3d
|
||||
_speech_balloon_3d = speech_balloon_3d
|
||||
|
||||
|
||||
def getSpeechBalloon3d():
|
||||
return _speech_balloon_3d
|
||||
|
||||
|
||||
_global_nametag_scale = 1.0
|
||||
|
||||
|
||||
def setGlobalNametagScale(global_nametag_scale):
|
||||
global _global_nametag_scale
|
||||
_global_nametag_scale = global_nametag_scale
|
||||
|
||||
|
||||
def getGlobalNametagScale():
|
||||
return _global_nametag_scale
|
||||
|
||||
|
||||
_camera = NodePath()
|
||||
|
||||
|
||||
def setCamera(camera):
|
||||
global _camera
|
||||
_camera = camera
|
||||
|
||||
|
||||
def getCamera():
|
||||
return _camera
|
||||
|
||||
|
||||
_master_nametags_active = True
|
||||
|
||||
|
||||
def setMasterNametagsActive(master_nametags_active):
|
||||
global _master_nametags_active
|
||||
_master_nametags_active = master_nametags_active
|
||||
|
||||
|
||||
def getMasterNametagsActive():
|
||||
return _master_nametags_active
|
||||
|
||||
|
||||
_min_2d_alpha = 0.0
|
||||
|
||||
|
||||
def setMin2dAlpha(min_2d_alpha):
|
||||
global _min_2d_alpha
|
||||
global _margin_prop_seq
|
||||
_min_2d_alpha = min_2d_alpha
|
||||
_margin_prop_seq += 1
|
||||
|
||||
|
||||
def getMin2dAlpha():
|
||||
return _min_2d_alpha
|
||||
|
||||
|
||||
_arrow_color = [
|
||||
Vec4(1.0, 0.40000001, 0.2, 1.0),
|
||||
Vec4(1.0, 0.40000001, 0.2, 1.0),
|
||||
Vec4(1.0, 0.40000001, 0.2, 1.0),
|
||||
Vec4(1.0, 0.40000001, 0.2, 1.0),
|
||||
Vec4(0.30000001, 0.60000002, 1.0, 1.0),
|
||||
Vec4(0.55000001, 0.55000001, 0.55000001, 1.0),
|
||||
Vec4(0.30000001, 0.60000002, 1.0, 1.0),
|
||||
Vec4(0.30000001, 0.69999999, 0.30000001, 1.0),
|
||||
Vec4(0.30000001, 0.30000001, 0.69999999, 1.0)
|
||||
]
|
||||
|
||||
|
||||
def getArrowColor(index):
|
||||
return _arrow_color[index]
|
||||
|
||||
|
||||
_mouse_watcher = None
|
||||
|
||||
|
||||
def setMouseWatcher(mouse_watcher):
|
||||
global _mouse_watcher
|
||||
_mouse_watcher = mouse_watcher
|
||||
|
||||
|
||||
def getMouseWatcher():
|
||||
return _mouse_watcher
|
||||
|
||||
|
||||
_master_arrows_on = True
|
||||
|
||||
|
||||
def setMasterArrowsOn(master_arrows_on):
|
||||
global _master_arrows_on
|
||||
_master_arrows_on = master_arrows_on
|
||||
|
||||
|
||||
def getMasterArrowsOn():
|
||||
return _master_arrows_on
|
||||
|
||||
|
||||
_toon = NodePath()
|
||||
|
||||
|
||||
def setToon(toon):
|
||||
global _toon
|
||||
_toon = toon
|
||||
|
||||
|
||||
def getToon():
|
||||
return _toon
|
||||
|
||||
|
||||
_master_nametags_visible = True
|
||||
|
||||
|
||||
def setMasterNametagsVisible(master_nametags_visible):
|
||||
global _master_nametags_visible
|
||||
_master_nametags_visible = master_nametags_visible
|
||||
|
||||
|
||||
def getMasterNametagsVisible():
|
||||
return _master_nametags_visible
|
||||
|
||||
|
||||
_thought_balloon_2d = None
|
||||
|
||||
|
||||
def setThoughtBalloon2d(thought_balloon_2d):
|
||||
global _thought_balloon_2d
|
||||
_thought_balloon_2d = thought_balloon_2d
|
||||
|
||||
|
||||
def getThoughtBalloon2d():
|
||||
return _thought_balloon_2d
|
||||
|
||||
|
||||
_max_2d_alpha = 0.6
|
||||
|
||||
|
||||
def setMax2dAlpha(max_2d_alpha):
|
||||
global _max_2d_alpha
|
||||
global _margin_prop_seq
|
||||
_max_2d_alpha = max_2d_alpha
|
||||
_margin_prop_seq += 1
|
||||
|
||||
|
||||
def getMax2dAlpha():
|
||||
return _max_2d_alpha
|
||||
|
||||
|
||||
_onscreen_chat_forced = False
|
||||
|
||||
|
||||
def setOnscreenChatForced(onscreen_chat_forced):
|
||||
global _onscreen_chat_forced
|
||||
_onscreen_chat_forced = onscreen_chat_forced
|
||||
|
||||
|
||||
def getOnscreenChatForced():
|
||||
return _onscreen_chat_forced
|
||||
|
||||
|
||||
_nametag_card = NodePath()
|
||||
|
||||
|
||||
def setNametagCard(nametag_card, nametag_card_frame):
|
||||
global _nametag_card
|
||||
global _nametag_card_frame
|
||||
_nametag_card = nametag_card
|
||||
_nametag_card_frame = nametag_card_frame
|
||||
|
||||
|
||||
def getNametagCard():
|
||||
return _nametag_card
|
||||
|
||||
|
||||
_nametag_card_frame = Vec4(0, 0, 0, 0)
|
||||
|
||||
|
||||
def getNametagCardFrame():
|
||||
return _nametag_card_frame
|
||||
|
||||
|
||||
_rollover_sound = None
|
||||
|
||||
|
||||
def setRolloverSound(rollover_sound):
|
||||
global _rollover_sound
|
||||
_rollover_sound = rollover_sound
|
||||
|
||||
|
||||
def getRolloverSound():
|
||||
return _rollover_sound
|
||||
|
||||
|
||||
_speech_balloon_2d = None
|
||||
|
||||
|
||||
def setSpeechBalloon2d(speech_balloon_2d):
|
||||
global _speech_balloon_2d
|
||||
_speech_balloon_2d = speech_balloon_2d
|
||||
|
||||
|
||||
def getSpeechBalloon2d():
|
||||
return _speech_balloon_2d
|
||||
|
||||
|
||||
_text_node = TextNode('nametag')
|
||||
|
||||
|
||||
def getTextNode():
|
||||
return _text_node
|
||||
|
||||
|
||||
_click_sound = None
|
||||
|
||||
|
||||
def setClickSound(click_sound):
|
||||
global _click_sound
|
||||
_click_sound = click_sound
|
||||
|
||||
|
||||
def getClickSound():
|
||||
return _click_sound
|
||||
|
||||
|
||||
_quit_button = [NodePath(), NodePath(), NodePath(), NodePath()]
|
||||
|
||||
|
||||
def setQuitButton(state, quit_button):
|
||||
global _quit_button
|
||||
_quit_button[state] = quit_button
|
||||
|
||||
|
||||
def getQuitButton(state):
|
||||
return _quit_button[state]
|
||||
|
||||
|
||||
_arrow_model = NodePath()
|
||||
|
||||
|
||||
def setArrowModel(arrow_model):
|
||||
global _arrow_model
|
||||
_arrow_model = arrow_model
|
||||
|
||||
|
||||
def getArrowModel():
|
||||
return _arrow_model
|
||||
|
||||
|
||||
_thought_balloon_3d = None
|
||||
|
||||
|
||||
def setThoughtBalloon3d(thought_balloon_3d):
|
||||
global _thought_balloon_3d
|
||||
_thought_balloon_3d = thought_balloon_3d
|
||||
|
||||
|
||||
def getThoughtBalloon3d():
|
||||
return _thought_balloon_3d
|
||||
|
||||
|
||||
_name_bg = [
|
||||
# CCNormal
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCNoChat
|
||||
Vec4(1, 1, 1, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCNonPlayer
|
||||
Vec4(1, 1, 1, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCSuit
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCToonBuilding
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCSuitBuilding
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCHouseBuilding
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCSpeedChat
|
||||
Vec4(1, 1, 1, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
|
||||
# CCFreeChat
|
||||
Vec4(0.8, 0.8, 0.8, 0.5),
|
||||
Vec4(0.2, 0.2, 0.2, 0.6),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.8, 0.8, 0.8, 0.5)
|
||||
]
|
||||
|
||||
|
||||
def getNameBg(color_code, state):
|
||||
return _name_bg[4 * color_code + state]
|
||||
|
||||
|
||||
_page_button = [NodePath(), NodePath(), NodePath(), NodePath()]
|
||||
|
||||
|
||||
def setPageButton(state, page_button):
|
||||
global _page_button
|
||||
_page_button[state] = page_button
|
||||
|
||||
|
||||
def getPageButton(state):
|
||||
return _page_button[state]
|
||||
|
||||
|
||||
_name_fg = [
|
||||
# CCNormal
|
||||
Vec4(0, 0, 1, 1),
|
||||
Vec4(0.5, 0.5, 1, 1),
|
||||
Vec4(0.5, 0.5, 1, 1),
|
||||
Vec4(0.3, 0.3, 0.7, 1),
|
||||
|
||||
# CCNoChat
|
||||
Vec4(0.8, 0.4, 0, 1),
|
||||
Vec4(1, 0.5, 0.5, 1),
|
||||
Vec4(1, 0.5, 0, 1),
|
||||
Vec4(0.6, 0.4, 0.2, 1),
|
||||
|
||||
# CCNonPlayer
|
||||
Vec4(0.8, 0.4, 0, 1),
|
||||
Vec4(1, 0.5, 0.5, 1),
|
||||
Vec4(1, 0.5, 0, 1),
|
||||
Vec4(0.6, 0.4, 0.2, 1),
|
||||
|
||||
# CCSuit
|
||||
Vec4(0, 0, 0, 1),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.5, 0.5, 0.5, 1),
|
||||
Vec4(0.2, 0.2, 0.2, 1),
|
||||
|
||||
# CCToonBuilding
|
||||
Vec4(0, 0, 0, 1),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.5, 0.5, 0.5, 1),
|
||||
Vec4(0.3, 0.6, 1, 1),
|
||||
|
||||
# CCSuitBuilding
|
||||
Vec4(0, 0, 0, 1),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.5, 0.5, 0.5, 1),
|
||||
Vec4(0.55, 0.55, 0.55, 1),
|
||||
|
||||
# CCHouseBuilding
|
||||
Vec4(0, 0, 0, 1),
|
||||
Vec4(1, 1, 1, 1),
|
||||
Vec4(0.5, 0.5, 0.5, 1),
|
||||
Vec4(0.3, 0.6, 1, 1),
|
||||
|
||||
# CCSpeedChat
|
||||
Vec4(0, 0.6, 0.2, 1),
|
||||
Vec4(0, 0.6, 0.2, 1),
|
||||
Vec4(0, 1, 0.5, 1),
|
||||
Vec4(0.1, 0.4, 0.2, 1),
|
||||
|
||||
# CCFreeChat
|
||||
Vec4(0.3, 0.3, 0.7, 1),
|
||||
Vec4(0.2, 0.2, 0.5, 1),
|
||||
Vec4(0.5, 0.5, 1, 1),
|
||||
Vec4(0.3, 0.3, 0.7, 1)
|
||||
]
|
||||
|
||||
|
||||
def getNameFg(color_code, state):
|
||||
return _name_fg[4 * color_code + state]
|
||||
|
||||
|
||||
def getNameWordwrap():
|
||||
return 7.5
|
||||
|
||||
|
||||
_card_pad = Vec4(0.1, 0.1, 0.1, 0)
|
||||
|
||||
|
||||
def getCardPad():
|
||||
return _card_pad
|
||||
|
||||
|
||||
_balloon_modulation_color = Vec4(1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
|
||||
def setBalloonModulationColor(balloon_modulation_color):
|
||||
global _balloon_modulation_color
|
||||
_balloon_modulation_color = balloon_modulation_color
|
||||
|
||||
|
||||
def getBalloonModulationColor():
|
||||
return _balloon_modulation_color
|
||||
|
||||
|
||||
def getChatFg(color_code, state):
|
||||
return [Vec4(0.0, 0.0, 0.0, 1.0),
|
||||
Vec4(1.0, 0.5, 0.5, 1.0),
|
||||
Vec4(0.0, 0.6, 0.6, 1.0),
|
||||
Vec4(0.0, 0.0, 0.0, 1.0)][state]
|
||||
|
||||
|
||||
def getChatBg(color_code, state):
|
||||
return [Vec4(1.0, 1.0, 1.0, 1.0),
|
||||
Vec4(1.0, 1.0, 1.0, 1.0),
|
||||
Vec4(1.0, 1.0, 1.0, 1.0),
|
||||
Vec4(1.0, 1.0, 1.0, 1.0)][state]
|
||||
|
||||
|
||||
_margin_prop_seq = 0
|
||||
_default_qt_color = Vec4(0.8, 0.8, 1, 1)
|
||||
_balloon_text_origin = Point3(1.0, 0, 2.0)
|
||||
|
||||
import weakref
|
||||
_active_nametags_2d = weakref.WeakSet()
|
||||
_active_nametags_3d = weakref.WeakSet()
|
||||
_active_whisper_popups = weakref.WeakSet()
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
from panda3d.core import *
|
||||
|
||||
from .ColorProfile import ColorProfile, GRAY
|
||||
from . import NametagGlobals
|
||||
from .Nametag2d import Nametag2d
|
||||
from .Nametag3d import Nametag3d
|
||||
from ._constants import *
|
||||
|
||||
|
||||
class NametagGroup:
|
||||
CCNormal = 0
|
||||
CCNoChat = 1
|
||||
CCNonPlayer = 2
|
||||
CCSuit = 3
|
||||
CCToonBuilding = 4
|
||||
CCSuitBuilding = 5
|
||||
CCHouseBuilding = 6
|
||||
CCSpeedChat = 7
|
||||
CCFreeChat = 8
|
||||
|
||||
_unique_index = 0
|
||||
|
||||
def __init__(self):
|
||||
self.m_nametags = []
|
||||
|
||||
self.m_name_font = None
|
||||
self.m_chat_font = None
|
||||
|
||||
self.m_avatar = None
|
||||
self.m_node = None
|
||||
|
||||
self.m_name = ''
|
||||
self.m_display_name = ''
|
||||
|
||||
self.m_chat_pages = []
|
||||
self.m_stomp_text = ''
|
||||
self.m_unique_name = ''
|
||||
|
||||
self.m_region_seq = 0
|
||||
|
||||
self.m_name_icon = NodePath.anyPath(PandaNode('icon'))
|
||||
self.m_name_frame = Vec4(0, 0, 0, 0)
|
||||
|
||||
self.m_wordwrap = -1.0
|
||||
self.m_color_code = 0
|
||||
self.m_qt_color = Vec4(NametagGlobals._default_qt_color)
|
||||
self.m_balloon_color = Vec4(NametagGlobals._balloon_modulation_color)
|
||||
self.m_shadow = (0, 0)
|
||||
self.m_has_shadow = False
|
||||
|
||||
self.m_timeout = 0.0
|
||||
self.m_timeout_start = 0.0
|
||||
self.m_has_timeout = False
|
||||
self.m_stomp_time = 0.0
|
||||
self.m_stomp_chat_flags = None
|
||||
self.m_chat_flags = 0
|
||||
self.m_page_number = 0
|
||||
self.m_stomp_delay = 0.5
|
||||
self.m_chat_stomp = 0
|
||||
|
||||
self.m_unique_name = 'nametag-%d' % NametagGroup._unique_index
|
||||
NametagGroup._unique_index += 1
|
||||
|
||||
self.m_object_code = 0
|
||||
self.m_nametag3d_flag = 0
|
||||
self.m_manager = None
|
||||
|
||||
self.m_region_seq += 1
|
||||
|
||||
self.m_contents = CFSpeech | CFThought | CFQuicktalker
|
||||
self.m_is_active = 1
|
||||
self.m_active = NametagGlobals._master_nametags_active
|
||||
self.m_visible = NametagGlobals._master_nametags_visible
|
||||
|
||||
self.m_tag2d = Nametag2d()
|
||||
self.m_tag3d = Nametag3d()
|
||||
self.addNametag(self.m_tag2d)
|
||||
self.addNametag(self.m_tag3d)
|
||||
|
||||
def setFont(self, font):
|
||||
self.setNameFont(font)
|
||||
self.setChatFont(font)
|
||||
|
||||
def setNameFont(self, font):
|
||||
self.m_name_font = font
|
||||
|
||||
def getNameFont(self):
|
||||
return self.m_name_font
|
||||
|
||||
def setChatFont(self, font):
|
||||
self.m_chat_font = font
|
||||
|
||||
def getChatFont(self):
|
||||
return self.m_chat_font
|
||||
|
||||
def setAvatar(self, avatar):
|
||||
self.m_avatar = avatar
|
||||
|
||||
def getAvatar(self):
|
||||
return self.m_avatar
|
||||
|
||||
def setNameIcon(self, icon):
|
||||
self.m_name_icon = icon
|
||||
|
||||
def getNameIcon(self):
|
||||
return self.m_name_icon
|
||||
|
||||
def setColorCode(self, code):
|
||||
self.m_color_code = code
|
||||
|
||||
def getColorCode(self):
|
||||
return self.m_color_code
|
||||
|
||||
def setContents(self, contents):
|
||||
self.m_contents = contents
|
||||
|
||||
def getContents(self):
|
||||
return self.m_contents
|
||||
|
||||
def setDisplayName(self, name):
|
||||
self.m_display_name = name
|
||||
|
||||
if name and self.m_name_font:
|
||||
text_node = NametagGlobals.getTextNode()
|
||||
text_node.setFont(self.m_name_font)
|
||||
text_node.setWordwrap(self.getNameWordwrap())
|
||||
text_node.setAlign(TextNode.ACenter)
|
||||
text_node.setText(name)
|
||||
gen = text_node.generate()
|
||||
self.m_node = gen
|
||||
self.m_name_frame = text_node.getCardActual()
|
||||
|
||||
if self.m_has_shadow:
|
||||
self.m_node = PandaNode('name')
|
||||
self.m_node.addChild(gen)
|
||||
|
||||
pos = Point3(self.m_shadow[0], 0, -self.m_shadow[1])
|
||||
attached = NodePath.anyPath(self.m_node).attachNewNode(gen.copySubgraph())
|
||||
attached.setPos(pos)
|
||||
attached.setColor(0, 0, 0, 1)
|
||||
|
||||
else:
|
||||
self.m_node = None
|
||||
|
||||
self.updateContentsAll()
|
||||
|
||||
def getDisplayName(self):
|
||||
return self.m_display_name
|
||||
|
||||
def setName(self, name):
|
||||
self.m_name = name
|
||||
self.setDisplayName(name)
|
||||
|
||||
def getName(self):
|
||||
return self.m_name
|
||||
|
||||
def getNameFrame(self):
|
||||
return self.m_name_frame
|
||||
|
||||
def setNameWordwrap(self, wordwrap):
|
||||
self.m_wordwrap = wordwrap
|
||||
self.setDisplayName(self.m_display_name)
|
||||
|
||||
def getNameWordwrap(self):
|
||||
if self.m_wordwrap > 0.0:
|
||||
return self.m_wordwrap
|
||||
|
||||
wordwrap = NametagGlobals.getNameWordwrap()
|
||||
return {self.CCNoChat: 7.8,
|
||||
self.CCToonBuilding: 8.5,
|
||||
self.CCSuitBuilding: 8.5,
|
||||
self.CCHouseBuilding: 10.0}.get(self.m_color_code, wordwrap)
|
||||
|
||||
def getNametag(self, index):
|
||||
return self.m_nametags[index]
|
||||
|
||||
def getNametag2d(self):
|
||||
return self.m_tag2d
|
||||
|
||||
def getNametag3d(self):
|
||||
return self.m_tag3d
|
||||
|
||||
def setNametag3dFlag(self, flag):
|
||||
self.m_nametag3d_flag = flag
|
||||
|
||||
def getNametag3dFlag(self):
|
||||
return self.m_nametag3d_flag
|
||||
|
||||
def getNumChatPages(self):
|
||||
return len(self.m_chat_pages)
|
||||
|
||||
def getNumNametags(self):
|
||||
return len(self.m_nametags)
|
||||
|
||||
def setObjectCode(self, code):
|
||||
self.m_object_code = code
|
||||
|
||||
def getObjectCode(self):
|
||||
return self.m_object_code
|
||||
|
||||
def setPageNumber(self, page):
|
||||
if self.m_page_number == page:
|
||||
return
|
||||
|
||||
self.m_page_number = page
|
||||
if self.willHaveButton():
|
||||
self.m_timeout_start = 0.0
|
||||
self.m_has_timeout = False
|
||||
|
||||
self.updateContentsAll()
|
||||
|
||||
def getPageNumber(self):
|
||||
return self.m_page_number
|
||||
|
||||
def getBalloonModulationColor(self):
|
||||
return self.m_balloon_color
|
||||
|
||||
def setQtColor(self, color):
|
||||
self.m_qt_color = color
|
||||
|
||||
def getQtColor(self):
|
||||
return self.m_qt_color
|
||||
|
||||
def getRegionSeq(self):
|
||||
return self.m_region_seq
|
||||
|
||||
def setShadow(self, shadow):
|
||||
self.m_shadow = shadow
|
||||
|
||||
def getShadow(self):
|
||||
return self.m_shadow
|
||||
|
||||
def getStompDelay(self):
|
||||
return self.m_stomp_delay
|
||||
|
||||
def getStompText(self):
|
||||
return self.m_stomp_text
|
||||
|
||||
def setUniqueId(self, name):
|
||||
self.m_unique_name = name
|
||||
|
||||
def getUniqueId(self):
|
||||
return self.m_unique_name
|
||||
|
||||
def hasButton(self):
|
||||
if self.m_has_timeout:
|
||||
return False
|
||||
|
||||
return self.willHaveButton()
|
||||
|
||||
def hasNoQuitButton(self):
|
||||
return (not self.m_has_timeout) and self.m_chat_flags & CFSpeech
|
||||
|
||||
def hasQuitButton(self):
|
||||
return (not self.m_has_timeout) and self.m_chat_flags & CFQuitButton
|
||||
|
||||
def hasPageButton(self):
|
||||
return (not self.m_has_timeout) and self.m_chat_flags & CFPageButton
|
||||
|
||||
def hasShadow(self):
|
||||
return self.m_has_shadow
|
||||
|
||||
def clearShadow(self):
|
||||
self.m_has_shadow = False
|
||||
|
||||
def incrementNametag3dFlag(self, flag):
|
||||
self.m_nametag3d_flag = max(self.m_nametag3d_flag, flag)
|
||||
|
||||
def isManaged(self):
|
||||
return self.m_manager is not None
|
||||
|
||||
def manage(self, manager):
|
||||
if not self.m_manager:
|
||||
self.m_manager = manager
|
||||
for nametag in self.m_nametags:
|
||||
nametag.manage(manager)
|
||||
|
||||
def unmanage(self, manager):
|
||||
if self.m_manager:
|
||||
self.m_manager = None
|
||||
for nametag in self.m_nametags:
|
||||
nametag.unmanage(manager)
|
||||
|
||||
def addNametag(self, nametag):
|
||||
if nametag.m_group:
|
||||
print('Attempt to add %s twice to %s.' % (nametag.__class__.__name__, self.m_name))
|
||||
return
|
||||
|
||||
nametag.m_group = self
|
||||
nametag.updateContents()
|
||||
self.m_nametags.append(nametag)
|
||||
|
||||
if self.m_manager:
|
||||
nametag.manage(self.m_manager)
|
||||
|
||||
def removeNametag(self, nametag):
|
||||
if not nametag.m_group:
|
||||
print('Attempt to removed %s twice from %s.' % (nametag.__class__.__name__, self.m_name))
|
||||
return
|
||||
|
||||
if self.m_manager:
|
||||
nametag.unmanage(self.m_manager)
|
||||
|
||||
nametag.m_group = None
|
||||
nametag.updateContents()
|
||||
self.m_nametags.remove(nametag)
|
||||
|
||||
def setActive(self, active):
|
||||
self.m_is_active = active
|
||||
|
||||
def isActive(self):
|
||||
return self.m_active
|
||||
|
||||
def updateContentsAll(self):
|
||||
for nametag in self.m_nametags:
|
||||
nametag.updateContents()
|
||||
|
||||
def updateRegions(self):
|
||||
for nametag in self.m_nametags:
|
||||
nametag.updateRegion(self.m_region_seq)
|
||||
|
||||
self.m_region_seq += 1
|
||||
|
||||
now = globalClock.getFrameTime()
|
||||
if self.m_stomp_time < now and self.m_chat_stomp > 1:
|
||||
self.m_chat_stomp = 0
|
||||
self.setChat(self.m_stomp_text, self.m_stomp_chat_flags, self.m_page_number)
|
||||
|
||||
if self.m_chat_flags & CFTimeout and now >= self.m_timeout:
|
||||
self.clearChat()
|
||||
self.m_chat_stomp = 0
|
||||
|
||||
v7 = False
|
||||
if self.m_has_timeout and now >= self.m_timeout_start:
|
||||
self.m_has_timeout = 0
|
||||
v7 = True
|
||||
|
||||
if self.m_active != NametagGlobals._master_nametags_active:
|
||||
self.m_active = NametagGlobals._master_nametags_active
|
||||
v7 = True
|
||||
|
||||
if self.m_visible == NametagGlobals._master_nametags_visible:
|
||||
if not v7:
|
||||
return
|
||||
|
||||
else:
|
||||
self.m_visible = NametagGlobals._master_nametags_visible
|
||||
|
||||
self.updateContentsAll()
|
||||
|
||||
def willHaveButton(self):
|
||||
return self.m_chat_flags & (CFPageButton | CFQuitButton)
|
||||
|
||||
def setChat(self, chat, chat_flags, page_number=0):
|
||||
self.m_chat_flags = chat_flags
|
||||
self.m_page_number = page_number
|
||||
|
||||
now = globalClock.getFrameTime()
|
||||
must_split = True
|
||||
if chat_flags and chat:
|
||||
self.m_chat_stomp += 1
|
||||
if self.m_chat_stomp >= 2 and self.m_stomp_delay >= 0.05:
|
||||
self.m_stomp_text = chat
|
||||
self.m_stomp_chat_flags = self.m_chat_flags
|
||||
self.m_stomp_time = now + self.m_stomp_delay
|
||||
self.m_chat_flags = 0
|
||||
must_split = False
|
||||
|
||||
else:
|
||||
self.m_chat_flags = 0
|
||||
self.m_chat_stomp = 0
|
||||
must_split = False
|
||||
|
||||
if must_split:
|
||||
self.m_chat_pages = chat.split('\x07')
|
||||
else:
|
||||
self.m_chat_pages = []
|
||||
|
||||
if self.m_chat_flags & CFTimeout and self.m_stomp_time < now:
|
||||
timeout = len(chat) * 0.5
|
||||
timeout = min(12.0, max(timeout, 4.0))
|
||||
self.m_timeout = timeout + now
|
||||
|
||||
if self.willHaveButton():
|
||||
self.m_has_timeout = True
|
||||
self.m_timeout_start = now + 0.2
|
||||
|
||||
else:
|
||||
self.m_has_timeout = False
|
||||
self.m_timeout_start = 0.0
|
||||
|
||||
self.updateContentsAll()
|
||||
|
||||
def getChat(self):
|
||||
if self.m_chat_pages:
|
||||
return self.m_chat_pages[self.m_page_number]
|
||||
|
||||
return ''
|
||||
|
||||
def clearChat(self):
|
||||
self.setChat('', 0, 0)
|
||||
|
||||
def getChatStomp(self):
|
||||
return self.m_chat_stomp
|
||||
|
||||
def clearAuxNametags(self):
|
||||
for nametag in self.nametags[:]:
|
||||
if nametag not in (self.m_tag2d, self.m_tag3d):
|
||||
self.removeNametag(nametag)
|
||||
|
||||
def click(self):
|
||||
messenger.send(self.m_unique_name)
|
||||
|
||||
def copyNameTo(self, to):
|
||||
return to.attachNewNode(self.m_node.copySubgraph())
|
||||
|
||||
def displayAsActive(self):
|
||||
if self.m_is_active and NametagGlobals._master_nametags_active:
|
||||
return 1
|
||||
|
||||
return self.hasButton()
|
||||
|
||||
def frameCallback(self):
|
||||
# This should be in Nametag2d
|
||||
# I have no idea where libotp called it
|
||||
# so I'm doing it in MarginManager.update
|
||||
self.updateRegions()
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from enum import IntEnum, auto
|
||||
|
||||
from panda3d.core import Vec4
|
||||
|
||||
from .ColorProfile import ColorProfile, GRAY
|
||||
|
||||
|
||||
class WhisperType(IntEnum):
|
||||
|
||||
WTNormal = auto()
|
||||
WTQuickTalker = auto()
|
||||
WTSystem = auto()
|
||||
WTBattleSOS = auto()
|
||||
WTEmote = auto()
|
||||
WTToontownBoardingGroup = auto()
|
||||
WTMagicWord = auto()
|
||||
|
||||
|
||||
# Defines some default color profiles from a whisper type.
|
||||
# Overrides can however be defined when calling localAvatar.displayWhisper()
|
||||
# Maps whisper type -> (ColorProfile(text), ColorProfile(background))
|
||||
WHISPER_COLORS = {
|
||||
|
||||
# Normal whisper popups
|
||||
WhisperType.WTNormal: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0),Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.3, 0.6, 0.8, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.4, 0.8, 1.0, 1.0),Vec4(0.3, 0.6, 0.8, 0.6)),
|
||||
),
|
||||
|
||||
# Toons whispering to each other
|
||||
WhisperType.WTQuickTalker: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.3, 0.6, 0.8, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.4, 0.8, 1.0, 1.0), Vec4(0.3, 0.6, 0.8, 0.6)),
|
||||
),
|
||||
|
||||
# System messages
|
||||
WhisperType.WTSystem: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.8, 0.3, 0.6, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.8, 0.4, 1.0, 1.0), Vec4(0.8, 0.3, 0.6, 0.6)),
|
||||
),
|
||||
|
||||
# Battle SOS
|
||||
WhisperType.WTBattleSOS: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.8, 0.3, 0.6, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.8, 0.4, 1.0, 1.0), Vec4(0.8, 0.3, 0.6, 0.6)),
|
||||
),
|
||||
|
||||
# Emote whispers
|
||||
WhisperType.WTEmote: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.3, 0.6, 0.8, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.4, 1.0, 1.0, 0.4), Vec4(0.3, 0.8, 0.3, 0.6)),
|
||||
),
|
||||
|
||||
# Boarding groups
|
||||
WhisperType.WTToontownBoardingGroup: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.97, 0.43, 0.1, 0.6), Vec4(1.0, 1.0, 1.0, 1.0), Vec4(0.98, 0.6, 0.38, 0.6), Vec4(0.97, 0.43, 0.1, 0.6)),
|
||||
),
|
||||
|
||||
# Commands
|
||||
WhisperType.WTMagicWord: (
|
||||
ColorProfile(Vec4(0.0, 0.0, 0.0, 1.0), Vec4(1.0, 0.5, 0.5, 1.0), Vec4(0.0, 0.0, 0.0, 1.0), Vec4(0.0, 0.0, 0.0, 1.0)),
|
||||
ColorProfile(Vec4(0.7, 0.7, 0.1, 0.6), Vec4(1.0, 1.0, 0.4, 0.8), Vec4(0.8, 0.8, 0.2, 0.6), Vec4(0.8, 0.8, 0.1, 0.6)),
|
||||
),
|
||||
|
||||
}
|
||||
|
||||
# Indeces of what is stored in the above dict per whisper type
|
||||
TEXT_COLOR = 0
|
||||
BACKGROUND_COLOR = 1
|
||||
|
||||
|
||||
# Given a whisper type, return its color profile for text.
|
||||
# If not found, returns gray as a fallback.
|
||||
def getWhisperTextColorProfile(color_code: WhisperType):
|
||||
|
||||
colors = WHISPER_COLORS.get(color_code)
|
||||
if colors is None:
|
||||
print(f'WhisperGlobals.getWhisperTextColorProfile(): Unknown color code: {color_code}! Defaulting to GRAY.')
|
||||
return GRAY
|
||||
|
||||
return colors[TEXT_COLOR]
|
||||
|
||||
|
||||
# Given a whisper type,
|
||||
def getWhisperBackgroundColorProfile(color_code):
|
||||
|
||||
colors = WHISPER_COLORS.get(color_code)
|
||||
if colors is None:
|
||||
print(f'WhisperGlobals.getWhisperBackgroundColorProfile(): Unknown color code: {color_code}! Defaulting to GRAY.')
|
||||
return GRAY
|
||||
|
||||
return colors[BACKGROUND_COLOR]
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
from typing import Union
|
||||
|
||||
from .ColorProfile import ColorProfile
|
||||
from . import WhisperGlobals
|
||||
from .ClickablePopup import *
|
||||
from .MarginPopup import *
|
||||
from . import NametagGlobals
|
||||
|
||||
|
||||
class WhisperPopup(ClickablePopup, MarginPopup):
|
||||
|
||||
def __init__(self, text, font, type, bg_override: ColorProfile = None):
|
||||
ClickablePopup.__init__(self)
|
||||
MarginPopup.__init__(self)
|
||||
|
||||
self.m_text = text
|
||||
self.m_font = font
|
||||
self.m_type = type
|
||||
|
||||
self.m_np_balloon = None
|
||||
self.m_avname = ''
|
||||
self.m_region = None
|
||||
self.m_mouse_watcher = None
|
||||
self.m_manager = None
|
||||
|
||||
# self.cbNode = CallbackNode(self.getName() + '-cbNode')
|
||||
# self.cbNode.setCullCallback(PythonCallbackObject(self.cullCallback))
|
||||
# self.addChild(self.cbNode)
|
||||
NametagGlobals._active_whisper_popups.add(self)
|
||||
|
||||
self.m_time = 0
|
||||
self.m_culled = False
|
||||
self.m_clickable = False
|
||||
self.m_avid = 0
|
||||
self.m_is_player = False
|
||||
self.m_is_player_id = None
|
||||
self.m_state = 3
|
||||
self.m_objcode = 0
|
||||
|
||||
self.bg_override: Union[ColorProfile, None] = bg_override
|
||||
|
||||
def setClickable(self, avatar_name, avatar_id, is_player_id=False):
|
||||
self.m_clickable = True
|
||||
self.m_avname = avatar_name
|
||||
self.m_avid = avatar_id
|
||||
self.m_is_player_id = is_player_id
|
||||
self.m_state = 0
|
||||
|
||||
def click(self):
|
||||
messenger.send('clickedWhisper', [self.m_avid, self.m_is_player])
|
||||
|
||||
def considerVisible(self):
|
||||
if self.m_clickable and self.m_visible and self.m_mouse_watcher != NametagGlobals._mouse_watcher:
|
||||
return False
|
||||
|
||||
if self.m_seq != NametagGlobals._margin_prop_seq:
|
||||
self.m_seq = NametagGlobals._margin_prop_seq
|
||||
self.updateContents()
|
||||
|
||||
return True
|
||||
|
||||
def manage(self, manager):
|
||||
self.m_manager = manager
|
||||
manager.managePopup(self)
|
||||
|
||||
def unmanage(self, manager):
|
||||
manager.unmanagePopup(self)
|
||||
del self.m_manager
|
||||
|
||||
def cullCallback(self, *args):
|
||||
if not self.m_culled:
|
||||
self.m_culled = True
|
||||
self.m_time = globalClock.getFrameTime()
|
||||
|
||||
def setVisible(self, value):
|
||||
MarginPopup.setVisible(self, value)
|
||||
self.updateContents()
|
||||
|
||||
if self.m_clickable:
|
||||
if self.m_region:
|
||||
if self.m_visible:
|
||||
self.m_mouse_watcher = NametagGlobals._mouse_watcher
|
||||
self.m_mouse_watcher.addRegion(self.m_region)
|
||||
|
||||
elif self.m_mouse_watcher:
|
||||
self.m_mouse_watcher.removeRegion(self.m_region)
|
||||
self.m_mouse_watcher = None
|
||||
|
||||
def setRegion(self, frame, sort):
|
||||
if self.m_region:
|
||||
self.m_region.setFrame(frame)
|
||||
|
||||
else:
|
||||
self.m_region = self._createRegion(frame)
|
||||
|
||||
self.m_region.setSort(sort)
|
||||
|
||||
def updateContents(self):
|
||||
if self.m_np_balloon:
|
||||
self.m_np_balloon.removeNode()
|
||||
self.m_np_balloon = None
|
||||
|
||||
if self.m_visible:
|
||||
self.generateText(NametagGlobals._speech_balloon_2d, self.m_text, self.m_font)
|
||||
|
||||
# Based on the whisper type, return a text color profile associated with it
|
||||
def getTextColorProfile(self) -> ColorProfile:
|
||||
return WhisperGlobals.getWhisperTextColorProfile(self.m_type).copy()
|
||||
|
||||
# Based on the whisper type, return a background color profile associated with it
|
||||
def getBackgroundColorProfile(self) -> ColorProfile:
|
||||
|
||||
# Do we have an override?
|
||||
if self.bg_override is not None:
|
||||
return self.bg_override.copy()
|
||||
|
||||
return WhisperGlobals.getWhisperBackgroundColorProfile(self.m_type).copy()
|
||||
|
||||
def generateText(self, balloon, text, font):
|
||||
text_color = self.getTextColorProfile().getColorFromState(self.m_state)
|
||||
balloon_color = self.getBackgroundColorProfile().getColorFromState(self.m_state)
|
||||
balloon_color[3] = max(balloon_color[3], NametagGlobals._min_2d_alpha)
|
||||
balloon_color[3] = min(balloon_color[3], NametagGlobals._max_2d_alpha)
|
||||
|
||||
balloon_result = balloon.generate(text, font, 8.0, text_color, balloon_color,
|
||||
False, False, 0, None, False, False, None)
|
||||
|
||||
self.m_np_balloon = self.m_np.attachNewNode(balloon_result)
|
||||
|
||||
v34 = self.m_cell_width * 0.22222222
|
||||
v35 = balloon.m_text_height * balloon.m_hscale * 0.5
|
||||
v57 = -balloon.m_hscale * 5.5
|
||||
v16 = -(NametagGlobals._balloon_text_origin[2] + v35)
|
||||
|
||||
v64 = Mat4(v34, 0, 0, 0,
|
||||
0, v34, 0, 0,
|
||||
0, 0, v34, 0,
|
||||
v57 * v34, 0, v16 * v34, 1.0)
|
||||
|
||||
self.m_np_balloon.setMat(v64)
|
||||
|
||||
reducer = SceneGraphReducer()
|
||||
reducer.applyAttribs(self.m_np_balloon.node())
|
||||
|
||||
if self.m_clickable:
|
||||
v22 = self.m_np.getNetTransform().getMat()
|
||||
v39, _, v41 = v22.xformPoint(Point3(v57 * v34, 0.0, v16 * v34))
|
||||
v27, _, v28 = v22.xformPoint(Point3(-v57 * v34, 0.0, -v16 * v34))
|
||||
self.setRegion(Vec4(v39, v27, v41, v28), 0)
|
||||
|
||||
def setObjectCode(self, objcode):
|
||||
self.m_objcode = objcode
|
||||
|
||||
def getObjectCode(self):
|
||||
return self.m_objcode
|
||||
|
||||
def getScore(self):
|
||||
result = 2000
|
||||
|
||||
if self.m_culled:
|
||||
elapsed = globalClock.getFrameTime() - self.m_time
|
||||
result -= elapsed * 200
|
||||
|
||||
# moved from considerManage:
|
||||
if elapsed > 15.0:
|
||||
self.unmanage(self.m_manager)
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
CFSpeech = 1
|
||||
CFThought = 2
|
||||
CFQuicktalker = 4
|
||||
CFTimeout = 8
|
||||
CFPageButton = 16
|
||||
CFQuitButton = 32
|
||||
CFReversed = 64
|
||||
CFSndOpenchat = 128
|
||||
CFNoQuitButton = 256
|
||||
|
||||
# Some nametag modules expect PGButton to be in this namespace via
|
||||
# `from ._constants import *`.
|
||||
from direct.gui.DirectGuiGlobals import PGButton
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,410 @@
|
|||
"""Procedural code-generated sky system for Toontown.
|
||||
|
||||
Replaces the hood.sky model-based sky with a GLSL-driven atmospheric sky dome
|
||||
that includes:
|
||||
• Rayleigh + Mie scattering sky gradient
|
||||
• Wide sunset/sunrise horizon corona
|
||||
• Visible SUN DISC with corona, limb darkening, blinding glare
|
||||
• Volumetric-looking FBM cumulus clouds with domain warping
|
||||
• Twinkling star field with spectral colour variation
|
||||
• Moon disc with surface detail and corona (all night zones)
|
||||
• Full day/night cycle integration
|
||||
|
||||
Usage (from SkyUtil / OutdoorLighting)
|
||||
──────────────────────────────────────
|
||||
from toontown.hood.ProceduralSky import ProceduralSky
|
||||
|
||||
sky = ProceduralSky()
|
||||
# Prefer the lens NodePath (e.g. base.cam) so the dome matches the view matrix
|
||||
# used for rendering; base.camera is fine when it coincides with the lens.
|
||||
sky.attach(base.cam)
|
||||
sky.update(spec, timeOfDay) # call each frame (or at least on spec changes)
|
||||
sky.detach() # cleanup on zone exit
|
||||
|
||||
The ProceduralSky.update() signature accepts the same 'spec' dict as
|
||||
OutdoorLighting zone profiles, so integration is zero-cost.
|
||||
|
||||
Per-zone sky parameters (added to _ZONE_PROFILES by OutdoorLighting):
|
||||
cloudCoverage – 0.0–1.0
|
||||
cloudSpeed – relative speed multiplier
|
||||
cloudSharpness – 0.0 (soft) – 1.0 (sharp)
|
||||
turbidity – 1.0–8.0 (Mie haze)
|
||||
starBrightness – 0.0–1.0
|
||||
moonEnabled – bool (all night zones, not just DL)
|
||||
moonDir – (h,p,r) HPR for moon direction (matches keyHpr format)
|
||||
skyExposure – scalar (passed to post-process; default 1.0)
|
||||
sunBlindStrength– 0.0–1.0 blinding glare when looking at sun (default 0.85)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
from panda3d.core import (
|
||||
Geom,
|
||||
GeomNode,
|
||||
GeomTriangles,
|
||||
GeomVertexData,
|
||||
GeomVertexFormat,
|
||||
GeomVertexWriter,
|
||||
Filename,
|
||||
NodePath,
|
||||
Shader,
|
||||
Vec3,
|
||||
Vec4,
|
||||
)
|
||||
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
|
||||
import builtins
|
||||
base = getattr(builtins, 'base', None)
|
||||
|
||||
_SHADER_DIR_CANDIDATES = (
|
||||
os.path.join(os.path.dirname(__file__), '..', 'shaders'),
|
||||
os.path.join(os.path.dirname(__file__), 'shaders'),
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', 'shaders'),
|
||||
)
|
||||
_SHADER_DIR = next((d for d in _SHADER_DIR_CANDIDATES if os.path.exists(d)), _SHADER_DIR_CANDIDATES[0])
|
||||
_SKY_VERT = os.path.join(_SHADER_DIR, 'sky.vert.glsl')
|
||||
_SKY_FRAG = os.path.join(_SHADER_DIR, 'sky.frag.glsl')
|
||||
|
||||
_SKY_RADIUS = 950.0 # units – large enough to contain all Toontown geometry
|
||||
_CLOUD_BASE_SPEED = 0.60 # base cloud animation speed (modified per zone)
|
||||
|
||||
_sky_shader: Shader | None = None
|
||||
|
||||
|
||||
def _loadSkyShader() -> Shader | None:
|
||||
global _sky_shader
|
||||
if _sky_shader is not None:
|
||||
return _sky_shader
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: starting...")
|
||||
try:
|
||||
try:
|
||||
from toontown.hood import OutdoorLighting as osl
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: imported from toontown.hood")
|
||||
except ImportError:
|
||||
import OutdoorLighting as osl
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: imported from root")
|
||||
bisect_level = getattr(osl, '_OUTDOOR_SHADER_BISECT_LEVEL', 0)
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: osl._OUTDOOR_SHADER_BISECT_LEVEL={bisect_level}")
|
||||
if bisect_level < 1:
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: bisect level < 1, returning None")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: exception in osl import/check: {e!r}")
|
||||
pass
|
||||
try:
|
||||
vp_os = os.path.normpath(_SKY_VERT)
|
||||
fp_os = os.path.normpath(_SKY_FRAG)
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: vp_os={vp_os}, fp_os={fp_os}")
|
||||
vp_exists = os.path.isfile(vp_os)
|
||||
fp_exists = os.path.isfile(fp_os)
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: vp_exists={vp_exists}, fp_exists={fp_exists}")
|
||||
if not (vp_exists and fp_exists):
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: file missing, returning None")
|
||||
return None
|
||||
# On Windows, Panda3D's shader loader expects Panda-style paths (eg
|
||||
# `/c/Users/...`) rather than raw OS paths with backslashes.
|
||||
vp = Filename.fromOsSpecific(vp_os)
|
||||
fp = Filename.fromOsSpecific(fp_os)
|
||||
try:
|
||||
vp.makeTrueCase()
|
||||
fp.makeTrueCase()
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: loading shader...")
|
||||
_sky_shader = Shader.load(Shader.SL_GLSL, vp, fp)
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: load returned {_sky_shader}")
|
||||
return _sky_shader
|
||||
except Exception as e:
|
||||
print(f"[DEBUG VideoSettings] _loadSkyShader: exception: {e!r}")
|
||||
import traceback; traceback.print_exc()
|
||||
_sky_shader = None
|
||||
return None
|
||||
|
||||
|
||||
def _makeSkySphereMesh(radius: float = _SKY_RADIUS,
|
||||
latSegs: int = 18,
|
||||
lonSegs: int = 36) -> NodePath:
|
||||
"""Build an inward-facing UV sphere NodePath.
|
||||
|
||||
The sphere is centred at the origin in model space. When attached to the
|
||||
camera NodePath the camera is always at the sphere centre, so every vertex
|
||||
position is a ray direction from the camera.
|
||||
"""
|
||||
vfmt = GeomVertexFormat.getV3()
|
||||
vdata = GeomVertexData('skyDomeMesh', vfmt, Geom.UHStatic)
|
||||
vdata.setNumRows((latSegs + 1) * (lonSegs + 1))
|
||||
vwrite = GeomVertexWriter(vdata, 'vertex')
|
||||
|
||||
for lat in range(latSegs + 1):
|
||||
phi = math.pi * lat / latSegs # 0 → π (north pole → south pole)
|
||||
sp = math.sin(phi)
|
||||
cp = math.cos(phi)
|
||||
for lon in range(lonSegs + 1):
|
||||
theta = 2.0 * math.pi * lon / lonSegs
|
||||
x = radius * sp * math.cos(theta)
|
||||
y = radius * sp * math.sin(theta)
|
||||
z = radius * cp
|
||||
vwrite.addData3(x, y, z)
|
||||
|
||||
tris = GeomTriangles(Geom.UHStatic)
|
||||
stride = lonSegs + 1
|
||||
for lat in range(latSegs):
|
||||
for lon in range(lonSegs):
|
||||
v0 = lat * stride + lon
|
||||
v1 = lat * stride + lon + 1
|
||||
v2 = (lat + 1) * stride + lon
|
||||
v3 = (lat + 1) * stride + lon + 1
|
||||
# Inward-facing: flip winding compared to outward sphere.
|
||||
tris.addVertices(v0, v2, v1)
|
||||
tris.addVertices(v1, v2, v3)
|
||||
tris.closePrimitive()
|
||||
|
||||
geom = Geom(vdata)
|
||||
geom.addPrimitive(tris)
|
||||
gnode = GeomNode('skyDomeGeom')
|
||||
gnode.addGeom(geom)
|
||||
return NodePath(gnode)
|
||||
|
||||
|
||||
def _dir_world_to_cam(world_dir: Vec3) -> Vec3:
|
||||
"""Map a world-space *direction* into the active camera's local space.
|
||||
|
||||
The sky dome is parented to ``base.cam``, so ``vDir`` in the GLSL fragment
|
||||
shader is in **camera space**. ``sunDir`` / ``moonDir`` must match that
|
||||
space or dot products (sun disc, clouds, Mie) are wrong and the sky looks
|
||||
like a flat clear colour with no sun or clouds.
|
||||
"""
|
||||
try:
|
||||
cam = getattr(base, 'cam', None)
|
||||
rnp = getattr(base, 'render', None)
|
||||
if cam is None or rnp is None or cam.isEmpty() or rnp.isEmpty():
|
||||
return Vec3(world_dir)
|
||||
v = cam.getRelativeVector(rnp, Vec3(world_dir))
|
||||
ln = v.length()
|
||||
if ln > 1.0e-7:
|
||||
v /= ln
|
||||
return v
|
||||
except Exception:
|
||||
return Vec3(world_dir)
|
||||
|
||||
|
||||
def _hprToDir(h_deg: float, p_deg: float) -> Vec3:
|
||||
"""Convert a Panda3D HPR heading/pitch to a world-space direction vector.
|
||||
|
||||
The direction returned is the *forward* vector that a node with (H, P, 0)
|
||||
orientation points toward. For the sun key light this is the direction the
|
||||
light shines (scene ← sun); for sun position in the sky pass the negated
|
||||
result.
|
||||
"""
|
||||
h = math.radians(h_deg)
|
||||
p = math.radians(p_deg)
|
||||
# Panda3D right-hand Y-forward Z-up:
|
||||
# H rotates around Z (clockwise from above, i.e. left-hand around Z)
|
||||
# P rotates around X after H
|
||||
x = math.sin(h) * math.cos(p)
|
||||
y = -math.cos(h) * math.cos(p)
|
||||
z = -math.sin(p)
|
||||
return Vec3(x, y, z)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Per-zone cloud + sky parameters defaults
|
||||
# (OutdoorLighting zones may override any of these in their profile dict)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_ZONE_SKY_DEFAULTS: dict[str, dict] = {
|
||||
'tt': {'cloudCoverage': 0.42, 'cloudSpeed': 0.70, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dd': {'cloudCoverage': 0.88, 'cloudSpeed': 1.10, 'cloudSharpness': 0.20,
|
||||
'turbidity': 5.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dg': {'cloudCoverage': 0.28, 'cloudSpeed': 0.55, 'cloudSharpness': 0.70,
|
||||
'turbidity': 1.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'mm': {'cloudCoverage': 0.55, 'cloudSpeed': 0.85, 'cloudSharpness': 0.45,
|
||||
'turbidity': 3.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'br': {'cloudCoverage': 0.78, 'cloudSpeed': 1.40, 'cloudSharpness': 0.15,
|
||||
'turbidity': 6.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dl': {'cloudCoverage': 0.22, 'cloudSpeed': 0.25, 'cloudSharpness': 0.50,
|
||||
'turbidity': 1.5, 'starBrightness': 0.92, 'moonEnabled': True,
|
||||
'moonDir': (225, -55, 0)},
|
||||
'gs': {'cloudCoverage': 0.32, 'cloudSpeed': 0.90, 'cloudSharpness': 0.50,
|
||||
'turbidity': 3.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'estate': {'cloudCoverage': 0.38, 'cloudSpeed': 0.60, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'playground': {'cloudCoverage': 0.35, 'cloudSpeed': 0.65, 'cloudSharpness': 0.50,
|
||||
'turbidity': 2.4, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
# HQ zones: minimal sky (player rarely sees it indoors/dense area)
|
||||
'sellbot_hq': {'cloudCoverage': 0.95, 'cloudSpeed': 0.30, 'cloudSharpness': 0.05,
|
||||
'turbidity': 8.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'cashbot_hq': {'cloudCoverage': 0.85, 'cloudSpeed': 0.40, 'cloudSharpness': 0.10,
|
||||
'turbidity': 7.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'lawbot_hq': {'cloudCoverage': 0.92, 'cloudSpeed': 0.20, 'cloudSharpness': 0.10,
|
||||
'turbidity': 7.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'bossbot_hq': {'cloudCoverage': 0.99, 'cloudSpeed': 0.10, 'cloudSharpness': 0.05,
|
||||
'turbidity': 8.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'cog': {'cloudCoverage': 0.75, 'cloudSpeed': 0.50, 'cloudSharpness': 0.20,
|
||||
'turbidity': 6.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
# Street variants
|
||||
'tt_street': {'cloudCoverage': 0.40, 'cloudSpeed': 0.68, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.4, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dd_street': {'cloudCoverage': 0.90, 'cloudSpeed': 1.20, 'cloudSharpness': 0.18,
|
||||
'turbidity': 5.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dg_street': {'cloudCoverage': 0.24, 'cloudSpeed': 0.52, 'cloudSharpness': 0.72,
|
||||
'turbidity': 1.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'mm_street': {'cloudCoverage': 0.52, 'cloudSpeed': 0.88, 'cloudSharpness': 0.42,
|
||||
'turbidity': 3.6, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'br_street': {'cloudCoverage': 0.80, 'cloudSpeed': 1.50, 'cloudSharpness': 0.12,
|
||||
'turbidity': 6.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dl_street': {'cloudCoverage': 0.20, 'cloudSpeed': 0.22, 'cloudSharpness': 0.48,
|
||||
'turbidity': 1.5, 'starBrightness': 0.88, 'moonEnabled': True,
|
||||
'moonDir': (225, -55, 0)},
|
||||
'golf_course': {'cloudCoverage': 0.30, 'cloudSpeed': 0.65, 'cloudSharpness': 0.60,
|
||||
'turbidity': 2.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ProceduralSky class
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class ProceduralSky:
|
||||
"""Manages a GLSL-driven procedural sky sphere.
|
||||
|
||||
One instance per hood / zone. OutdoorLighting creates and destroys it
|
||||
alongside the light rig.
|
||||
"""
|
||||
|
||||
_TASK_NAME = 'proceduralSkyTask'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._skyNp: NodePath | None = None
|
||||
self._time: float = 0.0
|
||||
self._activeStyle: str = 'playground'
|
||||
self._attached: bool = False
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def attach(self, parent: NodePath, style: str = 'playground') -> None:
|
||||
"""Create the sky sphere and attach it to *parent* (usually camera)."""
|
||||
if self._attached:
|
||||
return
|
||||
shader = _loadSkyShader()
|
||||
if shader is None:
|
||||
return # graceful fallback – sky model will be used instead
|
||||
|
||||
try:
|
||||
self._skyNp = _makeSkySphereMesh()
|
||||
self._skyNp.reparentTo(parent)
|
||||
self._skyNp.setDepthTest(False)
|
||||
self._skyNp.setDepthWrite(False)
|
||||
self._skyNp.setLightOff(1)
|
||||
# Ensure the procedural sky is drawn after any legacy/model sky that
|
||||
# might also live in the background bin.
|
||||
self._skyNp.setBin('background', 1000)
|
||||
self._skyNp.setTwoSided(True)
|
||||
self._skyNp.setShader(shader)
|
||||
self._skyNp.setShaderAuto(False)
|
||||
|
||||
self._activeStyle = style
|
||||
self._attached = True
|
||||
self._time = 0.0
|
||||
|
||||
# No update task here – OutdoorLighting's main task calls update().
|
||||
except Exception as e:
|
||||
import traceback; traceback.print_exc()
|
||||
self._skyNp = None
|
||||
|
||||
def update(self, spec: dict, timeOfDay: float = 12.0) -> None:
|
||||
"""Push zone spec parameters as shader uniforms."""
|
||||
if not self._attached or self._skyNp is None or self._skyNp.isEmpty():
|
||||
return
|
||||
|
||||
self._time += globalClock.getDt()
|
||||
|
||||
# Resolve sky sub-parameters (check spec first, fall back to defaults).
|
||||
style = self._activeStyle
|
||||
skyDef = _ZONE_SKY_DEFAULTS.get(style, _ZONE_SKY_DEFAULTS['playground'])
|
||||
cov = float(spec.get('cloudCoverage', skyDef.get('cloudCoverage', 0.4)))
|
||||
spd = float(spec.get('cloudSpeed', skyDef.get('cloudSpeed', 0.6)))
|
||||
sharp = float(spec.get('cloudSharpness', skyDef.get('cloudSharpness', 0.5)))
|
||||
turb = float(spec.get('turbidity', skyDef.get('turbidity', 2.5)))
|
||||
stars = float(spec.get('starBrightness', skyDef.get('starBrightness', 0.0)))
|
||||
moonOn = float(1 if spec.get('moonEnabled', skyDef.get('moonEnabled', False)) else 0)
|
||||
moonHpr = spec.get('moonDir', skyDef.get('moonDir', (0, -45, 0)))
|
||||
|
||||
# Sun direction: forward vector of key light (direction light shines).
|
||||
keyHpr = spec.get('keyHpr', (135, -42, 0))
|
||||
lightFwd = _hprToDir(keyHpr[0], keyHpr[1])
|
||||
# Sun position in sky = opposite of light direction (world space).
|
||||
sunDirWorld = Vec3(-lightFwd.x, -lightFwd.y, -lightFwd.z)
|
||||
|
||||
moonDirWorld = Vec3(0, 0, 1)
|
||||
if moonOn > 0.5:
|
||||
mfwd = _hprToDir(moonHpr[0], moonHpr[1])
|
||||
moonDirWorld = Vec3(-mfwd.x, -mfwd.y, -mfwd.z)
|
||||
|
||||
# Must match camera-space ``vDir`` in the shader (dome is under base.cam).
|
||||
sunDirCam = _dir_world_to_cam(sunDirWorld)
|
||||
moonDirCam = _dir_world_to_cam(moonDirWorld)
|
||||
|
||||
keyColor = Vec4(*spec.get('key', (1, 1, 1, 1)))
|
||||
clearColor = spec.get('clearColor', (0.4, 0.6, 0.85, 1.0))
|
||||
fogColorV = spec.get('fogColor', (0.6, 0.7, 0.85, 1.0))
|
||||
skyScaleV = spec.get('skyScale', (1, 1, 1, 1))
|
||||
|
||||
# Derive zenith and horizon colours from clearColor and fogColor.
|
||||
# zenith = clearColor (the "perfect overhead blue")
|
||||
# horizon = blend clearColor → fogColor
|
||||
zenith = Vec3(clearColor[0], clearColor[1], clearColor[2])
|
||||
horizon = Vec3(fogColorV[0] * 0.85, fogColorV[1] * 0.85, fogColorV[2] * 0.85)
|
||||
|
||||
# Day/night: fade stars from *world* sun elevation (stable when camera tilts).
|
||||
isDaytime = max(0.0, min(1.0, (sunDirWorld.z + 0.2) * 4.0))
|
||||
effectiveStars = stars * (1.0 - isDaytime)
|
||||
|
||||
# Sun blind strength: default 0.85 (strong cinematic glare), clamped 0–1
|
||||
blindStr = float(spec.get('sunBlindStrength', 0.85))
|
||||
blindStr = max(0.0, min(1.0, blindStr))
|
||||
|
||||
try:
|
||||
np = self._skyNp
|
||||
np.setShaderInput('sunDir', sunDirCam)
|
||||
np.setShaderInput('sunWorldElev', float(sunDirWorld.z))
|
||||
np.setShaderInput('sunColor', keyColor)
|
||||
np.setShaderInput('zenithColor', zenith)
|
||||
np.setShaderInput('horizonColor', horizon)
|
||||
np.setShaderInput('fogColor', Vec3(fogColorV[0], fogColorV[1], fogColorV[2]))
|
||||
np.setShaderInput('cloudCoverage', cov)
|
||||
np.setShaderInput('cloudSpeed', spd * _CLOUD_BASE_SPEED)
|
||||
np.setShaderInput('cloudSharpness', sharp)
|
||||
np.setShaderInput('turbidity', turb)
|
||||
np.setShaderInput('starBrightness', effectiveStars)
|
||||
np.setShaderInput('moonEnabled', moonOn)
|
||||
np.setShaderInput('moonDir', moonDirCam)
|
||||
np.setShaderInput('moonColor', Vec4(*spec.get('key', (0.5, 0.6, 1, 1))))
|
||||
np.setShaderInput('time', self._time)
|
||||
np.setShaderInput('skyScale', Vec4(*skyScaleV))
|
||||
# New feature uniforms
|
||||
np.setShaderInput('sunDiscEnabled', 1.0)
|
||||
np.setShaderInput('sunBlindStrength', blindStr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def setStyle(self, style: str) -> None:
|
||||
"""Update which zone sky defaults to use."""
|
||||
self._activeStyle = style
|
||||
|
||||
def isActive(self) -> bool:
|
||||
return self._attached and self._skyNp is not None and not self._skyNp.isEmpty()
|
||||
|
||||
def detach(self) -> None:
|
||||
"""Remove the sky sphere and clean up."""
|
||||
if self._skyNp and not self._skyNp.isEmpty():
|
||||
self._skyNp.removeNode()
|
||||
self._skyNp = None
|
||||
self._attached = False
|
||||
self._time = 0.0
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,11 +2,15 @@ import sys
|
|||
import os
|
||||
import tokenize
|
||||
import copy
|
||||
|
||||
import typing
|
||||
from direct.interval.IntervalGlobal import *
|
||||
from direct.directnotify import DirectNotifyGlobal
|
||||
from panda3d.core import *
|
||||
from panda3d.otp import *
|
||||
from direct.showbase import DirectObject
|
||||
|
||||
from libotp import CFSpeech, CFTimeout
|
||||
from libotp.nametag import _constants
|
||||
from . import BlinkingArrows
|
||||
from toontown.toon import ToonHeadFrame
|
||||
from toontown.char import CharDNA
|
||||
|
|
@ -24,11 +28,19 @@ lineDict = {}
|
|||
globalVarDict = {}
|
||||
curId = None
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from toontown.toonbase.ToonBaseGlobals import *
|
||||
|
||||
def init():
|
||||
globalVarDict.update({'render': render,
|
||||
'camera': camera,
|
||||
'hidden': hidden,
|
||||
'aspect2d': aspect2d,
|
||||
'bottomLeft' : base.a2dBottomLeft,
|
||||
'topLeft' : base.a2dTopLeft,
|
||||
'topRight' : base.a2dTopRight,
|
||||
'bottomRight' : base.a2dBottomRight,
|
||||
'localToon': base.localAvatar,
|
||||
'laffMeter': base.localAvatar.laffMeter,
|
||||
'inventory': base.localAvatar.inventory,
|
||||
|
|
@ -74,18 +86,18 @@ def getLineOfTokens(gen):
|
|||
if token[0] == tokenize.ENDMARKER:
|
||||
return None
|
||||
while token[0] != tokenize.NEWLINE and token[0] != tokenize.NL:
|
||||
if token[0] in (tokenize.COMMENT, tokenize.ENCODING):
|
||||
if token[0] == tokenize.COMMENT:
|
||||
pass
|
||||
elif token[0] == tokenize.OP and token[1] == '-':
|
||||
nextNeg = 1
|
||||
elif token[0] == tokenize.NUMBER:
|
||||
if nextNeg:
|
||||
tokens.append(-eval(token[1]))
|
||||
tokens.append(-float(token[1]))
|
||||
nextNeg = 0
|
||||
else:
|
||||
tokens.append(eval(token[1]))
|
||||
tokens.append(float(token[1]))
|
||||
elif token[0] == tokenize.STRING:
|
||||
tokens.append(eval(token[1]))
|
||||
tokens.append(str(token[1]))
|
||||
elif token[0] == tokenize.NAME:
|
||||
tokens.append(token[1])
|
||||
else:
|
||||
|
|
@ -639,7 +651,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
toonId = self.toon.getDoId()
|
||||
avatarName = line[1]
|
||||
avatar = self.getVar(avatarName)
|
||||
chatString = eval('TTLocalizer.' + line[2])
|
||||
chatString = getattr(TTLocalizer, line[2])
|
||||
chatFlags = CFSpeech | CFTimeout
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[3:])
|
||||
if extraChatFlags:
|
||||
|
|
@ -666,7 +678,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
quitButton = arg
|
||||
elif type(arg) == type(''):
|
||||
if len(arg) > 2 and arg[:2] == 'CF':
|
||||
extraChatFlags = eval(arg)
|
||||
extraChatFlags = getattr(_constants, str(arg))
|
||||
else:
|
||||
dialogueList.append(self.getVar(arg))
|
||||
else:
|
||||
|
|
@ -679,7 +691,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
toonId = self.toon.getDoId()
|
||||
avatarName = line[1]
|
||||
avatar = self.getVar(avatarName)
|
||||
chatString = eval('TTLocalizer.' + line[2])
|
||||
chatString = getattr(TTLocalizer, line[2])
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[3:])
|
||||
return Func(avatar.setPageChat, toonId, 0, chatString, quitButton, extraChatFlags, dialogueList)
|
||||
|
||||
|
|
@ -687,7 +699,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
lineLength = len(line)
|
||||
avatarName = line[1]
|
||||
avatar = self.getVar(avatarName)
|
||||
chatString = eval('TTLocalizer.' + line[2])
|
||||
chatString = getattr(TTLocalizer, line[2])
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[3:])
|
||||
return Func(avatar.setLocalPageChat, chatString, quitButton, extraChatFlags, dialogueList)
|
||||
|
||||
|
|
@ -695,7 +707,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
lineLength = len(line)
|
||||
avatarName = line[1]
|
||||
avatar = self.getVar(avatarName)
|
||||
chatString = eval('TTLocalizer.' + line[2])
|
||||
chatString = getattr(TTLocalizer, line[2])
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[3:])
|
||||
if len(dialogueList) > 0:
|
||||
dialogue = dialogueList[0]
|
||||
|
|
@ -710,8 +722,8 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
toAvatarKey = line[2]
|
||||
toAvatar = self.getVar(toAvatarKey)
|
||||
localizerAvatarName = toAvatar.getName().capitalize()
|
||||
toAvatarName = eval('TTLocalizer.' + localizerAvatarName)
|
||||
chatString = eval('TTLocalizer.' + line[3])
|
||||
toAvatarName = getattr(TTLocalizer, localizerAvatarName)
|
||||
chatString = getattr(TTLocalizer, line[3])
|
||||
chatString = chatString.replace('%s', toAvatarName)
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[4:])
|
||||
return Func(avatar.setLocalPageChat, chatString, quitButton, extraChatFlags, dialogueList)
|
||||
|
|
@ -721,9 +733,9 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
avatarName = line[1]
|
||||
avatar = self.getVar(avatarName)
|
||||
if self.toon.getStyle().gender == 'm':
|
||||
chatString = eval('TTLocalizer.' + line[2] % 'Mickey')
|
||||
chatString = getattr(TTLocalizer, (line[2] % 'Mickey').replace('"', ""))
|
||||
else:
|
||||
chatString = eval('TTLocalizer.' + line[2] % 'Minnie')
|
||||
chatString = getattr(TTLocalizer, (line[2] % 'Minnie').replace('"', ""))
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[3:])
|
||||
return Func(avatar.setLocalPageChat, chatString, quitButton, extraChatFlags, dialogueList)
|
||||
|
||||
|
|
@ -734,11 +746,11 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
toAvatarKey = line[2]
|
||||
toAvatar = self.getVar(toAvatarKey)
|
||||
localizerAvatarName = toAvatar.getName().capitalize()
|
||||
toAvatarName = eval('TTLocalizer.' + localizerAvatarName)
|
||||
toAvatarName = getattr(TTLocalizer, localizerAvatarName)
|
||||
if self.toon.getStyle().gender == 'm':
|
||||
chatString = eval('TTLocalizer.' + line[3] % 'Mickey')
|
||||
chatString = getattr(TTLocalizer, (line[3] % 'Mickey').replace('"', ""))
|
||||
else:
|
||||
chatString = eval('TTLocalizer.' + line[3] % 'Minnie')
|
||||
chatString = getattr(TTLocalizer, (line[3] % 'Minnie').replace('"', ""))
|
||||
chatString = chatString.replace('%s', toAvatarName)
|
||||
quitButton, extraChatFlags, dialogueList = self.parseExtraChatArgs(line[4:])
|
||||
return Func(avatar.setLocalPageChat, chatString, quitButton, extraChatFlags, dialogueList)
|
||||
|
|
@ -855,9 +867,18 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
|
||||
def parseFunction(self, line):
|
||||
token, objectName, functionName = line
|
||||
objectName = objectName.replace('"', "")
|
||||
functionName = functionName.replace('"', "")
|
||||
object = self.getVar(objectName)
|
||||
cfunc = compile('object' + '.' + functionName, '<string>', 'eval')
|
||||
return Func(eval(cfunc))
|
||||
# This is hacky, but it works.
|
||||
if "questPage" in functionName:
|
||||
if "showQuestsOnscreenTutorial" in functionName:
|
||||
return Func(base.localAvatar.questPage.showQuestsOnscreenTutorial)
|
||||
elif "hideQuestsOnscreenTutorial" in functionName:
|
||||
return Func(base.localAvatar.questPage.hideQuestsOnscreenTutorial)
|
||||
else:
|
||||
cfunc = getattr(object, functionName)
|
||||
return Func(cfunc)
|
||||
|
||||
def parseAddLaffMeter(self, line):
|
||||
token, maxHpDelta = line
|
||||
|
|
@ -986,7 +1007,7 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
token, enable = line
|
||||
if enable:
|
||||
|
||||
def phraseSaid(phraseId):
|
||||
def phraseSaid(phraseId, displayType):
|
||||
toontastic = 315
|
||||
if phraseId == toontastic:
|
||||
messenger.send(DistributedBlackCatMgr.DistributedBlackCatMgr.ActivateEvent)
|
||||
|
|
@ -1066,9 +1087,9 @@ class NPCMoviePlayer(DirectObject.DirectObject):
|
|||
return Wait(0.0)
|
||||
|
||||
|
||||
vfs = VirtualFileSystem.getGlobalPtr()
|
||||
searchPath = DSearchPath()
|
||||
if __debug__:
|
||||
searchPath.appendDirectory(Filename('resources/phase_3/etc'))
|
||||
searchPath.appendDirectory(Filename('/phase_3/etc'))
|
||||
scriptFile = Filename('QuestScripts.txt')
|
||||
found = vfs.resolveFilename(scriptFile, searchPath)
|
||||
if not found:
|
||||
|
|
|
|||
|
|
@ -17675,6 +17675,19 @@ QuestDict = {
|
|||
NA,
|
||||
TTLocalizer.QuestDialogDict[12032])}
|
||||
|
||||
WANT_LEGACY_QUESTS = True
|
||||
|
||||
if WANT_LEGACY_QUESTS:
|
||||
from toontown.quest import LegacyQuestDict
|
||||
for key, value in LegacyQuestDict.QuestDict.items():
|
||||
QuestDict[key] = value
|
||||
|
||||
def questExists(questId) -> bool:
|
||||
try:
|
||||
return int(questId) in QuestDict
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
Tier2QuestsDict = {}
|
||||
for questId, questDesc in list(QuestDict.items()):
|
||||
if questDesc[QuestDictStartIndex] == Start:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Vertex shader for the HDR scene composite quad rendered through a
|
||||
// 3D orthographic FilterManager camera. Unlike the render2dp overlay
|
||||
// path (sunrays.vert.glsl), the quad lives in model-space (XZ plane,
|
||||
// Y=0), so the full ModelViewProjection transform is required to map it
|
||||
// to clip space correctly.
|
||||
#version 130
|
||||
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
in vec4 p3d_Vertex;
|
||||
in vec2 p3d_MultiTexCoord0;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
|
||||
uv = p3d_MultiTexCoord0;
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
// Scene post-processing composite fragment shader.
|
||||
//
|
||||
// Single-pass pipeline that reads a FilterManager-captured scene and applies:
|
||||
// 1. Depth-buffer-occluded screen-space god rays (Kenny Mitchell technique,
|
||||
// GPU Gems 3 Ch. 13). Sky pixels (depth ≈ 1.0) let light through;
|
||||
// solid geometry pixels block it, creating real geometry-cast light shafts.
|
||||
// 2. Approximate single-pass bloom (bright-pass + large kernel box blur
|
||||
// at multiple offsets — not physically perfect but fast and convincing).
|
||||
// 3. ACES filmic tonemapping with per-zone exposure adjustment.
|
||||
//
|
||||
// Uniforms set by OutdoorLighting._setupPostProcess():
|
||||
// sceneColor – RGBA scene texture (float16 or RGBA8 offscreen; exposure+ACES below)
|
||||
// sceneDepth – depth texture matching sceneColor dimensions
|
||||
// sunScreenPos – sun position in [0,1] screen UV space
|
||||
// rayColor – god-ray tint (RGBA)
|
||||
// rayIntensity – master ray strength (0 = off)
|
||||
// bloomIntensity – bloom strength (0 = off)
|
||||
// bloomThreshold – luminance threshold for bright-pass
|
||||
// exposure – scene exposure scalar (default 1.0)
|
||||
// tonemapEnabled – 0 = bypass tonemap (linear output), 1 = ACES
|
||||
// vignetteStrength – 0 = off, 0.25 = subtle, 1 = strong
|
||||
// time – animation seconds (unused here; reserved for shimmer)
|
||||
// texelSize – vec2(1/width, 1/height) for blur offsets
|
||||
#version 130
|
||||
|
||||
uniform sampler2D sceneColor;
|
||||
uniform sampler2D sceneDepth;
|
||||
uniform vec2 sunScreenPos;
|
||||
uniform vec4 rayColor;
|
||||
uniform float rayIntensity;
|
||||
// Bloom is temporarily disabled in code to avoid a Panda3D shader input
|
||||
// assertion on some drivers; keep the effect path available for later.
|
||||
uniform float exposure;
|
||||
uniform float tonemapEnabled;
|
||||
uniform float vignetteStrength;
|
||||
uniform float time;
|
||||
uniform vec2 texelSize;
|
||||
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ── ACES filmic tonemapper ───────────────────────────────────────────────────
|
||||
// Fitted curve by Krzysztof Narkowicz (2015). Very close to the full ACES
|
||||
// reference at a fraction of the cost.
|
||||
vec3 acesTonemap(vec3 x) {
|
||||
const float a = 2.51;
|
||||
const float b = 0.03;
|
||||
const float c = 2.43;
|
||||
const float d = 0.59;
|
||||
const float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// ── Reinhard (per-channel) ───────────────────────────────────────────────────
|
||||
vec3 reinhardTonemap(vec3 x) {
|
||||
return x / (1.0 + x);
|
||||
}
|
||||
|
||||
// ── Luminance helper ─────────────────────────────────────────────────────────
|
||||
float luminance(vec3 c) {
|
||||
return dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
// ── Screen-space depth-occluded god rays ─────────────────────────────────────
|
||||
//
|
||||
// Algorithm: march from the current pixel toward the sun position in screen
|
||||
// space (NUM_SAMPLES steps). At each step sample the depth buffer.
|
||||
// depth == 1.0 (or very close) → sky pixel → sun is visible → accumulate
|
||||
// depth < DEPTH_THRESHOLD → geometry pixel → occluded → skip
|
||||
//
|
||||
// The accumulated value is weighted by an exponential decay so samples nearer
|
||||
// the sun contribute more. The classic GPU Gems 3 weighting applies.
|
||||
vec3 godRays(vec2 pixelUV, float intensity) {
|
||||
if (intensity <= 0.001) return vec3(0.0);
|
||||
|
||||
const int NUM_SAMPLES = 96;
|
||||
const float DECAY = 0.966;
|
||||
const float DENSITY = 0.84;
|
||||
const float WEIGHT = 0.45;
|
||||
const float EXPOSURE_RAY = 0.16;
|
||||
const float DEPTH_THRESHOLD = 0.9998; // sky depth threshold
|
||||
|
||||
// Cull when the sun is completely off-screen to avoid aliasing artefacts.
|
||||
vec2 sunEdgeDist = min(sunScreenPos, 1.0 - sunScreenPos);
|
||||
float edgeFade = smoothstep(0.0, 0.08, min(sunEdgeDist.x, sunEdgeDist.y));
|
||||
if (edgeFade <= 0.0) return vec3(0.0);
|
||||
|
||||
vec2 delta = (pixelUV - sunScreenPos) * (DENSITY / float(NUM_SAMPLES));
|
||||
vec2 sampleUV = pixelUV;
|
||||
float illum = 0.0;
|
||||
float decay = 1.0;
|
||||
|
||||
for (int i = 0; i < NUM_SAMPLES; ++i) {
|
||||
sampleUV -= delta;
|
||||
// Clamp so we don't sample outside the texture.
|
||||
vec2 cUV = clamp(sampleUV, vec2(0.001), vec2(0.999));
|
||||
float d = texture(sceneDepth, cUV).r;
|
||||
// Sky pixels (d ≥ DEPTH_THRESHOLD) are unoccluded → contribute.
|
||||
float sky = step(DEPTH_THRESHOLD, d);
|
||||
illum += sky * decay * WEIGHT;
|
||||
decay *= DECAY;
|
||||
}
|
||||
illum *= EXPOSURE_RAY * intensity * edgeFade;
|
||||
|
||||
return rayColor.rgb * illum;
|
||||
}
|
||||
|
||||
// ── Single-pass approximate bloom ───────────────────────────────────────────
|
||||
//
|
||||
// Extracts bright pixels then blurs with a two-ring sample pattern (Poisson
|
||||
// disc approximation). Not as smooth as multi-pass Gaussian but avoids the
|
||||
// need for ping-pong buffers, keeping us in one FilterManager pass.
|
||||
vec3 bloom(vec2 pixUV, float threshold, float intensity) {
|
||||
if (intensity <= 0.001) return vec3(0.0);
|
||||
|
||||
vec3 acc = vec3(0.0);
|
||||
float total = 0.0;
|
||||
|
||||
// Two rings: inner (4 samples) + outer (8 samples)
|
||||
// Offsets are in texel units; scale drives blur radius.
|
||||
float blurRadius = mix(3.0, 9.0, intensity);
|
||||
|
||||
vec2 offsets[12];
|
||||
// Inner ring
|
||||
offsets[0] = vec2( 1.0, 0.0);
|
||||
offsets[1] = vec2(-1.0, 0.0);
|
||||
offsets[2] = vec2( 0.0, 1.0);
|
||||
offsets[3] = vec2( 0.0, -1.0);
|
||||
// Mid ring
|
||||
offsets[4] = vec2( 1.5, 1.5);
|
||||
offsets[5] = vec2(-1.5, 1.5);
|
||||
offsets[6] = vec2( 1.5, -1.5);
|
||||
offsets[7] = vec2(-1.5, -1.5);
|
||||
// Outer ring
|
||||
offsets[8] = vec2( 3.0, 0.0);
|
||||
offsets[9] = vec2(-3.0, 0.0);
|
||||
offsets[10] = vec2( 0.0, 3.0);
|
||||
offsets[11] = vec2( 0.0, -3.0);
|
||||
|
||||
float weights[12];
|
||||
weights[0] = 1.00; weights[1] = 1.00;
|
||||
weights[2] = 1.00; weights[3] = 1.00;
|
||||
weights[4] = 0.70; weights[5] = 0.70;
|
||||
weights[6] = 0.70; weights[7] = 0.70;
|
||||
weights[8] = 0.35; weights[9] = 0.35;
|
||||
weights[10] = 0.35; weights[11] = 0.35;
|
||||
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
vec2 sUV = pixUV + offsets[i] * texelSize * blurRadius;
|
||||
vec3 col = texture(sceneColor, sUV).rgb;
|
||||
float lum = luminance(col);
|
||||
float bright = max(0.0, lum - threshold);
|
||||
acc += col * bright * weights[i];
|
||||
total += weights[i];
|
||||
}
|
||||
if (total > 0.0) acc /= total;
|
||||
|
||||
return acc * intensity * 1.4;
|
||||
}
|
||||
|
||||
// ── Vignette ────────────────────────────────────────────────────────────────
|
||||
float vignette(vec2 u, float strength) {
|
||||
vec2 d = u - 0.5;
|
||||
return 1.0 - dot(d, d) * strength * 3.2;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
vec4 sceneRGBA = texture(sceneColor, uv);
|
||||
vec3 col = sceneRGBA.rgb * exposure;
|
||||
|
||||
// ── God rays ─────────────────────────────────────────────────────────────
|
||||
col += godRays(uv, rayIntensity);
|
||||
|
||||
// ── Bloom ────────────────────────────────────────────────────────────────
|
||||
// Disabled for now (see note above).
|
||||
col += bloom(uv, 1.0, 0.0);
|
||||
|
||||
// ── Tonemapping ──────────────────────────────────────────────────────────
|
||||
if (tonemapEnabled > 0.5) {
|
||||
col = acesTonemap(col);
|
||||
} else {
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// ── Vignette ─────────────────────────────────────────────────────────────
|
||||
if (vignetteStrength > 0.001) {
|
||||
col *= clamp(vignette(uv, vignetteStrength), 0.0, 1.0);
|
||||
}
|
||||
|
||||
fragColor = vec4(col, sceneRGBA.a);
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
// Procedural physically-based sky fragment shader – MASSIVE REWRITE
|
||||
//
|
||||
// Implements (new and improved):
|
||||
// • Rayleigh + Mie scattering – accurate wavelength-dependent blue-sky gradient
|
||||
// • Wide sunset/sunrise horizon corona (orange-pink band near horizon)
|
||||
// • Ozone absorption – cyan/yellow sky transition near sunset
|
||||
// • Horizon haze / atmospheric extinction with turbidity control
|
||||
// • VISIBLE SUN DISC with:
|
||||
// – Physical angular radius (~0.27°)
|
||||
// – Limb darkening (edges slightly darker than centre)
|
||||
// – Colour shifts noon→white-yellow, sunset→deep orange-red
|
||||
// – Multi-layer exponential corona / aureole
|
||||
// – Subtle vertical lens-flare streak
|
||||
// – BLINDING GLARE when staring directly into the sun (chromatic aberration glow)
|
||||
// • Volumetric-looking FBM cumulus clouds with domain warping:
|
||||
// – Two-pass domain warp (organic, non-repetitive shapes)
|
||||
// – Multi-layer depth sampling (distinct top/middle/base layers)
|
||||
// – Sun back-lighting (silver lining on edges facing sun)
|
||||
// – Sunset orange-pink underbelly glow
|
||||
// – Moon-lit night-time clouds (cool blue-grey)
|
||||
// – Per-zone coverage, speed, sharpness
|
||||
// • Twinkling star field:
|
||||
// – Per-star unique twinkle frequency & phase
|
||||
// – Four spectral classes: blue-white / white / yellow-white / warm
|
||||
// – Occasional "sparkle" diffraction cross on bright stars
|
||||
// – Fades behind clouds and near the sun
|
||||
// • MOON disc with:
|
||||
// – Surface noise detail (craters)
|
||||
// – Limb darkening
|
||||
// – Multi-layer corona glow
|
||||
// – Cloud occlusion
|
||||
// • Zone colour scale (skyScale) applied to final output
|
||||
// • Reinhard tonemapping + gamma lift for natural HDR-to-LDR
|
||||
//
|
||||
// Uniforms set by ProceduralSky.update():
|
||||
// sunDir – world-space direction TOWARD the sun (normalised)
|
||||
// sunColor – key light colour; used for Mie glow tint
|
||||
// zenithColor – deep sky colour at zenith (zone)
|
||||
// horizonColor – sky colour at horizon (zone)
|
||||
// fogColor – atmospheric haze/fog tint (zone)
|
||||
// cloudCoverage – 0.0 (clear) … 1.0 (overcast) (zone)
|
||||
// cloudSpeed – cloud animation multiplier (zone)
|
||||
// cloudSharpness – edge sharpness 0.0 (fluffy) … 1.0 (sharp) (zone)
|
||||
// turbidity – Mie strength 1.0 (crisp) … 8.0 (hazy) (zone)
|
||||
// starBrightness – star intensity; 0 in day, up to 1 at night (zone)
|
||||
// moonEnabled – 1.0 = draw a moon disc
|
||||
// moonDir – world-space direction toward moon
|
||||
// moonColor – moon disc tint
|
||||
// time – animation seconds (drives cloud drift + star twinkle)
|
||||
// skyScale – vec4 colour multiplier (zone tint)
|
||||
// sunDiscEnabled – 1.0 = draw visible sun disc (default 1)
|
||||
// sunBlindStrength – 0.0–1.0 blinding glare when staring into sun
|
||||
// sunWorldElev – sun direction Z in *world* space [-1..1] (time-of-day);
|
||||
// separate from sunDir so atmosphere does not swim when the camera tilts
|
||||
#version 130
|
||||
|
||||
// ── Atmosphere uniforms ────────────────────────────────────────────────────
|
||||
uniform vec3 sunDir;
|
||||
uniform float sunWorldElev;
|
||||
uniform vec4 sunColor;
|
||||
uniform vec3 zenithColor;
|
||||
uniform vec3 horizonColor;
|
||||
uniform vec3 fogColor;
|
||||
// ── Cloud uniforms ─────────────────────────────────────────────────────────
|
||||
uniform float cloudCoverage;
|
||||
uniform float cloudSpeed;
|
||||
uniform float cloudSharpness;
|
||||
// ── Scattering ────────────────────────────────────────────────────────────
|
||||
uniform float turbidity;
|
||||
// ── Night sky ─────────────────────────────────────────────────────────────
|
||||
uniform float starBrightness;
|
||||
uniform float moonEnabled;
|
||||
uniform vec3 moonDir;
|
||||
uniform vec4 moonColor;
|
||||
// ── Animation ─────────────────────────────────────────────────────────────
|
||||
uniform float time;
|
||||
uniform vec4 skyScale;
|
||||
// ── New features ──────────────────────────────────────────────────────────
|
||||
uniform float sunDiscEnabled; // 1.0 = render sun disc + corona
|
||||
uniform float sunBlindStrength; // 0–1 glare when staring at sun
|
||||
|
||||
in vec3 vDir;
|
||||
in vec2 vUV;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Noise / hash utilities
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
float _hash(vec2 p) {
|
||||
p = fract(p * vec2(127.1, 311.7));
|
||||
p += dot(p, p + 19.19);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float _vnoise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(_hash(i), _hash(i + vec2(1.0, 0.0)), u.x),
|
||||
mix(_hash(i + vec2(0.0, 1.0)), _hash(i + vec2(1.0, 1.0)), u.x),
|
||||
u.y
|
||||
);
|
||||
}
|
||||
|
||||
// 8-octave FBM with per-octave rotation (breaks axis-aligned repetition)
|
||||
float _fbm(vec2 p) {
|
||||
float v = 0.0;
|
||||
float amp = 0.5;
|
||||
mat2 rot = mat2(1.6, 1.2, -1.2, 1.6);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
v += amp * _vnoise(p);
|
||||
p = rot * p * 2.1;
|
||||
amp *= 0.46;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Phase functions
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
float _mie(float cosA, float g) {
|
||||
float g2 = g * g;
|
||||
return (1.0 - g2) / pow(max(1.0e-4, 1.0 + g2 - 2.0 * g * cosA), 1.5) * 0.25;
|
||||
}
|
||||
|
||||
float _rayleigh(float cosA) {
|
||||
return 0.75 * (1.0 + cosA * cosA);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Main
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
vec3 dir = normalize(vDir);
|
||||
float elev = dir.z;
|
||||
float elevAbs = abs(elev);
|
||||
float cosTheta = dot(dir, sunDir);
|
||||
// World-space sun height (stable when camera pitches/yaws); sunDir is camera-space for dots.
|
||||
float sunElevW = sunWorldElev;
|
||||
// sunPower: 0 when sun is on or below horizon, rises to 1 near zenith
|
||||
float sunPower = clamp(sunElevW * 3.5 + 0.28, 0.0, 1.0);
|
||||
|
||||
// ── Rayleigh scattering ──────────────────────────────────────────────
|
||||
// Wavelength-dependent (shorter λ = blue scatters more)
|
||||
vec3 rayleigh_wl = vec3(0.26, 0.50, 1.00);
|
||||
float rayleighPhase = _rayleigh(cosTheta);
|
||||
vec3 rayleighCol = rayleigh_wl * rayleighPhase * mix(0.28, 1.05, sunPower);
|
||||
|
||||
// ── Mie scattering (turbidity-driven forward halo) ───────────────────
|
||||
float g = max(0.58, 0.84 - turbidity * 0.020);
|
||||
float mieStr = turbidity * 0.13;
|
||||
float mieGlow = _mie(cosTheta, g) * mieStr;
|
||||
vec3 mieCol = sunColor.rgb * mieGlow;
|
||||
|
||||
// ── Wide horizon corona at sunset/sunrise ────────────────────────────
|
||||
// Horizontal dot: measures how closely dir aligns with sun's azimuth at horizon
|
||||
vec2 sunAz = vec2(sunDir.x, sunDir.y);
|
||||
float sunAzLen = max(0.001, length(sunAz));
|
||||
float horizDot = dot(vec2(dir.x, dir.y), sunAz / sunAzLen);
|
||||
float horizBand = pow(max(0.0, horizDot), 3.5)
|
||||
* max(0.0, 1.0 - abs(sunElevW) * 3.8) // only near horizon
|
||||
* (1.0 - abs(elev) * 3.0) // fade away from horizon line
|
||||
* 0.70;
|
||||
vec3 horizGlowCol = mix(
|
||||
vec3(1.00, 0.42, 0.06), // deep orange at low elevation
|
||||
vec3(1.00, 0.80, 0.42), // golden higher up
|
||||
clamp(sunPower * 1.5, 0.0, 1.0)
|
||||
) * horizBand;
|
||||
|
||||
// ── Ozone absorption ─────────────────────────────────────────────────
|
||||
float ozone = max(0.0, 1.0 - elevAbs * 1.55);
|
||||
vec3 ozoneCol = vec3(0.00, 0.13, 0.22) * ozone * sunPower;
|
||||
|
||||
// ── Sky gradient (zenith → horizon) ──────────────────────────────────
|
||||
float horizonT = pow(clamp(elev * 1.25 + 0.14, 0.0, 1.0), 0.50);
|
||||
vec3 gradCol = mix(horizonColor, zenithColor, horizonT);
|
||||
|
||||
// Sunset/sunrise band: orange-pink near horizon, purple higher up
|
||||
float sunsetBand = max(0.0, 1.0 - abs(sunElevW) * 2.2) * (1.0 - horizonT * 0.75);
|
||||
vec3 sunsetTint = mix(
|
||||
vec3(1.00, 0.38, 0.05), // near-horizon orange
|
||||
vec3(0.55, 0.25, 0.72), // purple higher
|
||||
horizonT
|
||||
) * sunsetBand * 0.62;
|
||||
gradCol += sunsetTint;
|
||||
|
||||
// Assemble base sky
|
||||
vec3 sky = gradCol
|
||||
+ rayleighCol * 0.42
|
||||
+ mieCol
|
||||
+ ozoneCol
|
||||
+ horizGlowCol;
|
||||
|
||||
// ── Horizon haze (atmospheric extinction) ────────────────────────────
|
||||
float hazeT = exp(-max(0.0, elev) * 5.2 * turbidity * 0.27);
|
||||
sky = mix(sky, fogColor, hazeT * 0.52);
|
||||
|
||||
// ── Below horizon: fade to dark fog ──────────────────────────────────
|
||||
if (elev < 0.0) {
|
||||
float below = clamp(-elev * 9.5, 0.0, 1.0);
|
||||
sky = mix(sky, fogColor * 0.48, below);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Volumetric clouds
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
float cloudAlpha = 0.0;
|
||||
vec3 cloudRGB = vec3(1.0);
|
||||
|
||||
if (cloudCoverage > 0.02 && elev > -0.07) {
|
||||
// Perspective projection onto cloud layer at ~1 km altitude
|
||||
float layerScale = 1.0 / max(0.05, elev + 0.05);
|
||||
vec2 cloudUV = vec2(dir.x, dir.y) * layerScale * 0.36
|
||||
+ vec2(time * cloudSpeed * 0.00115, time * cloudSpeed * 0.00045);
|
||||
|
||||
// ── Domain warping pass 1 (large-scale organic distortion) ───────
|
||||
vec2 warp1 = vec2(
|
||||
_fbm(cloudUV * 1.55),
|
||||
_fbm(cloudUV * 1.55 + vec2(5.20, 1.30))
|
||||
) * 0.32;
|
||||
|
||||
// ── Domain warping pass 2 (medium-scale detail) ──────────────────
|
||||
vec2 warp2 = vec2(
|
||||
_fbm(cloudUV * 0.82 + vec2(1.70, 9.20)),
|
||||
_fbm(cloudUV * 0.82 + vec2(8.30, 2.80))
|
||||
) * 0.14;
|
||||
|
||||
vec2 warpedUV = cloudUV + warp1 + warp2;
|
||||
|
||||
// Primary cloud density field
|
||||
float density = _fbm(warpedUV * 2.35);
|
||||
|
||||
// Coverage → threshold
|
||||
float threshold = 1.0 - cloudCoverage * 0.76;
|
||||
float raw = density - threshold;
|
||||
float edgeWidth = mix(0.32, 0.045, cloudSharpness);
|
||||
float shaped = smoothstep(0.0, edgeWidth, raw);
|
||||
|
||||
// Horizon fade (perspective stretch makes low-angle clouds blur badly)
|
||||
shaped *= smoothstep(-0.04, 0.18, elev);
|
||||
|
||||
// ── Multi-layer depth sampling for 3-D cloud body feel ──────────
|
||||
float densityMid = _fbm(warpedUV * 4.00 + vec2(1.70, 3.10));
|
||||
float densityFine = _fbm(warpedUV * 8.50 + vec2(-2.30, 0.80));
|
||||
// volDepth: 0 = outer edge, 1 = deep interior
|
||||
float volDepth = clamp(densityMid * 0.45 + densityFine * 0.18, 0.0, 1.0);
|
||||
|
||||
// ── Cloud illumination ───────────────────────────────────────────
|
||||
float sunDot = max(0.0, dot(dir, sunDir));
|
||||
float shadowing = clamp(1.0 - shaped * 0.68, 0.16, 1.0);
|
||||
|
||||
// Top surface: brightly lit by direct sun
|
||||
vec3 litTop = mix(vec3(0.96, 0.97, 1.00), sunColor.rgb * 1.20, 0.20);
|
||||
|
||||
// Cloud base: deeper interior → darker grey-blue shadow
|
||||
float depthSh = mix(0.36, 0.62, volDepth);
|
||||
vec3 litBase = litTop * vec3(depthSh * 0.88, depthSh * 0.93, depthSh * 1.04);
|
||||
|
||||
// Sunset underbelly: orange-pink glow when sun is near horizon
|
||||
float sunsetC = max(0.0, 1.0 - abs(sunElevW) * 4.0) * sunPower;
|
||||
vec3 sunsetBelly = mix(
|
||||
vec3(1.0, 0.55, 0.22),
|
||||
vec3(1.0, 0.78, 0.52),
|
||||
clamp(sunDot, 0.0, 1.0)
|
||||
) * sunsetC * 0.75;
|
||||
litBase += sunsetBelly;
|
||||
|
||||
// Second FBM sample for vertical shading variation
|
||||
float baseShade = _fbm(warpedUV * 2.35 + vec2(0.30, 0.15)) * 0.72 + 0.28;
|
||||
cloudRGB = mix(litBase, litTop, baseShade * shadowing);
|
||||
|
||||
// Silver lining: bright backlit halo on sun-facing cloud edges
|
||||
float silverEdge = smoothstep(edgeWidth * 0.55, 0.0, raw) * sunDot;
|
||||
cloudRGB += vec3(0.72, 0.64, 0.46) * silverEdge * sunPower * 0.70;
|
||||
|
||||
// Night / moon-lit clouds (cool dim blue-grey)
|
||||
if (moonEnabled > 0.5 && sunPower < 0.30) {
|
||||
float mnFade = clamp((0.30 - sunPower) * 4.0, 0.0, 1.0);
|
||||
float mnDot = max(0.0, dot(dir, normalize(moonDir)));
|
||||
vec3 mnLight = vec3(0.35, 0.42, 0.60) * mnDot * 0.38;
|
||||
float nDepth = mix(0.06, 0.52, shaped);
|
||||
cloudRGB = mix(cloudRGB, vec3(0.05, 0.07, 0.14) + mnLight * shaped,
|
||||
mnFade * nDepth);
|
||||
}
|
||||
|
||||
cloudAlpha = shaped;
|
||||
}
|
||||
|
||||
sky = mix(sky, cloudRGB, cloudAlpha);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Twinkling star field
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (starBrightness > 0.004 && elev > 0.02) {
|
||||
// Quantise direction → star cells (each has at most 1 star)
|
||||
vec3 snap = floor(dir * 210.0) / 210.0;
|
||||
float seed = _hash(snap.xy * vec2(43.0, 127.0) + snap.z * 59.0);
|
||||
|
||||
// Star brightness / size (varies per cell)
|
||||
float szSeed = _hash(snap.yx * vec2(71.0, 23.0) + snap.z * 17.0);
|
||||
float exponent = mix(430.0, 680.0, szSeed);
|
||||
float rawBrt = pow(seed, exponent) * mix(2.4, 5.2, szSeed);
|
||||
|
||||
// ── Twinkle: per-star unique frequency and phase ─────────────────
|
||||
float twSeed = _hash(snap.xy * 33.0 + snap.z * 71.0);
|
||||
float twFreq = mix(0.35, 4.0, twSeed);
|
||||
float twPhase = twSeed * 6.28318530;
|
||||
// Primary twinkle
|
||||
float twinkle = 0.62 + 0.38 * sin(time * twFreq + twPhase);
|
||||
// Secondary high-frequency shimmer on bright stars
|
||||
float shimmer = 1.0 + 0.15 * sin(time * twFreq * 3.3 + twPhase * 1.7);
|
||||
float star = rawBrt * twinkle * shimmer;
|
||||
|
||||
// ── Star spectral class ──────────────────────────────────────────
|
||||
float colSeed = _hash(snap.yz * vec2(37.0, 53.0));
|
||||
vec3 starCol;
|
||||
if (colSeed < 0.22) starCol = vec3(0.76, 0.84, 1.00); // O/B blue-white
|
||||
else if (colSeed < 0.48) starCol = vec3(1.00, 1.00, 0.94); // A/F white
|
||||
else if (colSeed < 0.74) starCol = vec3(1.00, 0.95, 0.70); // G yellow-white
|
||||
else starCol = vec3(1.00, 0.75, 0.55); // K/M warm orange
|
||||
|
||||
// Dim near sun's position in sky
|
||||
float nearSun = max(0.0, dot(dir, sunDir));
|
||||
star *= max(0.0, 1.0 - nearSun * nearSun * 4.0);
|
||||
|
||||
// Fade behind clouds
|
||||
star *= max(0.0, 1.0 - cloudAlpha * 1.2);
|
||||
|
||||
sky += starCol * star * starBrightness;
|
||||
|
||||
// ── Occasional bright "sparkle" with diffraction cross ───────────
|
||||
if (seed > 0.9982) {
|
||||
float sparkle = (seed - 0.9982) / 0.0018;
|
||||
sparkle = sparkle * sparkle * twinkle * twinkle;
|
||||
// Cross arms: exponential falloff from snap position
|
||||
float crossW = 0.0055;
|
||||
float arm_h = exp(-abs(dir.x - snap.x) / crossW);
|
||||
float arm_v = exp(-abs(dir.y - snap.y) / crossW);
|
||||
float cross2 = (arm_h + arm_v) * 0.5;
|
||||
sky += starCol * cross2 * sparkle * starBrightness * 0.50
|
||||
* max(0.0, 1.0 - cloudAlpha * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Sun disc + corona + blinding glare
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (sunDiscEnabled > 0.5 && sunElevW > -0.14) {
|
||||
float angDist = acos(clamp(cosTheta, -1.0, 1.0));
|
||||
float sunR = 0.0048; // angular radius of sun disc (~0.275°)
|
||||
|
||||
// ── Solar disc with limb darkening ───────────────────────────────
|
||||
float limbT = clamp(1.0 - angDist / sunR, 0.0, 1.0);
|
||||
float limb = smoothstep(0.0, 1.0, limbT) * step(angDist, sunR * 1.30);
|
||||
float limbDrk = mix(0.68, 1.0, limbT); // edges 32% darker than centre
|
||||
|
||||
// Colour: white-yellow at noon → deep orange-red at horizon
|
||||
float sunsetT = clamp(1.0 - sunElevW * 5.5, 0.0, 1.0);
|
||||
vec3 discCol = mix(
|
||||
vec3(1.00, 0.97, 0.82) * 5.0, // noon: brilliant white-yellow
|
||||
vec3(1.00, 0.44, 0.05) * 2.8, // sunset: deep orange-red
|
||||
sunsetT
|
||||
) * limbDrk;
|
||||
|
||||
// ── Multi-layer corona / aureole ─────────────────────────────────
|
||||
float c1 = exp(-angDist * 340.0) * 1.60;
|
||||
float c2 = exp(-angDist * 95.0) * 0.70;
|
||||
float c3 = exp(-angDist * 30.0) * 0.32;
|
||||
float c4 = exp(-angDist * 8.5) * 0.12;
|
||||
vec3 coronaCol = sunColor.rgb * (c1 + c2 + c3 + c4) * sunPower;
|
||||
|
||||
// ── Vertical diffraction streak ──────────────────────────────────
|
||||
float streak = exp(-abs(dir.z - sunDir.z) * 90.0)
|
||||
* exp(-max(0.0, 1.0 - cosTheta) * 150.0)
|
||||
* 0.28 * sunPower;
|
||||
vec3 streakCol = sunColor.rgb * streak;
|
||||
|
||||
// ── Blinding glare + chromatic aberration ────────────────────────
|
||||
float blindAmt = pow(max(0.0, cosTheta), 55.0) * sunBlindStrength * sunPower;
|
||||
// Slightly wider red channel for chromatic effect
|
||||
vec3 blindCol = vec3(
|
||||
pow(max(0.0, cosTheta), 38.0) * sunBlindStrength * sunPower * 1.35,
|
||||
blindAmt * 0.90,
|
||||
blindAmt * 0.65
|
||||
);
|
||||
|
||||
// ── Visibility: blocked by clouds, clipped below horizon ─────────
|
||||
float sunVis = (1.0 - cloudAlpha * 0.93)
|
||||
* clamp((sunElevW + 0.12) * 7.5, 0.0, 1.0);
|
||||
|
||||
sky += (discCol * limb + coronaCol + streakCol + blindCol) * sunVis;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Moon disc + surface detail + corona
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (moonEnabled > 0.5) {
|
||||
vec3 mDir = normalize(moonDir);
|
||||
float moonDot = dot(dir, mDir);
|
||||
float moonR = 0.9990; // cos(~2.56°)
|
||||
float moonDisc = smoothstep(moonR, moonR + 0.0008, moonDot);
|
||||
|
||||
// Surface noise → subtle crater/mare variation
|
||||
vec3 mTang = normalize(dir - mDir * moonDot);
|
||||
float mDetail = _vnoise(vec2(mTang.x * 550.0 + 30.0,
|
||||
mTang.z * 550.0 + 70.0)) * 0.18 + 0.82;
|
||||
|
||||
// Limb darkening
|
||||
float mLimb = mix(0.65, 1.0,
|
||||
clamp(1.0 - max(0.0, 1.0 - moonDot) * 3.0, 0.0, 1.0));
|
||||
vec3 moonSurf = vec3(0.88, 0.88, 0.80) * mDetail * mLimb;
|
||||
|
||||
// Multi-layer glow
|
||||
float mH1 = exp(-max(0.0, 1.0 - moonDot) * 110.0) * 0.30;
|
||||
float mH2 = exp(-max(0.0, 1.0 - moonDot) * 24.0) * 0.09;
|
||||
float mH3 = exp(-max(0.0, 1.0 - moonDot) * 6.0) * 0.03;
|
||||
vec3 moonGlow = moonColor.rgb * (mH1 + mH2 + mH3) * 0.85;
|
||||
|
||||
float moonVis = max(0.0, 1.0 - cloudAlpha * 0.90);
|
||||
sky = mix(sky, moonSurf * 1.55, moonDisc * moonVis);
|
||||
sky += moonGlow * moonVis;
|
||||
}
|
||||
|
||||
// ── Zone colour scale ─────────────────────────────────────────────────
|
||||
sky *= skyScale.rgb;
|
||||
|
||||
// ── Tonemapping: Reinhard + mild gamma lift ───────────────────────────
|
||||
sky = sky / (sky + vec3(0.72)); // soft knee HDR clamp
|
||||
sky = pow(clamp(sky, 0.0, 1.0), vec3(1.0 / 1.15)); // slight gamma lift
|
||||
|
||||
fragColor = vec4(sky, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Procedural sky sphere vertex shader.
|
||||
//
|
||||
// The sky is rendered on a large sphere (~950 unit radius) that follows the
|
||||
// camera. Because the sphere is centred at the camera in model space, every
|
||||
// vertex position is already a world-space ray direction. The fragment shader
|
||||
// uses that direction to compute atmospheric scattering colour and clouds.
|
||||
//
|
||||
// Panda3D coordinate convention: Y = forward, Z = up, X = right.
|
||||
#version 130
|
||||
|
||||
in vec4 p3d_Vertex;
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
|
||||
out vec3 vDir; // unnormalised model-space direction (normalised in frag)
|
||||
out vec2 vUV; // model-space polar UV for cloud tiling
|
||||
|
||||
void main() {
|
||||
gl_Position = (p3d_ModelViewProjectionMatrix * p3d_Vertex).xyww;
|
||||
// xyww trick forces depth to 1.0 (far clip) in NDC so sky is always behind
|
||||
// geometry — no depth write needed, but this makes the depth test pass even
|
||||
// without disabling depth write on the NodePath.
|
||||
|
||||
vDir = p3d_Vertex.xyz;
|
||||
|
||||
// Spherical UV: longitude (azimuth) on X, latitude (elevation) on Y.
|
||||
// Used for cloud layer texture-coordinate calculation in the fragment shader.
|
||||
float len = length(p3d_Vertex.xyz);
|
||||
vec3 d = p3d_Vertex.xyz / max(len, 0.001);
|
||||
float phi = atan(d.x, d.y); // azimuth [-π, π]
|
||||
float theta = asin(clamp(d.z, -1.0, 1.0)); // elevation [-π/2, π/2]
|
||||
vUV = vec2(phi / 6.28318530 + 0.5, theta / 3.14159265 + 0.5);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#version 120
|
||||
|
||||
// Sun rays / god rays fragment shader
|
||||
// Creates volumetric light scattering effect
|
||||
|
||||
uniform vec2 sunPos;
|
||||
uniform vec4 rayColor;
|
||||
uniform float rayIntensity;
|
||||
uniform float time;
|
||||
uniform float aspectRatio;
|
||||
|
||||
varying vec2 texcoord;
|
||||
|
||||
float hash(float n) {
|
||||
return fract(sin(n) * 43758.5453);
|
||||
}
|
||||
|
||||
float noise(vec2 x) {
|
||||
vec2 p = floor(x);
|
||||
vec2 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
float n = p.x + p.y * 57.0;
|
||||
return mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5000 * noise(p); p *= 2.02;
|
||||
f += 0.2500 * noise(p); p *= 2.03;
|
||||
f += 0.1250 * noise(p); p *= 2.01;
|
||||
f += 0.0625 * noise(p);
|
||||
return f / 0.9375;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = texcoord;
|
||||
|
||||
// Adjust for aspect ratio
|
||||
uv.x *= aspectRatio;
|
||||
vec2 lightPos = sunPos;
|
||||
lightPos.x *= aspectRatio;
|
||||
|
||||
// Vector from pixel to light source in screen space
|
||||
vec2 delta = lightPos - uv;
|
||||
float dist = length(delta);
|
||||
|
||||
// IMPORTANT:
|
||||
// This overlay is rendered on render2dp and does not necessarily have a
|
||||
// scene texture bound to p3d_Texture0. Sampling an unbound texture can
|
||||
// read undefined GPU memory (often seen as RGB flicker).
|
||||
//
|
||||
// Instead, generate a stable radial scattering term without sampling.
|
||||
float falloff = clamp(1.0 - dist, 0.0, 1.0);
|
||||
falloff = pow(falloff, 2.2);
|
||||
float shaft = falloff * (0.35 + 0.65 * falloff);
|
||||
|
||||
// Add subtle noise for atmospheric effect (animated very slowly).
|
||||
float n = fbm(uv * 3.0 + time * 0.05) * 0.05;
|
||||
shaft = clamp(shaft + n, 0.0, 1.0);
|
||||
|
||||
vec4 finalColor = vec4(rayColor.rgb * shaft, clamp(rayIntensity * shaft, 0.0, 1.0));
|
||||
|
||||
gl_FragColor = finalColor;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#version 120
|
||||
|
||||
// Simple sunrays vertex shader
|
||||
// Renders full-screen quad for sun shaft effect
|
||||
|
||||
attribute vec4 p3d_Vertex;
|
||||
attribute vec2 p3d_MultiTexCoord0;
|
||||
|
||||
varying vec2 texcoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = p3d_Vertex;
|
||||
texcoord = p3d_MultiTexCoord0;
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
#version 120
|
||||
|
||||
// Water fragment shader with reflection, refraction, and Fresnel effect
|
||||
|
||||
uniform vec4 waterColor;
|
||||
uniform float time;
|
||||
uniform float waveHeight;
|
||||
uniform float clarity;
|
||||
uniform float reflectivity;
|
||||
uniform sampler2D reflectionTex;
|
||||
uniform sampler2D p3d_Texture0;
|
||||
|
||||
varying vec3 normal;
|
||||
varying vec3 worldPos;
|
||||
varying vec2 texcoord;
|
||||
varying float depth;
|
||||
|
||||
// Fresnel reflectance approximation
|
||||
float fresnel(vec3 incident, vec3 normal, float power) {
|
||||
float cosine = dot(-incident, normal);
|
||||
cosine = clamp(cosine, 0.0, 1.0);
|
||||
return pow(1.0 - cosine, power);
|
||||
}
|
||||
|
||||
// Simple noise for water caustics
|
||||
float hash(float n) {
|
||||
return fract(sin(n) * 43758.5453);
|
||||
}
|
||||
|
||||
float noise(vec2 x) {
|
||||
vec2 p = floor(x);
|
||||
vec2 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
float n = p.x + p.y * 57.0;
|
||||
return mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5000 * noise(p); p *= 2.02;
|
||||
f += 0.2500 * noise(p); p *= 2.03;
|
||||
f += 0.1250 * noise(p); p *= 2.01;
|
||||
f += 0.0625 * noise(p);
|
||||
return f / 0.9375;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Calculate reflection coordinates
|
||||
vec3 viewDir = normalize(-worldPos);
|
||||
vec3 reflectDir = reflect(viewDir, normal);
|
||||
|
||||
// Convert reflection direction to texture coordinates
|
||||
vec2 reflCoord = 0.5 * reflectDir.xy / reflectDir.z + 0.5;
|
||||
reflCoord = clamp(reflCoord, 0.0, 1.0);
|
||||
|
||||
// Sample reflection texture
|
||||
vec4 reflection = texture2D(reflectionTex, reflCoord);
|
||||
|
||||
// Calculate Fresnel term
|
||||
float fresnelTerm = fresnel(viewDir, normal, 5.0);
|
||||
fresnelTerm = mix(0.02, 0.8, fresnelTerm);
|
||||
|
||||
// Base water color with depth attenuation
|
||||
vec4 baseColor = waterColor;
|
||||
float depthFactor = clamp(1.0 - depth * 0.5, 0.1, 1.0);
|
||||
baseColor.a *= depthFactor * clarity;
|
||||
|
||||
// Generate caustics pattern
|
||||
vec2 causticUV = worldPos.xz * 0.1 + time * 0.05;
|
||||
float caustics = fbm(causticUV) * 0.3 + 0.7;
|
||||
|
||||
// Combine reflection and base color
|
||||
vec4 finalColor = mix(baseColor, reflection, fresnelTerm * reflectivity);
|
||||
|
||||
// Apply caustics to underwater areas
|
||||
finalColor.rgb *= mix(1.0, caustics, 1.0 - fresnelTerm);
|
||||
|
||||
// Add wave foam based on slope
|
||||
float slope = 1.0 - abs(normal.y);
|
||||
float foam = smoothstep(0.3, 0.5, slope * waveHeight * 10.0);
|
||||
|
||||
// Foam noise
|
||||
vec2 foamUV = worldPos.xz * 0.5 + time * 0.2;
|
||||
float foamNoise = fbm(foamUV);
|
||||
foam *= foamNoise;
|
||||
|
||||
// Add white foam
|
||||
finalColor.rgb = mix(finalColor.rgb, vec3(1.0), foam * 0.3);
|
||||
|
||||
// Final alpha based on depth and clarity
|
||||
finalColor.a = mix(baseColor.a, 1.0, fresnelTerm * 0.5);
|
||||
|
||||
gl_FragColor = finalColor;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
#version 120
|
||||
|
||||
// Water vertex shader with wave animation
|
||||
|
||||
attribute vec4 p3d_Vertex;
|
||||
attribute vec3 p3d_Normal;
|
||||
attribute vec2 p3d_MultiTexCoord0;
|
||||
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
uniform mat4 p3d_ModelMatrix;
|
||||
uniform mat3 p3d_NormalMatrix;
|
||||
|
||||
uniform float time;
|
||||
uniform float waveHeight;
|
||||
uniform float waveSpeed;
|
||||
uniform vec4 waterColor;
|
||||
uniform sampler2D reflectionTex;
|
||||
|
||||
varying vec3 normal;
|
||||
varying vec3 worldPos;
|
||||
varying vec2 texcoord;
|
||||
varying vec4 reflectionColor;
|
||||
varying float depth;
|
||||
|
||||
// Simple sine wave function
|
||||
float wave(vec2 position, float frequency, float speed) {
|
||||
return sin(dot(position, vec2(frequency, frequency * 1.7)) + time * speed);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 vertex = p3d_Vertex;
|
||||
|
||||
// Generate wave displacement
|
||||
vec2 pos = vertex.xz;
|
||||
float height = 0.0;
|
||||
|
||||
// Multiple octaves for realistic waves
|
||||
height += wave(pos, 0.1, waveSpeed) * 0.5;
|
||||
height += wave(pos, 0.2, waveSpeed * 1.3) * 0.25;
|
||||
height += wave(pos, 0.4, waveSpeed * 1.7) * 0.125;
|
||||
height += wave(pos, 0.8, waveSpeed * 2.1) * 0.0625;
|
||||
|
||||
// Apply wave height
|
||||
vertex.y += height * waveHeight;
|
||||
|
||||
// Calculate normals from wave function derivatives
|
||||
vec3 waveNormal = vec3(
|
||||
-waveHeight * 0.1 * cos(dot(pos, vec2(0.1, 0.17)) + time * waveSpeed),
|
||||
1.0,
|
||||
-waveHeight * 0.1 * cos(dot(pos, vec2(0.17, 0.1)) + time * waveSpeed * 1.3)
|
||||
);
|
||||
waveNormal = normalize(waveNormal);
|
||||
|
||||
// Transform vertex
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * vertex;
|
||||
|
||||
// Pass data to fragment shader
|
||||
normal = normalize(p3d_NormalMatrix * waveNormal);
|
||||
worldPos = (p3d_ModelMatrix * vertex).xyz;
|
||||
texcoord = p3d_MultiTexCoord0;
|
||||
|
||||
// Simple depth calculation
|
||||
depth = gl_Position.z / gl_Position.w;
|
||||
}
|
||||
Loading…
Reference in New Issue