mirror of https://github.com/H-uru/korman.git
Browse Source
This adds just enough plumbing to be able to export GUI popup notes using the standard xDialogToggle.py. There are still a number of TODOs and FIXMEs in the basic stuff, but the design is mostly solid. The idea is that you'll create a GUI page for any objects that need to appear in a GUI. The GUI does not require an explicit GUI camera, however. For now, an automatic GUI camera will be made facing the largest object's -Z axis.pull/373/head
Adam Johnson
1 year ago
15 changed files with 934 additions and 7 deletions
@ -0,0 +1,210 @@ |
|||||||
|
# This file is part of Korman. |
||||||
|
# |
||||||
|
# Korman is free software: you can redistribute it and/or modify |
||||||
|
# it under the terms of the GNU General Public License as published by |
||||||
|
# the Free Software Foundation, either version 3 of the License, or |
||||||
|
# (at your option) any later version. |
||||||
|
# |
||||||
|
# Korman is distributed in the hope that it will be useful, |
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
# GNU General Public License for more details. |
||||||
|
# |
||||||
|
# You should have received a copy of the GNU General Public License |
||||||
|
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import bpy |
||||||
|
import mathutils |
||||||
|
|
||||||
|
from contextlib import contextmanager, ExitStack |
||||||
|
import itertools |
||||||
|
from PyHSPlasma import * |
||||||
|
from typing import * |
||||||
|
import weakref |
||||||
|
|
||||||
|
from .explosions import ExportError |
||||||
|
from .. import helpers |
||||||
|
from . import utils |
||||||
|
|
||||||
|
if TYPE_CHECKING: |
||||||
|
from .convert import Exporter |
||||||
|
from .logger import _ExportLogger as ExportLogger |
||||||
|
|
||||||
|
|
||||||
|
class Clipping(NamedTuple): |
||||||
|
hither: float |
||||||
|
yonder: float |
||||||
|
|
||||||
|
|
||||||
|
class PostEffectModMatrices(NamedTuple): |
||||||
|
c2w: hsMatrix44 |
||||||
|
w2c: hsMatrix44 |
||||||
|
|
||||||
|
|
||||||
|
class GuiConverter: |
||||||
|
|
||||||
|
if TYPE_CHECKING: |
||||||
|
_parent: weakref.ref[Exporter] = ... |
||||||
|
|
||||||
|
def __init__(self, parent: Optional[Exporter] = None): |
||||||
|
self._parent = weakref.ref(parent) if parent is not None else None |
||||||
|
|
||||||
|
# Go ahead and prepare the GUI transparent material for future use. |
||||||
|
if parent is not None: |
||||||
|
self._transp_material = parent.exit_stack.enter_context( |
||||||
|
helpers.TemporaryObject( |
||||||
|
bpy.data.materials.new("GUITransparent"), |
||||||
|
bpy.data.materials.remove |
||||||
|
) |
||||||
|
) |
||||||
|
self._transp_material.diffuse_color = mathutils.Vector((1.0, 1.0, 0.0)) |
||||||
|
self._transp_material.use_mist = False |
||||||
|
|
||||||
|
# Cyan's transparent GUI materials just set an opacity of 0% |
||||||
|
tex_slot = self._transp_material.texture_slots.add() |
||||||
|
tex_slot.texture = parent.exit_stack.enter_context( |
||||||
|
helpers.TemporaryObject( |
||||||
|
bpy.data.textures.new("AutoTransparentLayer", "NONE"), |
||||||
|
bpy.data.textures.remove |
||||||
|
) |
||||||
|
) |
||||||
|
tex_slot.texture.plasma_layer.opacity = 0.0 |
||||||
|
else: |
||||||
|
self._transp_material = None |
||||||
|
|
||||||
|
def calc_camera_matrix( |
||||||
|
self, |
||||||
|
scene: bpy.types.Scene, |
||||||
|
objects: Sequence[bpy.types.Object], |
||||||
|
fov: float, |
||||||
|
scale: float = 1.0, |
||||||
|
) -> mathutils.Matrix: |
||||||
|
if not objects: |
||||||
|
raise ExportError("No objects specified for GUI Camera generation.") |
||||||
|
|
||||||
|
class ObjArea(NamedTuple): |
||||||
|
obj: bpy.types.Object |
||||||
|
area: float |
||||||
|
|
||||||
|
|
||||||
|
remove_mesh = bpy.data.meshes.remove |
||||||
|
obj_areas: List[ObjArea] = [] |
||||||
|
for i in objects: |
||||||
|
mesh = i.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=False) |
||||||
|
with helpers.TemporaryObject(mesh, remove_mesh): |
||||||
|
utils.transform_mesh(mesh, i.matrix_world) |
||||||
|
obj_areas.append( |
||||||
|
ObjArea(i, sum((polygon.area for polygon in mesh.polygons))) |
||||||
|
) |
||||||
|
largest_obj = max(obj_areas, key=lambda x: x.area) |
||||||
|
|
||||||
|
# GUIs are generally flat planes, which, by default in Blender, have their normal pointing |
||||||
|
# in the localspace z axis. Therefore, what we would like to do is have a camera that points |
||||||
|
# at the localspace -Z axis. Where up is the localspace +Y axis. We are already pointing at |
||||||
|
# -Z with +Y being up from what I can tel. So, just use this matrix. |
||||||
|
mat = largest_obj.obj.matrix_world.to_3x3() |
||||||
|
|
||||||
|
# Now, we know the rotation of the camera. Great! What we need to do now is ensure that all |
||||||
|
# of the objects in question fit within the view of a 4:3 camera rotated as above. Blender |
||||||
|
# helpfully provides us with the localspace bounding boxes of all the objects and an API to |
||||||
|
# fit points into the camera view. |
||||||
|
with ExitStack() as stack: |
||||||
|
stack.enter_context(self.generate_camera_render_settings(scene)) |
||||||
|
|
||||||
|
# Create a TEMPORARY camera object so we can use a certain Blender API. |
||||||
|
camera = stack.enter_context(utils.temporary_camera_object(scene, "GUICameraTemplate")) |
||||||
|
camera.matrix_world = mat.to_4x4() |
||||||
|
camera.data.angle = fov |
||||||
|
camera.data.lens_unit = "FOV" |
||||||
|
|
||||||
|
# Get all of the bounding points and make sure they all fit inside the camera's view frame. |
||||||
|
bound_boxes = ( |
||||||
|
obj.matrix_world * mathutils.Vector(bbox) |
||||||
|
for obj in objects for bbox in obj.bound_box |
||||||
|
) |
||||||
|
co, _ = camera.camera_fit_coords( |
||||||
|
scene, |
||||||
|
# bound_box is a list of vectors of each corner of all the objects' bounding boxes; |
||||||
|
# however, Blender's API wants a sequence of individual channel positions. Therefore, |
||||||
|
# we need to flatten the vectors. |
||||||
|
list(itertools.chain.from_iterable(bound_boxes)) |
||||||
|
) |
||||||
|
|
||||||
|
# Calculate the distance from the largest object to the camera. The scale we were given |
||||||
|
# will be used to push the camera back in the Z+ direction of that object by scale times. |
||||||
|
bvh = mathutils.bvhtree.BVHTree.FromObject(largest_obj.obj, scene) |
||||||
|
loc, normal, index, distance = bvh.find_nearest(co) |
||||||
|
co += normal * distance * (scale - 1.0) |
||||||
|
|
||||||
|
# ... |
||||||
|
mat.resize_4x4() |
||||||
|
mat.translation = co |
||||||
|
return mat |
||||||
|
|
||||||
|
def calc_clipping( |
||||||
|
self, |
||||||
|
pose: mathutils.Matrix, |
||||||
|
scene: bpy.types.Scene, |
||||||
|
objects: Sequence[bpy.types.Object], |
||||||
|
fov: float |
||||||
|
) -> Clipping: |
||||||
|
with ExitStack() as stack: |
||||||
|
stack.enter_context(self.generate_camera_render_settings(scene)) |
||||||
|
camera = stack.enter_context(utils.temporary_camera_object(scene, "GUICameraTemplate")) |
||||||
|
camera.matrix_world = pose |
||||||
|
camera.data.angle = fov |
||||||
|
camera.data.lens_unit = "FOV" |
||||||
|
|
||||||
|
# Determine the camera plane's normal so we can do a distance check against the |
||||||
|
# bounding boxes of the objects shown in the GUI. |
||||||
|
view_frame = [i * pose for i in camera.data.view_frame(scene)] |
||||||
|
cam_plane = mathutils.geometry.normal(view_frame) |
||||||
|
bound_boxes = ( |
||||||
|
obj.matrix_world * mathutils.Vector(bbox) |
||||||
|
for obj in objects for bbox in obj.bound_box |
||||||
|
) |
||||||
|
pos = pose.to_translation() |
||||||
|
bounds_dists = [ |
||||||
|
abs(mathutils.geometry.distance_point_to_plane(i, pos, cam_plane)) |
||||||
|
for i in bound_boxes |
||||||
|
] |
||||||
|
|
||||||
|
# Offset them by some epsilon to ensure the objects are rendered. |
||||||
|
hither, yonder = min(bounds_dists), max(bounds_dists) |
||||||
|
if yonder - 0.5 < hither: |
||||||
|
hither -= 0.25 |
||||||
|
yonder += 0.25 |
||||||
|
return Clipping(hither, yonder) |
||||||
|
|
||||||
|
def convert_post_effect_matrices(self, camera_matrix: mathutils.Matrix) -> PostEffectModMatrices: |
||||||
|
# PostEffectMod matrices face *away* from the GUI... For some reason. |
||||||
|
# See plPostEffectMod::SetWorldToCamera() |
||||||
|
c2w = utils.matrix44(camera_matrix) |
||||||
|
w2c = utils.matrix44(camera_matrix.inverted()) |
||||||
|
for i in range(4): |
||||||
|
c2w[i, 2] *= -1.0 |
||||||
|
w2c[2, i] *= -1.0 |
||||||
|
return PostEffectModMatrices(c2w, w2c) |
||||||
|
|
||||||
|
@contextmanager |
||||||
|
def generate_camera_render_settings(self, scene: bpy.types.Scene) -> Iterator[None]: |
||||||
|
# Set the render info to basically TV NTSC 4:3, which will set Blender's camera |
||||||
|
# viewport up as a 4:3 thingy to match Plasma. |
||||||
|
with helpers.GoodNeighbor() as toggle: |
||||||
|
toggle.track(scene.render, "resolution_x", 720) |
||||||
|
toggle.track(scene.render, "resolution_y", 486) |
||||||
|
toggle.track(scene.render, "pixel_aspect_x", 10.0) |
||||||
|
toggle.track(scene.render, "pixel_aspect_y", 11.0) |
||||||
|
yield |
||||||
|
|
||||||
|
@property |
||||||
|
def _report(self) -> ExportLogger: |
||||||
|
return self._parent().report |
||||||
|
|
||||||
|
@property |
||||||
|
def transparent_material(self) -> bpy.types.Material: |
||||||
|
assert self._transp_material is not None |
||||||
|
return self._transp_material |
||||||
|
|
@ -0,0 +1,113 @@ |
|||||||
|
# This file is part of Korman. |
||||||
|
# |
||||||
|
# Korman is free software: you can redistribute it and/or modify |
||||||
|
# it under the terms of the GNU General Public License as published by |
||||||
|
# the Free Software Foundation, either version 3 of the License, or |
||||||
|
# (at your option) any later version. |
||||||
|
# |
||||||
|
# Korman is distributed in the hope that it will be useful, |
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
# GNU General Public License for more details. |
||||||
|
# |
||||||
|
# You should have received a copy of the GNU General Public License |
||||||
|
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import bpy |
||||||
|
from bpy.props import * |
||||||
|
|
||||||
|
import math |
||||||
|
|
||||||
|
from ..exporter.explosions import ExportError |
||||||
|
from ..exporter.gui import GuiConverter |
||||||
|
|
||||||
|
class CameraOperator: |
||||||
|
@classmethod |
||||||
|
def poll(cls, context): |
||||||
|
return context.scene.render.engine == "PLASMA_GAME" |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaGameGuiCameraOperator(CameraOperator, bpy.types.Operator): |
||||||
|
bl_idname = "camera.plasma_create_game_gui_camera" |
||||||
|
bl_label = "Create Game GUI Camera" |
||||||
|
|
||||||
|
fov: float = FloatProperty( |
||||||
|
name="Field of View", |
||||||
|
description="", |
||||||
|
subtype="ANGLE", |
||||||
|
default=math.radians(90.0), |
||||||
|
min=0.0, |
||||||
|
max=math.radians(360.0), |
||||||
|
precision=1, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
gui_page: str = StringProperty( |
||||||
|
name="GUI Page", |
||||||
|
description="", |
||||||
|
options={"HIDDEN"} |
||||||
|
) |
||||||
|
scale: float = FloatProperty( |
||||||
|
name="GUI Scale", |
||||||
|
description="", |
||||||
|
default=1.02, |
||||||
|
min=0.001, |
||||||
|
soft_max=2.0, |
||||||
|
precision=2, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
|
||||||
|
mod_id: str = StringProperty(options={"HIDDEN"}) |
||||||
|
cam_prop_name: str = StringProperty(options={"HIDDEN"}) |
||||||
|
|
||||||
|
def execute(self, context): |
||||||
|
# If the modifier has been given to us, select all of the objects in the |
||||||
|
# given GUI page. |
||||||
|
if self.gui_page: |
||||||
|
for i in context.scene.objects: |
||||||
|
i.select = i.plasma_object.page == self.gui_page |
||||||
|
context.scene.update() |
||||||
|
|
||||||
|
visible_objects = [ |
||||||
|
i for i in context.selected_objects |
||||||
|
if i.type in {"MESH", "FONT"} |
||||||
|
] |
||||||
|
|
||||||
|
gui = GuiConverter() |
||||||
|
try: |
||||||
|
cam_matrix = gui.calc_camera_matrix( |
||||||
|
context.scene, |
||||||
|
visible_objects, |
||||||
|
self.fov, |
||||||
|
self.scale |
||||||
|
) |
||||||
|
except ExportError as e: |
||||||
|
self.report({"ERROR"}, str(e)) |
||||||
|
return {"CANCELLED"} |
||||||
|
|
||||||
|
if self.mod_id and self.cam_prop_name: |
||||||
|
modifier = getattr(context.object.plasma_modifiers, self.mod_id) |
||||||
|
cam_obj = getattr(modifier, self.cam_prop_name) |
||||||
|
else: |
||||||
|
cam_obj = None |
||||||
|
if cam_obj is None: |
||||||
|
if self.gui_page: |
||||||
|
name = f"{self.gui_page}_GUICamera" |
||||||
|
else: |
||||||
|
name = f"{context.object.name}_GUICamera" |
||||||
|
cam_data = bpy.data.cameras.new(name) |
||||||
|
cam_obj = bpy.data.objects.new(name, cam_data) |
||||||
|
context.scene.objects.link(cam_obj) |
||||||
|
|
||||||
|
cam_obj.matrix_world = cam_matrix |
||||||
|
cam_obj.data.angle = self.fov |
||||||
|
cam_obj.data.lens_unit = "FOV" |
||||||
|
|
||||||
|
for i in context.scene.objects: |
||||||
|
i.select = i == cam_obj |
||||||
|
|
||||||
|
if self.mod_id and self.cam_prop_name: |
||||||
|
modifier = getattr(context.object.plasma_modifiers, self.mod_id) |
||||||
|
setattr(modifier, self.cam_prop_name, cam_obj) |
||||||
|
return {"FINISHED"} |
@ -0,0 +1,306 @@ |
|||||||
|
# This file is part of Korman. |
||||||
|
# |
||||||
|
# Korman is free software: you can redistribute it and/or modify |
||||||
|
# it under the terms of the GNU General Public License as published by |
||||||
|
# the Free Software Foundation, either version 3 of the License, or |
||||||
|
# (at your option) any later version. |
||||||
|
# |
||||||
|
# Korman is distributed in the hope that it will be useful, |
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
# GNU General Public License for more details. |
||||||
|
# |
||||||
|
# You should have received a copy of the GNU General Public License |
||||||
|
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import bpy |
||||||
|
from bpy.props import * |
||||||
|
|
||||||
|
import itertools |
||||||
|
import math |
||||||
|
from typing import * |
||||||
|
|
||||||
|
from PyHSPlasma import * |
||||||
|
|
||||||
|
from ...exporter import ExportError |
||||||
|
from .base import PlasmaModifierProperties |
||||||
|
from ... import idprops |
||||||
|
|
||||||
|
if TYPE_CHECKING: |
||||||
|
from ...exporter import Exporter |
||||||
|
from ..prop_world import PlasmaAge, PlasmaPage |
||||||
|
|
||||||
|
class _GameGuiMixin: |
||||||
|
def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> Optional[pfGUIControlMod]: |
||||||
|
return None |
||||||
|
|
||||||
|
@property |
||||||
|
def has_gui_proc(self) -> bool: |
||||||
|
return True |
||||||
|
|
||||||
|
def iterate_control_modifiers(self) -> Iterator[_GameGuiMixin]: |
||||||
|
pl_mods = self.id_data.plasma_modifiers |
||||||
|
yield from ( |
||||||
|
getattr(pl_mods, i.pl_id) |
||||||
|
for i in self.iterate_control_subclasses() |
||||||
|
if getattr(pl_mods, i.pl_id).enabled |
||||||
|
) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def iterate_control_subclasses(cls) -> Iterator[_GameGuiMixin]: |
||||||
|
yield from filter( |
||||||
|
lambda x: x.is_game_gui_control(), |
||||||
|
_GameGuiMixin.__subclasses__() |
||||||
|
) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def is_game_gui_control(cls) -> bool: |
||||||
|
return True |
||||||
|
|
||||||
|
@property |
||||||
|
def requires_dyntext(self) -> bool: |
||||||
|
return False |
||||||
|
|
||||||
|
def sanity_check(self): |
||||||
|
age: PlasmaAge = bpy.context.scene.world.plasma_age |
||||||
|
|
||||||
|
# Game GUI modifiers must be attached to objects in a GUI page, ONLY |
||||||
|
page_name: str = self.id_data.plasma_object.page |
||||||
|
our_page: Optional[PlasmaPage] = next( |
||||||
|
(i for i in age.pages if i.name == page_name) |
||||||
|
) |
||||||
|
if our_page is None or our_page.page_type != "gui": |
||||||
|
raise ExportError(f"'{self.id_data.name}': {self.bl_label} Modifier must be in a GUI page!") |
||||||
|
|
||||||
|
# Only one Game GUI Control per object. Continuously check this because objects can be |
||||||
|
# generated/mutated during the pre-export phase. |
||||||
|
modifiers = self.id_data.plasma_modifiers |
||||||
|
controls = [i for i in self.iterate_control_subclasses() if getattr(modifiers, i.pl_id).enabled] |
||||||
|
num_controls = len(controls) |
||||||
|
if num_controls > 1: |
||||||
|
raise ExportError(f"'{self.id_data.name}': Only 1 GUI Control modifier is allowed per object. We found {num_controls}.") |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaGameGuiControlModifier(PlasmaModifierProperties, _GameGuiMixin): |
||||||
|
pl_id = "gui_control" |
||||||
|
|
||||||
|
bl_category = "GUI" |
||||||
|
bl_label = "Ex: Game GUI Control" |
||||||
|
bl_description = "XXX" |
||||||
|
|
||||||
|
tag_id = IntProperty( |
||||||
|
name="Tag ID", |
||||||
|
description="", |
||||||
|
min=0, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
visible = BoolProperty( |
||||||
|
name="Visible", |
||||||
|
description="", |
||||||
|
default=True, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
proc = EnumProperty( |
||||||
|
name="Notification Procedure", |
||||||
|
description="", |
||||||
|
items=[ |
||||||
|
("default", "[Default]", "Send notifications to the owner's notification procedure."), |
||||||
|
("close_dialog", "Close Dialog", "Close the current Game GUI Dialog."), |
||||||
|
("console_command", "Run Console Command", "Run a Plasma Console command.") |
||||||
|
], |
||||||
|
options=set() |
||||||
|
) |
||||||
|
console_command = StringProperty( |
||||||
|
name="Command", |
||||||
|
description="", |
||||||
|
options=set() |
||||||
|
) |
||||||
|
|
||||||
|
def convert_gui_control(self, exporter: Exporter, ctrl: pfGUIControlMod, bo: bpy.types.Object, so: plSceneObject): |
||||||
|
ctrl.tagID = self.tag_id |
||||||
|
ctrl.visible = self.visible |
||||||
|
if self.proc == "default": |
||||||
|
ctrl.setFlag(pfGUIControlMod.kInheritProcFromDlg, True) |
||||||
|
elif self.proc == "close_dialog": |
||||||
|
ctrl.handler = pfGUICloseDlgProc() |
||||||
|
elif self.proc == "console_command": |
||||||
|
handler = pfGUIConsoleCmdProc() |
||||||
|
handler.command = self.console_command |
||||||
|
ctrl.handler = handler |
||||||
|
|
||||||
|
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): |
||||||
|
ctrl_mods = list(self.iterate_control_modifiers()) |
||||||
|
if not ctrl_mods: |
||||||
|
exporter.report.msg(str(list(self.iterate_control_subclasses()))) |
||||||
|
exporter.report.warn("This modifier has no effect because no GUI control modifiers are present!") |
||||||
|
for ctrl_mod in ctrl_mods: |
||||||
|
self.convert_gui_control(exporter, ctrl_mod.get_control(exporter, bo, so), bo, so) |
||||||
|
|
||||||
|
@property |
||||||
|
def has_gui_proc(self) -> bool: |
||||||
|
return any((i.has_gui_proc for i in self.iterate_control_modifiers())) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def is_game_gui_control(cls) -> bool: |
||||||
|
# How is a control not a control, you ask? Because, grasshopper, this modifier does not |
||||||
|
# actually export a GUI control itself. Instead, it holds common properties that may |
||||||
|
# or may not be used by other controls. This just helps fill out the other modifiers. |
||||||
|
return False |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaGameGuiButtonModifier(PlasmaModifierProperties, _GameGuiMixin): |
||||||
|
pl_id = "gui_button" |
||||||
|
pl_depends = {"gui_control"} |
||||||
|
|
||||||
|
bl_category = "GUI" |
||||||
|
bl_label = "Ex: Game GUI Button" |
||||||
|
bl_description = "XXX" |
||||||
|
|
||||||
|
def _update_notify_type(self, context): |
||||||
|
# It doesn't make sense to have no notify type at all selected, so |
||||||
|
# default to at least one option. |
||||||
|
if not self.notify_type: |
||||||
|
self.notify_type = {"DOWN"} |
||||||
|
|
||||||
|
notify_type = EnumProperty( |
||||||
|
name="Notify On", |
||||||
|
description="When the button should perform its action", |
||||||
|
items=[ |
||||||
|
("UP", "Up", "When the mouse button is down over the GUI button."), |
||||||
|
("DOWN", "Down", "When the mouse button is released over the GUI button."), |
||||||
|
], |
||||||
|
default={"UP"}, |
||||||
|
options={"ENUM_FLAG"}, |
||||||
|
update=_update_notify_type |
||||||
|
) |
||||||
|
|
||||||
|
def get_control(self, exporter: Exporter, bo: Optional[bpy.types.Object] = None, so: Optional[plSceneObject] = None) -> pfGUIButtonMod: |
||||||
|
return exporter.mgr.find_create_object(pfGUIButtonMod, bl=bo, so=so) |
||||||
|
|
||||||
|
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): |
||||||
|
ctrl = self.get_control(exporter, bo, so) |
||||||
|
ctrl.setFlag(pfGUIControlMod.kWantsInterest, True) |
||||||
|
|
||||||
|
if self.notify_type == {"UP"}: |
||||||
|
ctrl.notifyType = pfGUIButtonMod.kNotifyOnUp |
||||||
|
elif self.notify_type == {"DOWN"}: |
||||||
|
ctrl.notifyType = pfGUIButtonMod.kNotifyOnDown |
||||||
|
elif self.notify_type == {"UP", "DOWN"}: |
||||||
|
ctrl.notifyType = pfGUIButtonMod.kNotifyOnUpAndDown |
||||||
|
else: |
||||||
|
raise ValueError(self.notify_type) |
||||||
|
|
||||||
|
|
||||||
|
class PlasmaGameGuiDialogModifier(PlasmaModifierProperties, _GameGuiMixin): |
||||||
|
pl_id = "gui_dialog" |
||||||
|
|
||||||
|
bl_category = "GUI" |
||||||
|
bl_label = "Ex: Game GUI Dialog" |
||||||
|
bl_description = "XXX" |
||||||
|
|
||||||
|
camera_object: bpy.types.Object = PointerProperty( |
||||||
|
name="GUI Camera", |
||||||
|
description="Camera used to project the GUI to screenspace.", |
||||||
|
type=bpy.types.Object, |
||||||
|
poll=idprops.poll_camera_objects, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
is_modal = BoolProperty( |
||||||
|
name="Modal", |
||||||
|
description="", |
||||||
|
default=True, |
||||||
|
options=set() |
||||||
|
) |
||||||
|
|
||||||
|
def export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): |
||||||
|
# Find all of the visible objects in the GUI page for use in hither/yon raycast and |
||||||
|
# camera matrix calculations. |
||||||
|
visible_objects = [ |
||||||
|
i for i in exporter.get_objects(bo.plasma_object.page) |
||||||
|
if i.type == "MESH" and i.data.materials |
||||||
|
] |
||||||
|
|
||||||
|
camera_object = self.id_data if self.id_data.type == "CAMERA" else self.camera_object |
||||||
|
if camera_object: |
||||||
|
exporter.report.msg(f"Using camera matrix from camera '{camera_object.name}'") |
||||||
|
if camera_object != self.id_data and camera_object.plasma_object.enabled: |
||||||
|
with exporter.report.indent(): |
||||||
|
exporter.report.warn("The camera object should NOT be a Plasma Object!") |
||||||
|
camera_matrix = camera_object.matrix_world |
||||||
|
|
||||||
|
# Save the clipping info from the camera for later use. |
||||||
|
cam_data = camera_object.data |
||||||
|
fov, hither, yonder = cam_data.angle, cam_data.clip_start, cam_data.clip_end |
||||||
|
else: |
||||||
|
exporter.report.msg(f"Building a camera matrix to view: {', '.join((i.name for i in visible_objects))}") |
||||||
|
fov = math.radians(45.0) |
||||||
|
camera_matrix = exporter.gui.calc_camera_matrix( |
||||||
|
bpy.context.scene, |
||||||
|
visible_objects, |
||||||
|
fov, |
||||||
|
1.02 # FIXME |
||||||
|
) |
||||||
|
|
||||||
|
# There isn't a real camera, so just pretend like the user didn't set the clipping info. |
||||||
|
hither, yonder = 0.0, 0.0 |
||||||
|
with exporter.report.indent(): |
||||||
|
exporter.report.msg(str(camera_matrix)) |
||||||
|
|
||||||
|
# If no hither or yonder was specified on the camera, then we need to determine that ourselves. |
||||||
|
if not hither or not yonder: |
||||||
|
exporter.report.msg(f"Incomplete clipping: H:{hither:.02f} Y:{yonder:.02f}; calculating new...") |
||||||
|
with exporter.report.indent(): |
||||||
|
clipping = exporter.gui.calc_clipping( |
||||||
|
camera_matrix, |
||||||
|
bpy.context.scene, |
||||||
|
visible_objects, |
||||||
|
fov |
||||||
|
) |
||||||
|
exporter.report.msg(f"Calculated: H:{clipping.hither:.02f} Y:{clipping.yonder:.02f}") |
||||||
|
if not hither: |
||||||
|
hither = clipping.hither |
||||||
|
if not yonder: |
||||||
|
yonder = clipping.yonder |
||||||
|
exporter.report.msg(f"Corrected clipping: H:{hither:.02f} Y:{yonder:.02f}") |
||||||
|
|
||||||
|
# Both of the objects we export go into the pool. |
||||||
|
scene_node_key = exporter.mgr.get_scene_node(bl=bo) |
||||||
|
|
||||||
|
post_effect = exporter.mgr.find_create_object(plPostEffectMod, bl=bo) |
||||||
|
post_effect.defaultC2W, post_effect.defaultW2C = exporter.gui.convert_post_effect_matrices(camera_matrix) |
||||||
|
post_effect.fovX = math.degrees(fov) |
||||||
|
post_effect.fovY = math.degrees(fov * (3.0 / 4.0)) |
||||||
|
post_effect.hither = min((hither, yonder)) |
||||||
|
post_effect.yon = max((hither, yonder)) |
||||||
|
post_effect.nodeKey = scene_node_key |
||||||
|
|
||||||
|
dialog_mod = exporter.mgr.find_create_object(pfGUIDialogMod, bl=bo) |
||||||
|
dialog_mod.name = bo.plasma_object.page |
||||||
|
dialog_mod.setFlag(pfGUIDialogMod.kModal, self.is_modal) |
||||||
|
dialog_mod.renderMod = post_effect.key |
||||||
|
dialog_mod.sceneNode = scene_node_key |
||||||
|
|
||||||
|
@property |
||||||
|
def has_gui_proc(self) -> bool: |
||||||
|
return False |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def is_game_gui_control(cls) -> bool: |
||||||
|
return False |
||||||
|
|
||||||
|
def post_export(self, exporter: Exporter, bo: bpy.types.Object, so: plSceneObject): |
||||||
|
# All objects have been exported. Now, we can establish linkage to all controls that |
||||||
|
# have been exported. |
||||||
|
dialog = exporter.mgr.find_object(pfGUIDialogMod, bl=bo, so=so) |
||||||
|
control_modifiers: Iterable[_GameGuiMixin] = itertools.chain.from_iterable( |
||||||
|
obj.plasma_modifiers.gui_control.iterate_control_modifiers() |
||||||
|
for obj in exporter.get_objects(bo.plasma_object.page) |
||||||
|
if obj.plasma_modifiers.gui_control.enabled |
||||||
|
) |
||||||
|
for control_modifier in control_modifiers: |
||||||
|
control = control_modifier.get_control(exporter, control_modifier.id_data) |
||||||
|
ctrl_key = control.key |
||||||
|
exporter.report.msg(f"GUIDialog '{bo.name}': [{control.ClassName()}] '{ctrl_key.name}'") |
||||||
|
dialog.addControl(ctrl_key) |
@ -0,0 +1,42 @@ |
|||||||
|
# This file is part of Korman. |
||||||
|
# |
||||||
|
# Korman is free software: you can redistribute it and/or modify |
||||||
|
# it under the terms of the GNU General Public License as published by |
||||||
|
# the Free Software Foundation, either version 3 of the License, or |
||||||
|
# (at your option) any later version. |
||||||
|
# |
||||||
|
# Korman is distributed in the hope that it will be useful, |
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
# GNU General Public License for more details. |
||||||
|
# |
||||||
|
# You should have received a copy of the GNU General Public License |
||||||
|
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
def gui_button(modifier, layout, context): |
||||||
|
layout.prop(modifier, "notify_type") |
||||||
|
|
||||||
|
def gui_control(modifier, layout, context): |
||||||
|
split = layout.split() |
||||||
|
col = split.column() |
||||||
|
col.prop(modifier, "visible") |
||||||
|
|
||||||
|
col = split.column() |
||||||
|
col.prop(modifier, "tag_id") |
||||||
|
|
||||||
|
col = layout.column() |
||||||
|
col.active = modifier.has_gui_proc |
||||||
|
col.prop(modifier, "proc") |
||||||
|
row = col.row() |
||||||
|
row.active = col.active and modifier.proc == "console_command" |
||||||
|
row.prop(modifier, "console_command") |
||||||
|
|
||||||
|
def gui_dialog(modifier, layout, context): |
||||||
|
row = layout.row(align=True) |
||||||
|
row.prop(modifier, "camera_object") |
||||||
|
op = row.operator("camera.plasma_create_game_gui_camera", text="", icon="CAMERA_DATA") |
||||||
|
op.mod_id = modifier.pl_id |
||||||
|
op.cam_prop_name = "camera_object" |
||||||
|
op.gui_page = modifier.id_data.plasma_object.page |
||||||
|
|
||||||
|
layout.prop(modifier, "is_modal") |
Loading…
Reference in new issue