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:
Toontown Super 2026-08-02 16:21:14 -04:00 committed by GitHub
commit 3dc99bc40c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 24872 additions and 41 deletions

View File

@ -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.

13
libotp/__init__.py Normal file
View File

@ -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 *

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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

211
libotp/nametag/Nametag.py Normal file
View File

@ -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

339
libotp/nametag/Nametag2d.py Normal file
View File

@ -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()

405
libotp/nametag/Nametag3d.py Normal file
View File

@ -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)

View File

@ -0,0 +1,8 @@
from .Nametag3d import Nametag3d
class NametagFloat2d(Nametag3d):
def __init__(self):
Nametag3d.__init__(self)
self.m_is_3d = False
self.updateContents()

View File

@ -0,0 +1,6 @@
from .Nametag3d import Nametag3d
class NametagFloat3d(Nametag3d):
def __init__(self):
Nametag3d.__init__(self)

View File

@ -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()

View File

@ -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()

View File

@ -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]

View File

@ -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

View File

View File

@ -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

View File

@ -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.01.0
cloudSpeed relative speed multiplier
cloudSharpness 0.0 (soft) 1.0 (sharp)
turbidity 1.08.0 (Mie haze)
starBrightness 0.01.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.01.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 01
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

View File

@ -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:

View File

@ -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:

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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.01.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; // 01 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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}