diff --git a/scripts/addons_core/game_engine_add_basic_character.py b/scripts/addons_core/game_engine_add_basic_character.py new file mode 100644 index 00000000000..78d7f8e8d0e --- /dev/null +++ b/scripts/addons_core/game_engine_add_basic_character.py @@ -0,0 +1,338 @@ +# This is free software under the terms of the GNU General Public License +# you may redistribute it, and/or modify it. +# +# This code 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 (http://www.gnu.org/licenses/) for more details. +# +# ***** END GPL LICENSE BLOCK ***** + +# ######################################################################### + +bl_info = { + "name": "Add character", + "description": "Create basic bge character and fly camera", + "author": "Moaaa", + "version": (0, 0, 4), + "blender": (2, 77, 0), + "location": "View3D > TOOLS", + "warning": "WIP - Frequent changes for known issues and enhancements", + "support": "TESTING", + "wiki_url": "https://github.com/UPBGE/blender-addons/wiki/Basic-Character-addon", + "tracker_url": "", + "category": "Game Engine" +} + +import bpy +import os +from bpy.types import Operator, Panel + +bpy.types.Scene.character_size = bpy.props.FloatProperty(name="character_size", default=2.0, min=1.0, max=10.0) +bpy.types.Scene.key_sensitive = bpy.props.FloatProperty(name="key sensitive", default=0.2, min=0.1, max=5.0) +bpy.types.Scene.mouse_sensitive = bpy.props.FloatProperty(name="mouse sensitive", default=0.5, min=0.1, max=5.0) +bpy.types.Scene.character_name = bpy.props.StringProperty(name="character_name", default="character") +bpy.types.Scene.character_jump = bpy.props.BoolProperty(name="character_jump") + +key_mode = [("0", "Arrow Keys", "Arrow Keys"), ("1", "ZSDQ", "ZSDQ"), ("2", "WSDA", "WSDA"), ] + +bpy.types.Scene.character_keys = bpy.props.EnumProperty(items=key_mode, name="character_keys") + +class simple_character(Operator): + bl_label = 'Add character' + bl_idname = 'character.gen' + bl_description = 'Generate a simple character' + bl_context = 'objectmode' + + + + def execute(self, context): + + + + bpy.context.scene.render.engine = 'BLENDER_GAME' + bpy.context.scene.objects.active = None + + bpy.ops.mesh.primitive_cone_add(vertices=16, radius1=bpy.context.scene.character_size, radius2=0.0, depth=bpy.context.scene.character_size, end_fill_type='TRIFAN', view_align=False, + enter_editmode=False, location=bpy.context.scene.cursor_location, rotation=(0.0, 0.0, 0.0), + layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) + + # configure character + obj = bpy.context.selected_objects[0] + obj.name = bpy.context.scene.character_name + obj.game.physics_type = 'CHARACTER' + obj.game.use_actor = True + obj.game.use_collision_bounds = True + obj.game.collision_bounds_type = 'CONE' + obj.hide_render = True + + sensors = obj.game.sensors + controllers = obj.game.controllers + actuators = obj.game.actuators + + bpy.ops.logic.sensor_add(type="MOUSE", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", object=obj.name) + bpy.ops.logic.actuator_add(type='MOUSE', name="BodyTurn", object=obj.name) + + + + + sensor = sensors[-1] + sensor.mouse_event = 'MOVEMENT' + sensor.use_pulse_true_level = True + controller = controllers[-1] + actuator = actuators[-1] + actuator.mode = 'LOOK' + actuator.use_axis_y = False + actuator.sensitivity_x = bpy.context.scene.mouse_sensitive + + sensor.link(controller) + actuator.link(controller) + + keys_list = [("UP_ARROW", "DOWN_ARROW", "RIGHT_ARROW", "LEFT_ARROW", "RIGHT_CTRL"), ("Z", "S", "D", "Q", "SPACE"), ("W", "S", "D", "A", "SPACE")] + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="Forward", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="Forward", object=obj.name) + bpy.ops.logic.actuator_add(type='MOTION', name="Forward", object=obj.name) + + sensors["Forward"].key = keys_list[int(bpy.context.scene.character_keys)][0] + actuators["Forward"].mode = "OBJECT_CHARACTER" + actuators["Forward"].offset_location[1] = bpy.context.scene.key_sensitive + + sensors["Forward"].link(controllers["Forward"]) + actuators["Forward"].link(controllers["Forward"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="back", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="back", object=obj.name) + bpy.ops.logic.actuator_add(type='MOTION', name="back", object=obj.name) + + sensors["back"].key = keys_list[int(bpy.context.scene.character_keys)][1] + actuators["back"].mode = "OBJECT_CHARACTER" + actuators["back"].offset_location[1] = -bpy.context.scene.key_sensitive + + sensors["back"].link(controllers["back"]) + actuators["back"].link(controllers["back"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="right", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="right", object=obj.name) + bpy.ops.logic.actuator_add(type='MOTION', name="right", object=obj.name) + + sensors["right"].key = keys_list[int(bpy.context.scene.character_keys)][2] + actuators["right"].mode = "OBJECT_CHARACTER" + actuators["right"].offset_location[0] = bpy.context.scene.key_sensitive + + sensors["right"].link(controllers["right"]) + actuators["right"].link(controllers["right"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="left", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="left", object=obj.name) + bpy.ops.logic.actuator_add(type='MOTION', name="left", object=obj.name) + + sensors["left"].key = keys_list[int(bpy.context.scene.character_keys)][3] + actuators["left"].mode = "OBJECT_CHARACTER" + actuators["left"].offset_location[0] = -bpy.context.scene.key_sensitive + + sensors["left"].link(controllers["left"]) + actuators["left"].link(controllers["left"]) + + if bpy.context.scene.character_jump == True: + bpy.ops.logic.sensor_add(type="KEYBOARD", name="jump", object=obj.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="jump", object=obj.name) + bpy.ops.logic.actuator_add(type='MOTION', name="jump", object=obj.name) + + sensors["jump"].key = keys_list[int(bpy.context.scene.character_keys)][4] + actuators["jump"].mode = "OBJECT_CHARACTER" + actuators["jump"].use_character_jump = True + + sensors["jump"].link(controllers["jump"]) + actuators["jump"].link(controllers["jump"]) + + + for sen in sensors: + sen.show_expanded = False + + for act in actuators: + act.show_expanded = False + + #create camera and configure + cam = bpy.data.cameras.new("CameraM") + cam_ob = bpy.data.objects.new("CameraM", cam) + bpy.context.scene.objects.link(cam_ob) + + cam_ob.location = (bpy.context.scene.cursor_location[0], + bpy.context.scene.cursor_location[1], bpy.context.scene.cursor_location[2]+(bpy.context.scene.character_size/2.2)) + cam_ob.rotation_euler = (1.5708, 0, 0) + cam_ob.name = "cam" + bpy.context.scene.character_name + + sensors = cam_ob.game.sensors + controllers = cam_ob.game.controllers + actuators = cam_ob.game.actuators + + bpy.ops.logic.sensor_add(type="MOUSE", object=cam_ob.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", object=cam_ob.name) + bpy.ops.logic.actuator_add(type='MOUSE', name="HeadTurn", object=cam_ob.name) + + + sensor = sensors[-1] + sensor.mouse_event = 'MOVEMENT' + sensor.use_pulse_true_level = True + controller = controllers[-1] + actuator = actuators[-1] + actuator.mode = 'LOOK' + actuator.use_axis_x = False + actuator.sensitivity_y = bpy.context.scene.mouse_sensitive + + sensor.link(controller) + actuator.link(controller) + + bpy.context.scene.objects.active = None + + obj.location = (obj.location[0], obj.location[1], obj.location[2]+(bpy.context.scene.character_size/2)) + cam_ob.location = (cam_ob.location[0], cam_ob.location[1], cam_ob.location[2]+(bpy.context.scene.character_size/2)) + + obj.select = True + cam_ob.select = True + bpy.context.scene.objects.active = obj + bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + + bpy.context.scene.objects.active = cam_ob + bpy.ops.view3d.object_as_camera() + + return {'FINISHED'} + +class fly_camera(Operator): + bl_label = 'Add Fly Camera' + bl_idname = 'fly_camera.gen' + bl_description = 'Generate a simple fly camera' + bl_context = 'objectmode' + + def execute(self, context): + + bpy.context.scene.render.engine = 'BLENDER_GAME' + bpy.context.scene.objects.active = None + + #create camera and configure + cam = bpy.data.cameras.new("CameraM") + cam_object = bpy.data.objects.new("CameraM", cam) + bpy.context.scene.objects.link(cam_object) + + cam_object.location = (bpy.context.scene.cursor_location) + + cam_object.rotation_euler = (1.5708, 0, 0) + cam_object.name = bpy.context.scene.character_name + bpy.context.scene.objects.active = cam_object + + sensors = cam_object.game.sensors + controllers = cam_object.game.controllers + actuators = cam_object.game.actuators + + bpy.ops.logic.sensor_add(type="MOUSE", name="MouseMove", object=cam_object.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="MouseMove", object=cam_object.name) + bpy.ops.logic.actuator_add(type='MOUSE', name="MouseMove", object=cam_object.name) + + sensors["MouseMove"].use_pulse_true_level = True + sensors["MouseMove"].mouse_event = 'MOVEMENT' + actuators["MouseMove"].mode = 'LOOK' + actuators["MouseMove"].sensitivity_x = bpy.context.scene.mouse_sensitive + actuators["MouseMove"].sensitivity_y = bpy.context.scene.mouse_sensitive + + + sensors["MouseMove"].link(controllers["MouseMove"]) + actuators["MouseMove"].link(controllers["MouseMove"]) + + keys_list = [("UP_ARROW", "DOWN_ARROW", "RIGHT_ARROW", "LEFT_ARROW", "RIGHT_CTRL"), ("Z", "S", "D", "Q", "SPACE"), ("W", "S", "D", "A", "SPACE")] + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="Forward", object=cam_object.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="Forward", object=cam_object.name) + bpy.ops.logic.actuator_add(type='MOTION', name="Forward", object=cam_object.name) + + sensors["Forward"].key = keys_list[int(bpy.context.scene.character_keys)][0] + actuators["Forward"].mode = "OBJECT_CHARACTER" + actuators["Forward"].offset_location[1] = bpy.context.scene.key_sensitive + + sensors["Forward"].link(controllers["Forward"]) + actuators["Forward"].link(controllers["Forward"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="back", object=cam_object.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="back", object=cam_object.name) + bpy.ops.logic.actuator_add(type='MOTION', name="back", object=cam_object.name) + + sensors["back"].key = keys_list[int(bpy.context.scene.character_keys)][1] + actuators["back"].mode = "OBJECT_CHARACTER" + actuators["back"].offset_location[1] = -bpy.context.scene.key_sensitive + + sensors["back"].link(controllers["back"]) + actuators["back"].link(controllers["back"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="right", object=cam_object.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="right", object=cam_object.name) + bpy.ops.logic.actuator_add(type='MOTION', name="right", object=cam_object.name) + + sensors["right"].key = keys_list[int(bpy.context.scene.character_keys)][2] + actuators["right"].mode = "OBJECT_CHARACTER" + actuators["right"].offset_location[0] = bpy.context.scene.key_sensitive + + sensors["right"].link(controllers["right"]) + actuators["right"].link(controllers["right"]) + + bpy.ops.logic.sensor_add(type="KEYBOARD", name="left", object=cam_object.name) + bpy.ops.logic.controller_add(type="LOGIC_AND", name="left", object=cam_object.name) + bpy.ops.logic.actuator_add(type='MOTION', name="left", object=cam_object.name) + + sensors["left"].key = keys_list[int(bpy.context.scene.character_keys)][3] + actuators["left"].mode = "OBJECT_CHARACTER" + actuators["left"].offset_location[0] = -bpy.context.scene.key_sensitive + + sensors["left"].link(controllers["left"]) + actuators["left"].link(controllers["left"]) + + for sen in sensors: + sen.show_expanded = False + + for act in actuators: + act.show_expanded = False + + bpy.context.scene.objects.active = cam_object + bpy.ops.view3d.object_as_camera() + + return {'FINISHED'} + + +class addcharacter(Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = 'Simple Character' + bl_context = 'objectmode' + bl_category = 'Add Character' + + def draw(self, context): + layout = self.layout + row = layout.row() + row.prop(context.scene, "character_name", text="Name") + row = layout.row() + row.prop(context.scene, "character_size", text="Size") + row = layout.row() + row.prop(context.scene, "character_keys", text="Keys") + row = layout.row() + row.prop(context.scene, "key_sensitive", text="Key sensitive") + row = layout.row() + row.prop(context.scene, "mouse_sensitive", text="Mouse sensitive") + row = layout.row() + row.prop(context.scene, "character_jump", text="Jump available") + row = layout.row() + row.operator(simple_character.bl_idname, text='Add Character') + row = layout.row() + row.operator(fly_camera.bl_idname, text='Add Fly Camera') + +def register(): + bpy.utils.register_class(addcharacter) + bpy.utils.register_class(fly_camera) + bpy.utils.register_class(simple_character) + +def unregister(): + bpy.utils.unregister_class(addcharacter) + bpy.utils.unregister_class(fly_camera) + bpy.utils.unregister_class(simple_character) + +if __name__ == '__main__': + register() diff --git a/scripts/addons_core/game_engine_object_camera_vertex_cull.py b/scripts/addons_core/game_engine_object_camera_vertex_cull.py new file mode 100644 index 00000000000..dbb151ef544 --- /dev/null +++ b/scripts/addons_core/game_engine_object_camera_vertex_cull.py @@ -0,0 +1,287 @@ +# SPDX-FileCopyrightText: Mitko Nikov +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import bpy +from bpy.types import AddonPreferences, PropertyGroup, Panel +from bpy.props import (StringProperty, EnumProperty, IntProperty, + FloatProperty, BoolProperty, PointerProperty) + +from bpy.app.handlers import persistent +from bpy_extras.object_utils import world_to_camera_view + +from mathutils import * +from math import * + +bl_info = { + "name": "Camera Vertex Cull", + "author": "Mitko Nikov", + "version": (1, 0, 1), + "blender": (2, 83, 0), + "location": "Object > Camera Vertex Cull", + "description": "Hide vertices, edges and polys based on Camera Frustum.", + "doc_url": "", + "category": "Camera" +} + +debug = False +hasEverEnabled = False +inWork = False # used for debouncing + +# This creates a new array of vertices +# And applies the transform matrix in it +def applyTransform(obj): + global debug + + mat = obj.matrix_world + vertices = obj.data.vertices + + if debug: + print('------') + print(mat) + print('------') + + verts = [] + for vert in vertices: + verts.append(mat @ vert.co) + + return verts + +# Gets the camera according to the context +# Also, does some cool checks +def getCamera(context): + camera = context.scene.camera + if camera is None: + print("No scene camera") + elif camera.type == 'CAMERA': + if debug: + print("Regular scene camera") + else: + print("%s object as camera" % camera.type) + return False + + if debug: + print(camera.data.view_frame()) + + return camera + +@persistent +def update_handler(self, dummy): + context = bpy.context + camera = getCamera(context) + if camera is False: + return False + + for obj in context.scene.objects: + if obj.type == 'MESH': + update_object(obj, context.scene, camera) + +# This function updates the object based on context +def update_calc(self, context): + object = context.object + scene = context.scene + + camera = getCamera(context) + if camera is False: + return False + + if (object.type == 'MESH'): + update_object(object, scene, camera) + +# This is the main update object function +def update_object(object, scene, camera): + global debug + global inWork + + if inWork: + return + + enabled = object.camera_cull_props.camera_cull_enabled + dist_enabled = object.camera_cull_props.distance_cull_enabled + margin = object.camera_cull_props.margin + distance = object.camera_cull_props.distance + + if debug: + print("Enabled: ", enabled) + + data = object.data + vgs = object.vertex_groups + + if (object.type == 'CAMERA'): + print("Object is camera, cannot make a Camera Vertex Cull") + return False + + vg = None + done = False + + # Search for our Hide Group (Vertex Group) + for vgi in vgs: + if (vgi.name == "Hide_Group"): + vg = vgi + done = True + + # Create it if it doesn't exist + if done is False: + if enabled is False: + return + + vg = object.vertex_groups.new(name="Hide_Group") + + # set every vertex weight to -1 + for v in data.vertices: + vg.add([v.index], 1, "SUBTRACT") + + if enabled: + # Apply the location, scale and rotation matrix + objAfterTransform = applyTransform(object) + + # Convert to Camera View + coords_2d = [world_to_camera_view(scene, camera, coord) for coord in objAfterTransform] + + # Iterate through the vertices + # The count is used to find the vertex index + count = -1 + for x, y, distance_to_lens in coords_2d: + count = count + 1 + if (x >= -margin and x <= 1 + margin and y >= -margin and y <= 1 + margin): + if (dist_enabled): + if (distance_to_lens <= distance): + continue + else: + continue + + if debug: + print("Pixel Coords:", (x, y, distance_to_lens)) + + vg.add([data.vertices[count].index], 1, "ADD") + + inWork = True + # Search for a Mask Modifier + alreadyHaveMask = False + for mod in object.modifiers: + if mod.type == 'MASK': + alreadyHaveMask = True + break + + # Add if there's none + if not alreadyHaveMask: + bpy.ops.object.modifier_add(type='MASK') + + # Set up the modifier with the vertex group + object.modifiers["Mask"].vertex_group = "Hide_Group" + object.modifiers["Mask"].invert_vertex_group = True + + # Search for the handlers + toAddHandler = True + for han in bpy.app.handlers.frame_change_post: + if han.__name__ is 'update_handler': + toAddHandler = False + break + + # Add the handlers if they don't exist + # There's no need of the handlers if the addon is never used + if toAddHandler: + bpy.app.handlers.frame_change_post.append(update_handler) + bpy.app.handlers.depsgraph_update_post.append(update_handler) + if debug: + print("[Camera Vertex Cull] Handlers added.") + + # This is used to know to remove the handlers if it's used + global hasEverEnabled + hasEverEnabled = True + inWork = False + +class CameraCullProperties(PropertyGroup): + camera_cull_enabled: BoolProperty( + name="Enable", + description="Hide vertexes based on the Camera frustum", + default=False, + update=update_calc + ) + + distance_cull_enabled: BoolProperty( + name="Enable Distance Cull", + description="Hide vertexes based on the Camera Distance", + default=False, + update=update_calc + ) + + margin: FloatProperty( + name="Margin", + description="Threshold outside the Camera frustum", + update=update_calc, + default=0.3, + precision=3, + min=0, + max=100 + ) + + distance: FloatProperty( + name="Distance", + description="Culling Distance", + update=update_calc, + default=30, + precision=1, + min=0, + max=1000 + ) + +class PLS_PT_CameraCullPropertiesPanel(Panel): + """Camera Vertex Cull""" + bl_idname = "PLS_PT_camera_vertex_cull" + bl_label = "Camera Vertex Cull" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'object' + + context = None + + def draw(self, context): + layout = self.layout + if context.object.type == 'MESH': + settings = context.object.camera_cull_props + + row = layout.row() + + row.prop(settings, "camera_cull_enabled") + row.prop(settings, "distance_cull_enabled") + + layout.prop(settings, "margin") + layout.prop(settings, "distance") + else: + layout.label(text="Select a mesh object") + +class CameraVertexCullPreferences(AddonPreferences): + bl_idname = __name__ + + def draw(self, context): + layout = self.layout + layout.label(text="Vertex and Distance based culling") + +classes = ( + CameraCullProperties, + PLS_PT_CameraCullPropertiesPanel, + CameraVertexCullPreferences +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Object.camera_cull_props = PointerProperty( + type=CameraCullProperties) + +def unregister(): + global hasEverEnabled + + for cls in classes: + bpy.utils.unregister_class(cls) + + del bpy.types.Object.camera_cull_props + + if hasEverEnabled: + bpy.app.handlers.frame_change_post.remove(update_handler) + bpy.app.handlers.depsgraph_update_post.remove(update_handler) + +if __name__ == "__main__": + register() diff --git a/scripts/addons_core/game_engine_publishing.py b/scripts/addons_core/game_engine_publishing.py new file mode 100644 index 00000000000..495b01237ab --- /dev/null +++ b/scripts/addons_core/game_engine_publishing.py @@ -0,0 +1,576 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +import os +import tempfile +import shutil +import tarfile +import time +import stat + + +bl_info = { + "name": "Game Engine Publishing", + "author": "Mitchell Stokes (Moguri), Oren Titane (Genome36)", + "version": (0, 1, 0), + "blender": (2, 75, 0), + "location": "Render Properties > Publishing Info", + "description": "Publish .blend file as game engine runtime, manage versions and platforms", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Game_Engine/Publishing", + "category": "Game Engine", +} + + +def WriteRuntime(player_path, output_path, asset_paths, copy_python, overwrite_lib, copy_dlls, make_archive, report=print): + import struct + + player_path = bpy.path.abspath(player_path) + ext = os.path.splitext(player_path)[-1].lower() + output_path = bpy.path.abspath(output_path) + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + python_dir = os.path.join(os.path.dirname(player_path), + bpy.app.version_string.split()[0], + "python", + "lib") + + # Check the paths + if not os.path.isfile(player_path) and not(os.path.exists(player_path) and player_path.endswith('.app')): + report({'ERROR'}, "The player could not be found! Runtime not saved") + return + + # Check if we're bundling a .app + if player_path.lower().endswith('.app'): + # Python doesn't need to be copied for OS X since it's already inside blenderplayer.app + copy_python = False + + output_path = bpy.path.ensure_ext(output_path, '.app') + + if os.path.exists(output_path): + shutil.rmtree(output_path) + + shutil.copytree(player_path, output_path) + bpy.ops.wm.save_as_mainfile(filepath=os.path.join(output_path, 'Contents', 'Resources', 'game.blend'), + relative_remap=False, + compress=False, + copy=True, + ) + else: + # Enforce "exe" extension on Windows + if player_path.lower().endswith('.exe'): + output_path = bpy.path.ensure_ext(output_path, '.exe') + + # Get the player's binary and the offset for the blend + with open(player_path, "rb") as file: + player_d = file.read() + offset = file.tell() + + # Create a tmp blend file (Blenderplayer doesn't like compressed blends) + tempdir = tempfile.mkdtemp() + blend_path = os.path.join(tempdir, bpy.path.clean_name(output_path)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path, + relative_remap=False, + compress=False, + copy=True, + ) + + # Get the blend data + with open(blend_path, "rb") as blend_file: + blend_d = blend_file.read() + + # Get rid of the tmp blend, we're done with it + os.remove(blend_path) + os.rmdir(tempdir) + + # Create a new file for the bundled runtime + with open(output_path, "wb") as output: + # Write the player and blend data to the new runtime + print("Writing runtime...", end=" ", flush=True) + output.write(player_d) + output.write(blend_d) + + # Store the offset (an int is 4 bytes, so we split it up into 4 bytes and save it) + output.write(struct.pack('BBBB', (offset >> 24) & 0xFF, + (offset >> 16) & 0xFF, + (offset >> 8) & 0xFF, + (offset >> 0) & 0xFF)) + + # Stuff for the runtime + output.write(b'BRUNTIME') + + print("done", flush=True) + + # Make sure the runtime is executable + os.chmod(output_path, 0o755) + + # Copy bundled Python + blender_dir = os.path.dirname(player_path) + + if copy_python: + print("Copying Python files...", end=" ", flush=True) + py_folder = os.path.join(bpy.app.version_string.split()[0], "python", "lib") + dst = os.path.join(output_dir, py_folder) + src = python_dir + + if os.path.exists(dst) and overwrite_lib: + shutil.rmtree(dst) + + if not os.path.exists(dst): + shutil.copytree(src, dst, ignore=lambda dir, contents: [i for i in contents if i == '__pycache__']) + print("done", flush=True) + else: + print("used existing Python folder", flush=True) + + # And DLLs if we're doing a Windows runtime) + if copy_dlls and ext == ".exe": + print("Copying DLLs...", end=" ", flush=True) + for file in [i for i in os.listdir(blender_dir) if i.lower().endswith('.dll')]: + src = os.path.join(blender_dir, file) + dst = os.path.join(output_dir, file) + shutil.copy2(src, dst) + + print("done", flush=True) + + # Copy assets + for ap in asset_paths: + src = bpy.path.abspath(ap.name) + dst = os.path.join(output_dir, ap.name[2:] if ap.name.startswith('//') else ap.name) + + if os.path.exists(src): + if os.path.isdir(src): + if ap.overwrite and os.path.exists(dst): + shutil.rmtree(dst) + elif not os.path.exists(dst): + shutil.copytree(src, dst) + else: + if ap.overwrite or not os.path.exists(dst): + shutil.copy2(src, dst) + else: + report({'ERROR'}, "Could not find asset path: '%s'" % src) + + # Make archive + if make_archive: + print("Making archive...", end=" ", flush=True) + + arctype = '' + if player_path.lower().endswith('.exe'): + arctype = 'zip' + elif player_path.lower().endswith('.app'): + arctype = 'zip' + else: # Linux + arctype = 'gztar' + + basedir = os.path.normpath(os.path.join(os.path.dirname(output_path), '..')) + afilename = os.path.join(basedir, os.path.basename(output_dir)) + + if arctype == 'gztar': + # Create the tarball ourselves instead of using shutil.make_archive + # so we can handle permission bits. + + # The runtimename needs to use forward slashes as a path separator + # since this is what tarinfo.name is using. + runtimename = os.path.relpath(output_path, basedir).replace('\\', '/') + + def _set_ex_perm(tarinfo): + if tarinfo.name == runtimename: + tarinfo.mode = 0o755 + return tarinfo + + with tarfile.open(afilename + '.tar.gz', 'w:gz') as tf: + tf.add(output_dir, os.path.relpath(output_dir, basedir), filter=_set_ex_perm) + elif arctype == 'zip': + shutil.make_archive(afilename, 'zip', output_dir) + else: + report({'ERROR'}, "Unknown archive type %s for runtime %s\n" % (arctype, player_path)) + + print("done", flush=True) + + +class PublishAllPlatforms(bpy.types.Operator): + bl_idname = "wm.publish_platforms" + bl_label = "Exports a runtime for each listed platform" + + def execute(self, context): + ps = context.scene.ge_publish_settings + + if ps.publish_default_platform: + print("Publishing default platform") + blender_bin_path = bpy.app.binary_path + blender_bin_dir = os.path.dirname(blender_bin_path) + ext = os.path.splitext(blender_bin_path)[-1].lower() + WriteRuntime(os.path.join(blender_bin_dir, 'blenderplayer' + ext), + os.path.join(ps.output_path, 'default', ps.runtime_name), + ps.asset_paths, + True, + True, + True, + ps.make_archive, + self.report + ) + else: + print("Skipping default platform") + + for platform in ps.platforms: + if platform.publish: + print("Publishing", platform.name) + WriteRuntime(platform.player_path, + os.path.join(ps.output_path, platform.name, ps.runtime_name), + ps.asset_paths, + True, + True, + True, + ps.make_archive, + self.report + ) + else: + print("Skipping", platform.name) + + return {'FINISHED'} + + +class RENDER_UL_assets(bpy.types.UIList): + bl_label = "Asset Paths Listing" + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + layout.prop(item, "name", text="", emboss=False) + + +class RENDER_UL_platforms(bpy.types.UIList): + bl_label = "Platforms Listing" + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + row = layout.row() + row.label(item.name) + row.prop(item, "publish", text="") + + +class RENDER_PT_publish(bpy.types.Panel): + bl_label = "Publishing Info" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + @classmethod + def poll(cls, context): + scene = context.scene + return scene and (scene.render.engine == "BLENDER_GAME") + + def draw(self, context): + ps = context.scene.ge_publish_settings + layout = self.layout + + # config + layout.prop(ps, 'output_path') + layout.prop(ps, 'runtime_name') + layout.prop(ps, 'lib_path') + layout.prop(ps, 'make_archive') + + layout.separator() + + # assets list + layout.label("Asset Paths") + + # UI_UL_list + row = layout.row() + row.template_list("RENDER_UL_assets", "assets_list", ps, 'asset_paths', ps, 'asset_paths_active') + + # operators + col = row.column(align=True) + col.operator(PublishAddAssetPath.bl_idname, icon='ZOOMIN', text="") + col.operator(PublishRemoveAssetPath.bl_idname, icon='ZOOMOUT', text="") + + # indexing + if len(ps.asset_paths) > ps.asset_paths_active >= 0: + ap = ps.asset_paths[ps.asset_paths_active] + row = layout.row() + row.prop(ap, 'overwrite') + + layout.separator() + + # publishing list + row = layout.row(align=True) + row.label("Platforms") + row.prop(ps, 'publish_default_platform') + + # UI_UL_list + row = layout.row() + row.template_list("RENDER_UL_platforms", "platforms_list", ps, 'platforms', ps, 'platforms_active') + + # operators + col = row.column(align=True) + col.operator(PublishAddPlatform.bl_idname, icon='ZOOMIN', text="") + col.operator(PublishRemovePlatform.bl_idname, icon='ZOOMOUT', text="") + col.menu("PUBLISH_MT_platform_specials", icon='DOWNARROW_HLT', text="") + + # indexing + if len(ps.platforms) > ps.platforms_active >= 0: + platform = ps.platforms[ps.platforms_active] + layout.prop(platform, 'name') + layout.prop(platform, 'player_path') + + layout.operator(PublishAllPlatforms.bl_idname, 'Publish Platforms') + + +class PublishAutoPlatforms(bpy.types.Operator): + bl_idname = "scene.publish_auto_platforms" + bl_label = "Auto Add Platforms" + + def execute(self, context): + ps = context.scene.ge_publish_settings + + # verify lib folder + lib_path = bpy.path.abspath(ps.lib_path) + if not os.path.exists(lib_path): + self.report({'ERROR'}, "Could not add platforms, lib folder (%s) does not exist" % lib_path) + return {'CANCELLED'} + + for lib in [i for i in os.listdir(lib_path) if os.path.isdir(os.path.join(lib_path, i))]: + print("Found folder:", lib) + player_found = False + for root, dirs, files in os.walk(os.path.join(lib_path, lib)): + if "__MACOSX" in root: + continue + + for f in dirs + files: + if f.startswith("blenderplayer.app") or f.startswith("blenderplayer"): + a = ps.platforms.add() + if lib.startswith('blender-'): + # Clean up names for packages from blender.org + # example: blender-2.71-RC2-OSX_10.6-x86_64.zip => OSX_10.6-x86_64.zip + # We're pretty consistent on naming, so this should hold up. + a.name = '-'.join(lib.split('-')[3 if 'rc' in lib.lower() else 2:]) + else: + a.name = lib + a.player_path = bpy.path.relpath(os.path.join(root, f)) + player_found = True + break + + if player_found: + break + + return {'FINISHED'} + +# TODO This operator takes a long time to run, which is bad for UX. Could this instead be done as some sort of +# modal dialog? This could also allow users to select which platforms to download and give a better progress +# indicator. +class PublishDownloadPlatforms(bpy.types.Operator): + bl_idname = "scene.publish_download_platforms" + bl_label = "Download Platforms" + + def execute(self, context): + import html.parser + import urllib.request + + remote_platforms = [] + + ps = context.scene.ge_publish_settings + + # create lib folder if not already available + lib_path = bpy.path.abspath(ps.lib_path) + if not os.path.exists(lib_path): + os.makedirs(lib_path) + + print("Retrieving list of platforms from blender.org...", end=" ", flush=True) + + class AnchorParser(html.parser.HTMLParser): + def handle_starttag(self, tag, attrs): + if tag == 'a': + for key, value in attrs: + if key == 'href' and value.startswith('blender'): + remote_platforms.append(value) + + url = 'http://download.blender.org/release/Blender' + bpy.app.version_string.split()[0] + parser = AnchorParser() + data = urllib.request.urlopen(url).read() + parser.feed(str(data)) + + print("done", flush=True) + + print("Downloading files (this will take a while depending on your internet connection speed).", flush=True) + for i in remote_platforms: + src = '/'.join((url, i)) + dst = os.path.join(lib_path, i) + + dst_dir = '.'.join([i for i in dst.split('.') if i not in {'zip', 'tar', 'bz2'}]) + if not os.path.exists(dst) and not os.path.exists(dst.split('.')[0]): + print("Downloading " + src + "...", end=" ", flush=True) + urllib.request.urlretrieve(src, dst) + print("done", flush=True) + else: + print("Reusing existing file: " + dst, flush=True) + + print("Unpacking " + dst + "...", end=" ", flush=True) + if os.path.exists(dst_dir): + shutil.rmtree(dst_dir) + shutil.unpack_archive(dst, dst_dir) + print("done", flush=True) + + print("Creating platform from libs...", flush=True) + bpy.ops.scene.publish_auto_platforms() + return {'FINISHED'} + + +class PublishAddPlatform(bpy.types.Operator): + bl_idname = "scene.publish_add_platform" + bl_label = "Add Publish Platform" + + def execute(self, context): + a = context.scene.ge_publish_settings.platforms.add() + a.name = a.name + return {'FINISHED'} + + +class PublishRemovePlatform(bpy.types.Operator): + bl_idname = "scene.publish_remove_platform" + bl_label = "Remove Publish Platform" + + def execute(self, context): + ps = context.scene.ge_publish_settings + if ps.platforms_active < len(ps.platforms): + ps.platforms.remove(ps.platforms_active) + return {'FINISHED'} + return {'CANCELLED'} + + +# TODO maybe this should display a file browser? +class PublishAddAssetPath(bpy.types.Operator): + bl_idname = "scene.publish_add_assetpath" + bl_label = "Add Asset Path" + + def execute(self, context): + a = context.scene.ge_publish_settings.asset_paths.add() + a.name = a.name + return {'FINISHED'} + + +class PublishRemoveAssetPath(bpy.types.Operator): + bl_idname = "scene.publish_remove_assetpath" + bl_label = "Remove Asset Path" + + def execute(self, context): + ps = context.scene.ge_publish_settings + if ps.asset_paths_active < len(ps.asset_paths): + ps.asset_paths.remove(ps.asset_paths_active) + return {'FINISHED'} + return {'CANCELLED'} + + +class PUBLISH_MT_platform_specials(bpy.types.Menu): + bl_label = "Platform Specials" + + def draw(self, context): + layout = self.layout + layout.operator(PublishAutoPlatforms.bl_idname) + layout.operator(PublishDownloadPlatforms.bl_idname) + + +class PlatformSettings(bpy.types.PropertyGroup): + name = bpy.props.StringProperty( + name = "Platform Name", + description = "The name of the platform", + default = "Platform", + ) + + player_path = bpy.props.StringProperty( + name = "Player Path", + description = "The path to the Blenderplayer to use for this platform", + default = "//lib/platform/blenderplayer", + subtype = 'FILE_PATH', + ) + + publish = bpy.props.BoolProperty( + name = "Publish", + description = "Whether or not to publish to this platform", + default = True, + ) + + +class AssetPath(bpy.types.PropertyGroup): + # TODO This needs a way to be a FILE_PATH or a DIR_PATH + name = bpy.props.StringProperty( + name = "Asset Path", + description = "Path to the asset to be copied", + default = "//src", + subtype = 'FILE_PATH', + ) + + overwrite = bpy.props.BoolProperty( + name = "Overwrite Asset", + description = "Overwrite the asset if it already exists in the destination folder", + default = True, + ) + + +class PublishSettings(bpy.types.PropertyGroup): + output_path = bpy.props.StringProperty( + name = "Publish Output", + description = "Where to publish the game", + default = "//bin/", + subtype = 'DIR_PATH', + ) + + runtime_name = bpy.props.StringProperty( + name = "Runtime name", + description = "The filename for the created runtime", + default = "game", + ) + + lib_path = bpy.props.StringProperty( + name = "Library Path", + description = "Directory to search for platforms", + default = "//lib/", + subtype = 'DIR_PATH', + ) + + publish_default_platform = bpy.props.BoolProperty( + name = "Publish Default Platform", + description = "Whether or not to publish the default platform (the Blender install running this addon) when publishing platforms", + default = True, + ) + + + platforms = bpy.props.CollectionProperty(type=PlatformSettings, name="Platforms") + platforms_active = bpy.props.IntProperty() + + asset_paths = bpy.props.CollectionProperty(type=AssetPath, name="Asset Paths") + asset_paths_active = bpy.props.IntProperty() + + make_archive = bpy.props.BoolProperty( + name = "Make Archive", + description = "Create a zip archive of the published game", + default = True, + ) + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.Scene.ge_publish_settings = bpy.props.PointerProperty(type=PublishSettings) + + +def unregister(): + bpy.utils.unregister_module(__name__) + del bpy.types.Scene.ge_publish_settings + + +if __name__ == "__main__": + register() diff --git a/scripts/addons_core/game_engine_save_as_runtime_eevee.py b/scripts/addons_core/game_engine_save_as_runtime_eevee.py new file mode 100644 index 00000000000..1b97ccd69bb --- /dev/null +++ b/scripts/addons_core/game_engine_save_as_runtime_eevee.py @@ -0,0 +1,416 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Save As Game Engine Runtime", + "author": "Mitchell Stokes (Moguri), Ulysse Martin (youle), Jorge Bernal (lordloki)", + "version": (0, 9, 0), + "blender": (2, 80, 0), + "location": "File > Import-Export", + "description": "Bundle a .blend file with the Blenderplayer", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/Game_Engine/Save_As_Runtime", + "category": "Import-Export", +} + +import bpy +import os +import sys +import shutil +import tempfile +import subprocess + + +def CopyPythonLibs(dst, overwrite_lib, report=print): + import platform + + # use python module to find python's libpath + src = os.path.dirname(platform.__file__) + + # dst points to lib/, but src points to current python's library path, eg: + # '/usr/lib/python3.2' vs '/usr/lib' + # append python's library dir name to destination, so only python's + # libraries would be copied + if os.name == 'posix': + dst = os.path.join(dst, os.path.basename(src)) + + if os.path.exists(src): + write = False + if os.path.exists(dst): + if overwrite_lib: + shutil.rmtree(dst) + write = True + else: + write = True + if write: + shutil.copytree(src, dst, ignore=lambda dir, contents: [i for i in contents if i == '__pycache__']) + else: + report({'WARNING'}, "Python not found in %r, skipping python copy" % src) + + +def WriteAppleRuntime(player_path, output_path, copy_python, overwrite_lib): + # Enforce the extension + if not output_path.endswith('.app'): + output_path += '.app' + + # Use the system's cp command to preserve some meta-data + os.system('cp -R "%s" "%s"' % (player_path, output_path)) + + bpy.ops.wm.save_as_mainfile(filepath=os.path.join(output_path, "Contents/Resources/game.blend"), + relative_remap=False, + compress=False, + copy=True, + ) + + # Python doesn't need to be copied for OS X since it's already inside blenderplayer.app + + +def WriteRuntime(player_path, output_path, new_icon_path, copy_python, overwrite_lib, copy_dlls, copy_libs, copy_scripts, copy_datafiles, copy_modules, copy_logic_nodes, report=print): + import struct + + # Check the paths + if not os.path.isfile(player_path) and not(os.path.exists(player_path) and player_path.endswith('.app')): + report({'ERROR'}, "The player could not be found! Runtime not saved") + return + + # Check if we're bundling a .app + if player_path.endswith('.app'): + WriteAppleRuntime(player_path, output_path, copy_python, overwrite_lib) + return + + # Enforce "exe" extension on Windows + if player_path.endswith('.exe') and not output_path.endswith('.exe'): + output_path += '.exe' + + # Setup main folders + blender_dir = os.path.dirname(bpy.app.binary_path) + runtime_dir = os.path.dirname(output_path) + + # Extract new version string. Only take first 3 digits (i.e 3.0) + string = bpy.app.version_string.split()[0] + version_string = string[:3] + + # Create temporal directory + tempdir = tempfile.mkdtemp() + player_path_temp = player_path + + # Change the icon for Windows + if (new_icon_path != '' and output_path.endswith('.exe')): + player_path_temp = os.path.join(tempdir, bpy.path.clean_name(player_path)) + shutil.copyfile(player_path, player_path_temp) + rcedit_folder = os.path.join(version_string, "rceditcustom") + rcedit_path = os.path.join(blender_dir, rcedit_folder, "rcedit-x64.exe") + subprocess.check_call([rcedit_path, player_path_temp, "--set-icon", new_icon_path]) + + # Get the player's binary and the offset for the blend + file = open(player_path_temp, 'rb') + player_d = file.read() + offset = file.tell() + file.close() + + # Create a tmp blend file (Blenderplayer doesn't like compressed blends) + blend_path = os.path.join(tempdir, bpy.path.clean_name(output_path)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path, + relative_remap=False, + compress=False, + copy=True, + ) + + # Get the blend data + blend_file = open(blend_path, 'rb') + blend_d = blend_file.read() + blend_file.close() + + # Get rid of the tmp blend, we're done with it + os.remove(blend_path) + if (new_icon_path != '' and output_path.endswith('.exe')): + os.remove(player_path_temp) + os.rmdir(tempdir) + + # Create a new file for the bundled runtime + output = open(output_path, 'wb') + + # Write the player and blend data to the new runtime + print("Writing runtime...", end=" ") + output.write(player_d) + output.write(blend_d) + + # Store the offset (an int is 4 bytes, so we split it up into 4 bytes and save it) + output.write(struct.pack('B', (offset>>24)&0xFF)) + output.write(struct.pack('B', (offset>>16)&0xFF)) + output.write(struct.pack('B', (offset>>8)&0xFF)) + output.write(struct.pack('B', (offset>>0)&0xFF)) + + # Stuff for the runtime + output.write(b'BRUNTIME') + output.close() + + print("done") + + # Make the runtime executable on Linux + if os.name == 'posix': + os.chmod(output_path, 0o755) + + if copy_python: + print("Copying Python files...", end=" ") + py_folder = os.path.join(version_string, "python", "lib") + dst = os.path.join(runtime_dir, py_folder) + CopyPythonLibs(dst, overwrite_lib, report) + if output_path.endswith('.exe'): + py_folder = os.path.join(version_string, "python", "DLLs") + src = os.path.join(blender_dir, py_folder) + dst = os.path.join(runtime_dir, py_folder) + shutil.copytree(src, dst) + print("done") + + # Copy DLLs + if copy_dlls: + print("Copying DLLs...", end=" ") + # Dlls at executable level + for file in [i for i in os.listdir(blender_dir) if i.lower().endswith('.dll')]: + src = os.path.join(blender_dir, file) + dst = os.path.join(runtime_dir, file) + shutil.copy2(src, dst) + # blender.crt DLLs + src = os.path.join(blender_dir, "blender.crt") + dst = os.path.join(runtime_dir, "blender.crt") + shutil.copytree(src, dst) + # blender.shared DLLs + src = os.path.join(blender_dir, "blender.shared") + dst = os.path.join(runtime_dir, "blender.shared") + shutil.copytree(src, dst) + print("done") + + # Copy linux shared libs + if copy_libs: + print("Copying shared libs...", end=" ") + # blender.crt DLLs + src = os.path.join(blender_dir, "lib") + dst = os.path.join(runtime_dir, "lib") + shutil.copytree(src, dst) + print("done") + + # Copy Scripts folder (also copy this folder when logic nodes option is selected) + if copy_scripts or copy_logic_nodes: + print("Copying scripts and modules...", end=" ") + scripts_folder = os.path.join(version_string, "scripts") + src = os.path.join(blender_dir, scripts_folder) + dst = os.path.join(runtime_dir, scripts_folder) + shutil.copytree(src, dst) + print("done") + print("Copying userpref.blend to can use addons...", end=" ") + user_path = bpy.utils.resource_path('USER') + user_config_path = os.path.join(user_path, "config") + user_config_userpref_path = os.path.join(user_config_path, "userpref.blend") + runtime_config_folder = os.path.join(version_string, "config") + runtime_config_folder_path = os.path.join(runtime_dir, runtime_config_folder) + os.makedirs(runtime_config_folder_path) + shutil.copy2(user_config_userpref_path, runtime_config_folder_path) + print("done") + + # Copy logic nodes game folder + if copy_logic_nodes: + print("Copying logic nodes game folder...", end=" ") + blend_directory = os.path.dirname(bpy.data.filepath) + src = os.path.join(blend_directory, "bgelogic") + if os.path.exists(src): + dst = os.path.join(runtime_dir, "bgelogic") + shutil.copytree(src, dst) + print("done") + + # Copy datafiles folder + if copy_datafiles: + print("Copying datafiles...", end=" ") + datafiles_folder = os.path.join(version_string, "datafiles", "gamecontroller") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "colormanagement") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "fonts") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + shutil.copytree(src, dst) + datafiles_folder = os.path.join(version_string, "datafiles", "studiolights") + src = os.path.join(blender_dir, datafiles_folder) + dst = os.path.join(runtime_dir, datafiles_folder) + shutil.copytree(src, dst) + print("done") + + # Copy modules folder (to have bpy working) + if copy_modules and not (copy_scripts or copy_logic_nodes): + print("Copying modules...", end=" ") + modules_folder = os.path.join(version_string, "scripts", "modules") + src = os.path.join(blender_dir, modules_folder) + dst = os.path.join(runtime_dir, modules_folder) + shutil.copytree(src, dst) + print("done") + + # Copy license folder + print("Copying UPBGE license folder...", end=" ") + src = os.path.join(blender_dir, "license") + dst = os.path.join(runtime_dir, "engine.license") + shutil.copytree(src, dst) + license_folder = os.path.join(runtime_dir, "engine.license") + src = os.path.join(blender_dir, "copyright.txt") + dst = os.path.join(license_folder, "copyright.txt") + shutil.copy2(src, dst) + print("done") + +from bpy.props import * + + +class SaveAsRuntime(bpy.types.Operator): + bl_idname = "wm.save_as_runtime" + bl_label = "Save As Game Engine Runtime" + bl_options = {'REGISTER'} + + if sys.platform == 'darwin': + blender_bin_dir = '/' + os.path.join(*bpy.app.binary_path.split('/')[0:-4]) + ext = '.app' + blenderplayer_name = 'Blenderplayer' + else: + blender_bin_path = bpy.app.binary_path + blender_bin_dir = os.path.dirname(blender_bin_path) + ext = os.path.splitext(blender_bin_path)[-1].lower() + blenderplayer_name = 'blenderplayer' + + default_player_path = os.path.join(blender_bin_dir, blenderplayer_name + ext) + player_path: StringProperty( + name="Player Path", + description="The path to the player to use", + default=default_player_path, + subtype='FILE_PATH', + ) + filepath: StringProperty( + subtype='FILE_PATH', + ) + copy_python: BoolProperty( + name="Copy Python", + description="Copy bundle Python with the runtime", + default=True, + ) + overwrite_lib: BoolProperty( + name="Overwrite 'lib' folder", + description="Overwrites the lib folder (if one exists) with the bundled Python lib folder", + default=False, + ) + copy_scripts: BoolProperty( + name="Copy Scripts folder", + description="Copy bundle Scripts folder with the runtime", + default=False, + ) + copy_datafiles: BoolProperty( + name="Copy Datafiles folder", + description="Copy bundle datafiles folder with the runtime", + default=True, + ) + copy_modules: BoolProperty( + name="Copy Script>Modules folder", + description="Copy bundle modules folder with the runtime", + default=True, + ) + copy_logic_nodes: BoolProperty( + name="Copy Logic Nodes game folder", + description="Copy Logic Nodes game with the runtime", + default=True, + ) + + # Only Windows has dlls to copy or can modify icon + if ext == '.exe': + copy_dlls: BoolProperty( + name="Copy DLLs", + description="Copy all needed DLLs with the runtime", + default=True, + ) + new_icon_path: StringProperty( + name="New Icon Path", + description="The path to the new icon for player to use", + default="", + subtype='FILE_PATH', + ) + else: + copy_dlls = False + new_icon_path = False + + # Only Linux has lib folder + if os.name == 'posix': + copy_libs: BoolProperty( + name="Copy shared libs", + description="Copy all the needed executable shared libs with the runtime", + default=True, + ) + else: + copy_libs = False + + def execute(self, context): + import time + start_time = time.time() + print("Saving runtime to %r" % self.filepath) + WriteRuntime(self.player_path, + self.filepath, + self.new_icon_path, + self.copy_python, + self.overwrite_lib, + self.copy_dlls, + self.copy_libs, + self.copy_scripts, + self.copy_datafiles, + self.copy_modules, + self.copy_logic_nodes, + self.report, + ) + print("Finished in %.4fs" % (time.time() - start_time)) + return {'FINISHED'} + + def invoke(self, context, event): + if not self.filepath: + ext = '.app' if sys.platform == 'darwin' else os.path.splitext(bpy.app.binary_path)[-1] + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ext) + + wm = context.window_manager + wm.fileselect_add(self) + return {'RUNNING_MODAL'} + + +def menu_func_export(self, context): + self.layout.operator(SaveAsRuntime.bl_idname, text="Save as game runtime") + +classes = ( +SaveAsRuntime, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + + for cls in classes: + bpy.utils.unregister_class(cls) + + +if __name__ == "__main__": + register() diff --git a/scripts/addons_core/game_engine_spring_bones.py b/scripts/addons_core/game_engine_spring_bones.py new file mode 100644 index 00000000000..e2257df5a4b --- /dev/null +++ b/scripts/addons_core/game_engine_spring_bones.py @@ -0,0 +1,1301 @@ +# SPDX-FileCopyrightText: Artell, Samurai-X +# +# SPDX-License-Identifier: GPL-2.0-or-later + +bl_info = { + "name": "Spring Bones", + "author": "Artell, added game engine support by Samurai-X", + "version": (0, 9), + "blender": (2, 80, 0), + "location": "Properties > Bones", + "description": "Add a spring dynamic effect to a single/multiple bones", + "category": "Animation"} + + +import bpy, time +from bpy.app.handlers import persistent +from mathutils import * +import math +import numpy +from numpy import dot +from math import sqrt +#from mathutils import Vector + +script = ''' +import bpy +from numpy import dot +from math import sqrt +import numpy +from mathutils import Vector + +def lerp_vec(vec_a, vec_b, t): + return vec_a*t + vec_b*(1-t) + +def project_point_onto_plane(q, p, n): + # q = (vector) point source + # p = (vector) point belonging to the plane + # n = (vector) normal of the plane + + n = n.normalized() + return q - ((q-p).dot(n)) * n + +def project_point_onto_line(a, b, p): + # project the point p onto the line a,b + ap = p-a + ab = b-a + + fac_a = (p-a).dot(b-a) + fac_b = (p-b).dot(b-a) + + result = a + ap.dot(ab)/ab.dot(ab) * ab + + if fac_a < 0: + result = a + if fac_b > 0: + result = b + + return result + +def project_point_onto_tri(TRI, P): + # return the distance and the projected surface point + # between a point and a triangle in 3D + # original code: https://gist.github.com/joshuashaffer/ + # Author: Gwolyn Fischer + + B = TRI[0, :] + E0 = TRI[1, :] - B + # E0 = E0/sqrt(sum(E0.^2)); %normalize vector + E1 = TRI[2, :] - B + # E1 = E1/sqrt(sum(E1.^2)); %normalize vector + D = B - P + a = dot(E0, E0) + b = dot(E0, E1) + c = dot(E1, E1) + d = dot(E0, D) + e = dot(E1, D) + f = dot(D, D) + + #print "{0} {1} {2} ".format(B,E1,E0) + det = a * c - b * b + s = b * e - c * d + t = b * d - a * e + + # Terible tree of conditionals to determine in which region of the diagram + # shown above the projection of the point into the triangle-plane lies. + if (s + t) <= det: + if s < 0.0: + if t < 0.0: + # region4 + if d < 0: + t = 0.0 + if -d >= a: + s = 1.0 + sqrdistance = a + 2.0 * d + f + else: + s = -d / a + sqrdistance = d * s + f + else: + s = 0.0 + if e >= 0.0: + t = 0.0 + sqrdistance = f + else: + if -e >= c: + t = 1.0 + sqrdistance = c + 2.0 * e + f + else: + t = -e / c + sqrdistance = e * t + f + + # of region 4 + else: + # region 3 + s = 0 + if e >= 0: + t = 0 + sqrdistance = f + else: + if -e >= c: + t = 1 + sqrdistance = c + 2.0 * e + f + else: + t = -e / c + sqrdistance = e * t + f + # of region 3 + else: + if t < 0: + # region 5 + t = 0 + if d >= 0: + s = 0 + sqrdistance = f + else: + if -d >= a: + s = 1 + sqrdistance = a + 2.0 * d + f; # GF 20101013 fixed typo d*s ->2*d + else: + s = -d / a + sqrdistance = d * s + f + else: + # region 0 + invDet = 1.0 / det + s = s * invDet + t = t * invDet + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + else: + if s < 0.0: + # region 2 + tmp0 = b + d + tmp1 = c + e + if tmp1 > tmp0: # minimum on edge s+t=1 + numer = tmp1 - tmp0 + denom = a - 2.0 * b + c + if numer >= denom: + s = 1.0 + t = 0.0 + sqrdistance = a + 2.0 * d + f; # GF 20101014 fixed typo 2*b -> 2*d + else: + s = numer / denom + t = 1 - s + sqrdistance = s * (a * s + b * t + 2 * d) + t * (b * s + c * t + 2 * e) + f + + else: # minimum on edge s=0 + s = 0.0 + if tmp1 <= 0.0: + t = 1 + sqrdistance = c + 2.0 * e + f + else: + if e >= 0.0: + t = 0.0 + sqrdistance = f + else: + t = -e / c + sqrdistance = e * t + f + # of region 2 + else: + if t < 0.0: + # region6 + tmp0 = b + e + tmp1 = a + d + if tmp1 > tmp0: + numer = tmp1 - tmp0 + denom = a - 2.0 * b + c + if numer >= denom: + t = 1.0 + s = 0 + sqrdistance = c + 2.0 * e + f + else: + t = numer / denom + s = 1 - t + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + + else: + t = 0.0 + if tmp1 <= 0.0: + s = 1 + sqrdistance = a + 2.0 * d + f + else: + if d >= 0.0: + s = 0.0 + sqrdistance = f + else: + s = -d / a + sqrdistance = d * s + f + else: + # region 1 + numer = c + e - b - d + if numer <= 0: + s = 0.0 + t = 1.0 + sqrdistance = c + 2.0 * e + f + else: + denom = a - 2.0 * b + c + if numer >= denom: + s = 1.0 + t = 0.0 + sqrdistance = a + 2.0 * d + f + else: + s = numer / denom + t = 1 - s + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + + # account for numerical round-off error + if sqrdistance < 0: + sqrdistance = 0 + + dist = sqrt(sqrdistance) + + PP0 = B + s * E0 + t * E1 + return dist, PP0 + +def spring_bone(foo): + #print("running...") + scene = bpy.context.scene + deps = bpy.context.evaluated_depsgraph_get() + + for bone in scene.sb_spring_bones: + # collider, skip + if bone.sb_bone_collider: + continue + armature = bpy.data.objects[bone.armature] + pose_bone = armature.pose.bones[bone.name] + # no influence, skip + if pose_bone.sb_global_influence == 0.0: + continue + + emp_tail = bpy.data.objects.get(bone.name + '_spring_tail') + emp_head = bpy.data.objects.get(bone.name + '_spring') + + if emp_tail == None or emp_head == None: + #print("no empties found, return") + return + + emp_tail_loc, rot, scale = emp_tail.matrix_world.decompose() + + axis_locked = None + if 'sb_lock_axis' in pose_bone.keys(): + axis_locked = pose_bone.sb_lock_axis + + # add gravity + base_pos_dir = Vector((0,0,-pose_bone.sb_gravity)) + + # add spring + base_pos_dir += (emp_tail_loc - emp_head.location) + + # evaluate bones collision + if bone.sb_bone_colliding: + + for bone_col in scene.sb_spring_bones: + if bone_col.sb_bone_collider == False: + continue + #print("collider bone", bone_col.name) + pose_bone_col = armature.pose.bones[bone_col.name] + sb_collider_dist = pose_bone_col.sb_collider_dist + #col_dir = (pose_bone.head - pose_bone_col.head) + pose_bone_center = (pose_bone.tail + pose_bone.head)*0.5 + p = project_point_onto_line(pose_bone_col.head, pose_bone_col.tail, pose_bone_center) + col_dir = (pose_bone_center - p) + dist = col_dir.magnitude + + if dist < sb_collider_dist: + push_vec = col_dir.normalized() * (sb_collider_dist-dist)*pose_bone_col.sb_collider_force + if axis_locked != "NONE" and axis_locked != None: + if axis_locked == "+Y": + direction_check = pose_bone.y_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.y_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-Y": + direction_check = pose_bone.y_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.y_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "+X": + direction_check = pose_bone.x_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.y_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-X": + direction_check = pose_bone.x_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.y_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "+Z": + direction_check = pose_bone.z_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-Z": + direction_check = pose_bone.z_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + #push_vec = push_vec - pose_bone.y_axis.normalized()*0.02 + base_pos_dir += push_vec + + + + # evaluate mesh collision + if bone.sb_bone_colliding: + for mesh in scene.sb_mesh_colliders: + obj = bpy.data.objects.get(mesh.name) + pose_bone_center = (pose_bone.tail + pose_bone.head)*0.5 + col_dir = Vector((0.0,0.0,0.0)) + push_vec = Vector((0.0,0.0,0.0)) + + object_eval = obj.evaluated_get(deps) + evaluated_mesh = object_eval.to_mesh(preserve_all_data_layers=False, depsgraph=deps) + for tri in obj.data.loop_triangles: + tri_coords = [] + for vi in tri.vertices: + v_coord = evaluated_mesh.vertices[vi].co + v_coord_global = obj.matrix_world @ v_coord + tri_coords.append([v_coord_global[0], v_coord_global[1], v_coord_global[2]]) + + tri_array = numpy.array(tri_coords) + P = numpy.array([pose_bone_center[0], pose_bone_center[1], pose_bone_center[2]]) + dist, p = project_point_onto_tri(tri_array, P) + p = Vector((p[0], p[1], p[2])) + collision_dist = obj.sb_collider_dist + repel_force = obj.sb_collider_force + + if dist < collision_dist: + col_dir += (pose_bone_center - p) + push_vec = col_dir.normalized() * (collision_dist-dist) * repel_force + base_pos_dir += push_vec * pose_bone.sb_global_influence + + + # add velocity + bone.speed += base_pos_dir * pose_bone.sb_stiffness + bone.speed *= pose_bone.sb_damp + + emp_head.location += bone.speed + # global influence + emp_head.location = lerp_vec(emp_head.location, emp_tail_loc, pose_bone.sb_global_influence) + + return None +if bpy.context.scene.sb_spring_game: + if bpy.context.scene.sb_lastexec >= bpy.context.scene.sb_frame_tickrate: + bpy.context.scene.sb_lastexec = 1 + spring_bone(True) + else: + bpy.context.scene.sb_lastexec += 1 +''' + +#print('\n Start Spring Bones Addon... \n') + + +def set_active_object(object_name): + bpy.context.view_layer.objects.active = bpy.data.objects[object_name] + bpy.data.objects[object_name].select_set(state=1) + + +def get_pose_bone(name): + try: + return bpy.context.object.pose.bones[name] + except: + return None + +@persistent +def spring_bone_frame_mode(foo): + if bpy.context.scene.sb_global_spring_frame == True: + spring_bone(foo) + +@persistent +def spring_bone_gamestart(foo): + if bpy.context.scene.sb_spring_game: + try: + update_bone(None, bpy.context) + except: + print("Initialize spring bones failed") + else: + my_obj = bpy.context.scene.armatr + if not my_obj.script_created: + bpy.ops.text.new() + text = bpy.data.texts[-1] + text.write(script) + bpy.ops.logic.sensor_add(name='SpringBones', type='ALWAYS', object=my_obj.name) + bpy.ops.logic.controller_add(type='PYTHON', object=my_obj.name) + sensor = my_obj.game.sensors[-1] + sensor.use_pulse_true_level = True + cont = my_obj.game.controllers[-1] + cont.text = text + sensor.link(cont) + my_obj.script_created = True + +@persistent +def spring_bone_gameend(foo): + if bpy.context.scene.sb_spring_game: + for item in bpy.context.scene.sb_spring_bones: + + active_bone = bpy.data.objects[item.armature].pose.bones.get(item.name) + if active_bone == None: + continue + + cns = active_bone.constraints.get('spring') + if cns: + active_bone.constraints.remove(cns) + + emp1 = bpy.data.objects.get(active_bone.name + '_spring') + emp2 = bpy.data.objects.get(active_bone.name + '_spring_tail') + if emp1: + bpy.data.objects.remove(emp1) + if emp2: + bpy.data.objects.remove(emp2) + + print("--End-Game-") + + +def lerp_vec(vec_a, vec_b, t): + return vec_a*t + vec_b*(1-t) + + +def spring_bone(foo): + #print("running...") + scene = bpy.context.scene + deps = bpy.context.evaluated_depsgraph_get() + + for bone in scene.sb_spring_bones: + # collider, skip + if bone.sb_bone_collider: + continue + armature = bpy.data.objects[bone.armature] + pose_bone = armature.pose.bones[bone.name] + # no influence, skip + if pose_bone.sb_global_influence == 0.0: + continue + + emp_tail = bpy.data.objects.get(bone.name + '_spring_tail') + emp_head = bpy.data.objects.get(bone.name + '_spring') + + if emp_tail == None or emp_head == None: + #print("no empties found, return") + return + + emp_tail_loc, rot, scale = emp_tail.matrix_world.decompose() + + axis_locked = None + if 'sb_lock_axis' in pose_bone.keys(): + axis_locked = pose_bone.sb_lock_axis + + # add gravity + base_pos_dir = Vector((0,0,-pose_bone.sb_gravity)) + + # add spring + base_pos_dir += (emp_tail_loc - emp_head.location) + + # evaluate bones collision + if bone.sb_bone_colliding: + + for bone_col in scene.sb_spring_bones: + if bone_col.sb_bone_collider == False: + continue + #print("collider bone", bone_col.name) + pose_bone_col = armature.pose.bones[bone_col.name] + sb_collider_dist = pose_bone_col.sb_collider_dist + #col_dir = (pose_bone.head - pose_bone_col.head) + pose_bone_center = (pose_bone.tail + pose_bone.head)*0.5 + p = project_point_onto_line(pose_bone_col.head, pose_bone_col.tail, pose_bone_center) + col_dir = (pose_bone_center - p) + dist = col_dir.magnitude + + if dist < sb_collider_dist: + push_vec = col_dir.normalized() * (sb_collider_dist-dist)*pose_bone_col.sb_collider_force + if axis_locked != "NONE" and axis_locked != None: + if axis_locked == "+Y": + direction_check = pose_bone.y_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.y_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-Y": + direction_check = pose_bone.y_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.y_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "+X": + direction_check = pose_bone.x_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.y_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-X": + direction_check = pose_bone.x_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.y_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "+Z": + direction_check = pose_bone.z_axis.normalized().dot(push_vec) + if direction_check > 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + elif axis_locked == "-Z": + direction_check = pose_bone.z_axis.normalized().dot(push_vec) + if direction_check < 0: + locked_vec = project_point_onto_plane(push_vec, pose_bone.z_axis, pose_bone.x_axis) + push_vec = lerp_vec(push_vec, locked_vec, 0.3) + + #push_vec = push_vec - pose_bone.y_axis.normalized()*0.02 + base_pos_dir += push_vec + + + + # evaluate mesh collision + if bone.sb_bone_colliding: + for mesh in scene.sb_mesh_colliders: + obj = bpy.data.objects.get(mesh.name) + pose_bone_center = (pose_bone.tail + pose_bone.head)*0.5 + col_dir = Vector((0.0,0.0,0.0)) + push_vec = Vector((0.0,0.0,0.0)) + + object_eval = obj.evaluated_get(deps) + evaluated_mesh = object_eval.to_mesh(preserve_all_data_layers=False, depsgraph=deps) + for tri in obj.data.loop_triangles: + tri_coords = [] + for vi in tri.vertices: + v_coord = evaluated_mesh.vertices[vi].co + v_coord_global = obj.matrix_world @ v_coord + tri_coords.append([v_coord_global[0], v_coord_global[1], v_coord_global[2]]) + + tri_array = numpy.array(tri_coords) + P = numpy.array([pose_bone_center[0], pose_bone_center[1], pose_bone_center[2]]) + dist, p = project_point_onto_tri(tri_array, P) + p = Vector((p[0], p[1], p[2])) + collision_dist = obj.sb_collider_dist + repel_force = obj.sb_collider_force + + if dist < collision_dist: + col_dir += (pose_bone_center - p) + push_vec = col_dir.normalized() * (collision_dist-dist) * repel_force + base_pos_dir += push_vec * pose_bone.sb_global_influence + + + # add velocity + bone.speed += base_pos_dir * pose_bone.sb_stiffness + bone.speed *= pose_bone.sb_damp + + emp_head.location += bone.speed + # global influence + emp_head.location = lerp_vec(emp_head.location, emp_tail_loc, pose_bone.sb_global_influence) + + return None + + + +def project_point_onto_plane(q, p, n): + # q = (vector) point source + # p = (vector) point belonging to the plane + # n = (vector) normal of the plane + + n = n.normalized() + return q - ((q-p).dot(n)) * n + +def project_point_onto_line(a, b, p): + # project the point p onto the line a,b + ap = p-a + ab = b-a + + fac_a = (p-a).dot(b-a) + fac_b = (p-b).dot(b-a) + + result = a + ap.dot(ab)/ab.dot(ab) * ab + + if fac_a < 0: + result = a + if fac_b > 0: + result = b + + return result + + +def project_point_onto_tri(TRI, P): + # return the distance and the projected surface point + # between a point and a triangle in 3D + # original code: https://gist.github.com/joshuashaffer/ + # Author: Gwolyn Fischer + + B = TRI[0, :] + E0 = TRI[1, :] - B + # E0 = E0/sqrt(sum(E0.^2)); %normalize vector + E1 = TRI[2, :] - B + # E1 = E1/sqrt(sum(E1.^2)); %normalize vector + D = B - P + a = dot(E0, E0) + b = dot(E0, E1) + c = dot(E1, E1) + d = dot(E0, D) + e = dot(E1, D) + f = dot(D, D) + + #print "{0} {1} {2} ".format(B,E1,E0) + det = a * c - b * b + s = b * e - c * d + t = b * d - a * e + + # Terible tree of conditionals to determine in which region of the diagram + # shown above the projection of the point into the triangle-plane lies. + if (s + t) <= det: + if s < 0.0: + if t < 0.0: + # region4 + if d < 0: + t = 0.0 + if -d >= a: + s = 1.0 + sqrdistance = a + 2.0 * d + f + else: + s = -d / a + sqrdistance = d * s + f + else: + s = 0.0 + if e >= 0.0: + t = 0.0 + sqrdistance = f + else: + if -e >= c: + t = 1.0 + sqrdistance = c + 2.0 * e + f + else: + t = -e / c + sqrdistance = e * t + f + + # of region 4 + else: + # region 3 + s = 0 + if e >= 0: + t = 0 + sqrdistance = f + else: + if -e >= c: + t = 1 + sqrdistance = c + 2.0 * e + f + else: + t = -e / c + sqrdistance = e * t + f + # of region 3 + else: + if t < 0: + # region 5 + t = 0 + if d >= 0: + s = 0 + sqrdistance = f + else: + if -d >= a: + s = 1 + sqrdistance = a + 2.0 * d + f; # GF 20101013 fixed typo d*s ->2*d + else: + s = -d / a + sqrdistance = d * s + f + else: + # region 0 + invDet = 1.0 / det + s = s * invDet + t = t * invDet + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + else: + if s < 0.0: + # region 2 + tmp0 = b + d + tmp1 = c + e + if tmp1 > tmp0: # minimum on edge s+t=1 + numer = tmp1 - tmp0 + denom = a - 2.0 * b + c + if numer >= denom: + s = 1.0 + t = 0.0 + sqrdistance = a + 2.0 * d + f; # GF 20101014 fixed typo 2*b -> 2*d + else: + s = numer / denom + t = 1 - s + sqrdistance = s * (a * s + b * t + 2 * d) + t * (b * s + c * t + 2 * e) + f + + else: # minimum on edge s=0 + s = 0.0 + if tmp1 <= 0.0: + t = 1 + sqrdistance = c + 2.0 * e + f + else: + if e >= 0.0: + t = 0.0 + sqrdistance = f + else: + t = -e / c + sqrdistance = e * t + f + # of region 2 + else: + if t < 0.0: + # region6 + tmp0 = b + e + tmp1 = a + d + if tmp1 > tmp0: + numer = tmp1 - tmp0 + denom = a - 2.0 * b + c + if numer >= denom: + t = 1.0 + s = 0 + sqrdistance = c + 2.0 * e + f + else: + t = numer / denom + s = 1 - t + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + + else: + t = 0.0 + if tmp1 <= 0.0: + s = 1 + sqrdistance = a + 2.0 * d + f + else: + if d >= 0.0: + s = 0.0 + sqrdistance = f + else: + s = -d / a + sqrdistance = d * s + f + else: + # region 1 + numer = c + e - b - d + if numer <= 0: + s = 0.0 + t = 1.0 + sqrdistance = c + 2.0 * e + f + else: + denom = a - 2.0 * b + c + if numer >= denom: + s = 1.0 + t = 0.0 + sqrdistance = a + 2.0 * d + f + else: + s = numer / denom + t = 1 - s + sqrdistance = s * (a * s + b * t + 2.0 * d) + t * (b * s + c * t + 2.0 * e) + f + + # account for numerical round-off error + if sqrdistance < 0: + sqrdistance = 0 + + dist = sqrt(sqrdistance) + + PP0 = B + s * E0 + t * E1 + return dist, PP0 + + +def update_bone(self, context): + print("Updating data...") + time_start = time.time() + scene = bpy.context.scene + if bpy.context.mode == 'POSE': + bpy.context.scene.armatr = bpy.context.active_object + elif bpy.context.scene.armatr is None: + print("Armature Object is not selected") + armature = bpy.context.scene.armatr + deps = bpy.context.evaluated_depsgraph_get() + #update collection + #delete all + if len(scene.sb_spring_bones) > 0: + i = len(scene.sb_spring_bones) + while i >= 0: + scene.sb_spring_bones.remove(i) + i -= 1 + + # mesh colliders + if len(scene.sb_mesh_colliders) > 0: + i = len(scene.sb_mesh_colliders) + while i >= 0: + scene.sb_mesh_colliders.remove(i) + i -= 1 + + if armature is not None: + for pbone in armature.pose.bones: + # are the properties there? + if len(pbone.keys()) == 0: + continue + if not 'sb_bone_spring' in pbone.keys() and not 'sb_bone_collider' in pbone.keys(): + continue + + is_spring_bone = False + is_collider_bone = False + rotation_enabled = False + is_colliding = True + + if 'sb_bone_spring' in pbone.keys(): + if pbone.get("sb_bone_spring") == False: + # remove old spring constraints + spring_cns = pbone.constraints.get("spring") + if spring_cns: + pbone.constraints.remove(spring_cns) + + else: + is_spring_bone = True + + if 'sb_bone_collider' in pbone.keys(): + is_collider_bone = pbone.get("sb_bone_collider") + + if 'sb_bone_rot' in pbone.keys(): + rotation_enabled = pbone.get("sb_bone_rot") + if 'sb_collide' in pbone.keys(): + is_colliding = pbone.get('sb_collide') + + #print("iterating on", pbone.name) + if is_spring_bone or is_collider_bone: + item = bpy.context.scene.sb_spring_bones.add() + item.name = pbone.name + print("registering", pbone.name) + bone_tail = armature.matrix_world @ pbone.tail + bone_head = armature.matrix_world @ pbone.head + item.last_loc = bone_head + item.armature = armature.name + parent_name = "" + if pbone.parent: + parent_name = pbone.parent.name + + item.sb_bone_rot = rotation_enabled + item.sb_bone_collider = is_collider_bone + item.sb_bone_colliding = is_colliding + + #create empty helpers + empty_radius = 1 + if is_spring_bone : + if not bpy.data.objects.get(item.name + '_spring'): + """ + # adding empties using operators cost too much performance + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + if rotation_enabled: + bpy.ops.object.empty_add(type='PLAIN_AXES', radius = empty_radius, location=bone_tail, rotation=(0,0,0)) + else: + bpy.ops.object.empty_add(type='PLAIN_AXES', radius = empty_radius, location=bone_head, rotation=(0,0,0)) + empty = bpy.context.active_object + empty.hide_set(True) + empty.hide_select = True + empty.name = item.name + '_spring' + """ + o = bpy.data.objects.new(item.name+'_spring', None ) + + # due to the new mechanism of "collection" + bpy.context.scene.collection.objects.link(o) + + # empty_draw was replaced by empty_display + o.empty_display_size = empty_radius + o.empty_display_type = 'PLAIN_AXES' + o.location = bone_tail if rotation_enabled else bone_head + o.hide_set(True) + o.hide_select = True + + if not bpy.data.objects.get(item.name + '_spring_tail'): + empty = bpy.data.objects.new(item.name+'_spring_tail', None ) + + # due to the new mechanism of "collection" + bpy.context.scene.collection.objects.link(empty) + + # empty_draw was replaced by empty_display + empty.empty_display_size = empty_radius + empty.empty_display_type = 'PLAIN_AXES' + #empty.location = bone_tail if rotation_enabled else bone_head + empty.matrix_world = Matrix.Translation(bone_tail if rotation_enabled else bone_head) + # >>setting the matrix instead of location attribute to avoid the despgraph update + # for performance reasons + #deps.update() + #empty.hide_set(True) + empty.hide_select = True + """ + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + if rotation_enabled: + bpy.ops.object.empty_add(type='PLAIN_AXES', radius = empty_radius, location=bone_tail, rotation=(0,0,0)) + else: + bpy.ops.object.empty_add(type='PLAIN_AXES', radius = empty_radius, location=bone_head, rotation=(0,0,0)) + empty = bpy.context.active_object + empty.hide_set(True) + empty.hide_select = True + + empty.name = item.name + '_spring_tail' + """ + mat = empty.matrix_world.copy() + empty.parent = armature + empty.parent_type = 'BONE' + empty.parent_bone = parent_name + empty.matrix_world = mat + + #create constraints + if pbone['sb_bone_spring'] == True: + #set_active_object(armature.name) + #bpy.ops.object.mode_set(mode='POSE') + spring_cns = pbone.constraints.get("spring") + if spring_cns: + pbone.constraints.remove(spring_cns) + if pbone.sb_bone_rot: + cns = pbone.constraints.new('DAMPED_TRACK') + cns.target = bpy.data.objects[item.name + '_spring'] + else: + cns = pbone.constraints.new('COPY_LOCATION') + cns.target = bpy.data.objects[item.name + '_spring'] + cns.name = 'spring' + + + # mesh colliders + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.sb_object_collider: + obj.data.calc_loop_triangles() + item = scene.sb_mesh_colliders.add() + item.name = obj.name + break + + + #set_active_object(armature.name) + #bpy.ops.object.mode_set(mode='POSE') + + print("Updated in", round(time.time()-time_start, 1), "seconds.") + +def end_spring_bone(context, self): + if context.scene.sb_global_spring: + #print("GOING TO CLOSE TIMER...") + wm = context.window_manager + wm.event_timer_remove(self.timer_handler) + #print("CLOSE TIMER") + + context.scene.sb_global_spring = False + + for item in context.scene.sb_spring_bones: + + active_bone = bpy.context.active_object.pose.bones.get(item.name) + if active_bone == None: + continue + + cns = active_bone.constraints.get('spring') + if cns: + active_bone.constraints.remove(cns) + + emp1 = bpy.data.objects.get(active_bone.name + '_spring') + emp2 = bpy.data.objects.get(active_bone.name + '_spring_tail') + if emp1: + bpy.data.objects.remove(emp1) + if emp2: + bpy.data.objects.remove(emp2) + + print("--End--end") + + +class SB_OT_spring_modal(bpy.types.Operator): + """Spring Bones, interactive mode""" + + bl_idname = "sb.spring_bone" + bl_label = "spring_bone" + + def __init__(self): + self.timer_handler = None + + def modal(self, context, event): + #print("self.timer_handler =", self.timer_handler) + if event.type == "ESC" or context.scene.sb_global_spring == False: + self.cancel(context) + #print("ESCAPE") + return {'FINISHED'} + + if event.type == 'TIMER': + spring_bone(context) + + + return {'PASS_THROUGH'} + + + def execute(self, context): + args = (self, context) + #print("self.timer_handler =", self.timer_handler) + # enable spring bone + if context.scene.sb_global_spring == False: + wm = context.window_manager + self.timer_handler = wm.event_timer_add(0.02, window=context.window) + wm.modal_handler_add(self) + print("--Start modal--") + + context.scene.sb_global_spring = True + update_bone(self, context) + + return {'RUNNING_MODAL'} + + # disable spring selection + + else: + print("--End modal--") + #self.cancel(context) + context.scene.sb_global_spring = False + return {'FINISHED'} + + + def cancel(self, context): + #if context.scene.sb_global_spring: + #print("GOING TO CLOSE TIMER...") + + wm = context.window_manager + wm.event_timer_remove(self.timer_handler) + #print("CLOSED TIMER") + + context.scene.sb_global_spring = False + + for item in context.scene.sb_spring_bones: + active_bone = bpy.context.active_object.pose.bones.get(item.name) + if active_bone == None: + continue + + cns = active_bone.constraints.get('spring') + if cns: + active_bone.constraints.remove(cns) + + emp1 = bpy.data.objects.get(active_bone.name + '_spring') + emp2 = bpy.data.objects.get(active_bone.name + '_spring_tail') + if emp1: + bpy.data.objects.remove(emp1) + if emp2: + bpy.data.objects.remove(emp2) + + print("--End-- interactive") + + +class SB_OT_spring(bpy.types.Operator): + """Spring Bones, animation mode. Support baking.""" + + bl_idname = "sb.spring_bone_frame" + bl_label = "spring_bone_frame" + + + def execute(self, context): + if context.scene.sb_global_spring_frame == False: + context.scene.sb_global_spring_frame = True + update_bone(self, context) + + else: + end_spring_bone(context, self) + context.scene.sb_global_spring_frame = False + + return {'FINISHED'} + +class SB_OT_select_bone(bpy.types.Operator): + """Select this bone""" + + bl_idname = "sb.select_bone" + bl_label = "select_bone" + + bone_name : bpy.props.StringProperty(default="") + + def execute(self, context): + data_bone = get_pose_bone(self.bone_name).bone + bpy.context.active_object.data.bones.active = data_bone + data_bone.select = True + for i, l in enumerate(data_bone.layers): + if l == True and bpy.context.active_object.data.layers[i] == False: + bpy.context.active_object.data.layers[i] = True + #print("enabled layer", i) + + + #get_pose_bone(self.bone_name).select = True + + return {'FINISHED'} + + +########### UI PANEL ################### + +class SB_PT_Game_ui(bpy.types.Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'scene' + bl_label = "Spring Bones Game" + + @classmethod + def poll(cls, context): + return (context.scene is not None) + + def draw(self, context): + col = self.layout.column(align=True) + col.label(text="Spring Bones Game Settings") + col.prop(context.scene, 'sb_spring_game', text="Use Spring in Game") + col.prop(context.scene, 'armatr', text="Armature Object") + col.prop(context.scene, 'sb_frame_tickrate', text="Run every _ frames") + +class SB_PT_ui(bpy.types.Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'bone' + bl_label = "Spring Bones" + + @classmethod + + def poll(cls, context): + return context.active_object + + def draw(self, context): + layout = self.layout + object = context.object + + scene = context.scene + col = layout.column(align=True) + + if context.mode == "POSE" and bpy.context.active_pose_bone: + active_bone = bpy.context.active_pose_bone + #col.label(text='Scene Parameters:') + col = layout.column(align=True) + #col.prop(scene, 'sb_global_spring', text="Enable spring") + if context.scene.sb_global_spring == False: + col.operator(SB_OT_spring_modal.bl_idname, text="Start - Interactive Mode", icon='PLAY') + if context.scene.sb_global_spring == True: + col.operator(SB_OT_spring_modal.bl_idname, text="Stop", icon='PAUSE') + + col.enabled = not context.scene.sb_global_spring_frame + + col = layout.column(align=True) + if context.scene.sb_global_spring_frame == False: + col.operator(SB_OT_spring.bl_idname, text="Start - Animation Mode", icon='PLAY') + if context.scene.sb_global_spring_frame == True: + col.operator(SB_OT_spring.bl_idname, text="Stop", icon='PAUSE') + + col.enabled = not context.scene.sb_global_spring + + col = layout.column(align=True) + + col.label(text='Bone Parameters:') + col.prop(active_bone, 'sb_bone_spring', text="Spring") + col.prop(active_bone, 'sb_bone_rot', text="Rotation") + col.prop(active_bone, 'sb_stiffness', text="Bouncy") + col.prop(active_bone,'sb_damp', text="Speed") + col.prop(active_bone,'sb_gravity', text="Gravity") + col.prop(active_bone,'sb_global_influence', text="Influence") + col.prop(active_bone,'sb_collide', text="Is Colliding") + col.label(text="Lock axis when colliding:") + col.prop(active_bone, 'sb_lock_axis', text="") + col.enabled = not active_bone.sb_bone_collider + + layout.separator() + col = layout.column(align=True) + col.prop(active_bone, 'sb_bone_collider', text="Collider") + col.prop(active_bone, 'sb_collider_dist', text="Collider Distance") + col.prop(active_bone, 'sb_collider_force', text="Collider Force") + col.enabled = not active_bone.sb_bone_spring + + layout.separator() + layout.prop(scene, "sb_show_colliders") + col = layout.column(align=True) + + if scene.sb_show_colliders: + for pbone in bpy.context.active_object.pose.bones: + if "sb_bone_collider" in pbone.keys(): + if pbone.sb_bone_collider: + row = col.row() + row.label(text=pbone.name) + r = row.operator(SB_OT_select_bone.bl_idname, text="Select") + r.bone_name = pbone.name + + + +class SB_PT_object_ui(bpy.types.Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'object' + bl_label = "Spring Bones" + + @classmethod + + def poll(cls, context): + if context.active_object: + return context.active_object.type == "MESH" + + def draw(self, context): + layout = self.layout + object = context.active_object + + scene = context.scene + col = layout.column(align=True) + + if context.mode == "OBJECT" and context.active_object: + + col = layout.column(align=True) + col.prop(context.active_object, 'sb_object_collider', text="Collider") + col.prop(context.active_object, 'sb_collider_dist', text="Collider Distance") + col.prop(context.active_object, 'sb_collider_force', text="Collider Force") + +#### REGISTER ############# + +class bones_collec(bpy.types.PropertyGroup): + armature : bpy.props.StringProperty(default="") + last_loc : bpy.props.FloatVectorProperty(name="Loc", subtype='DIRECTION', default=(0,0,0), size = 3) + speed : bpy.props.FloatVectorProperty(name="Speed", subtype='DIRECTION', default=(0,0,0), size = 3) + dist: bpy.props.FloatProperty(name="distance", default=1.0) + target_offset : bpy.props.FloatVectorProperty(name="TargetLoc", subtype='DIRECTION', default=(0,0,0), size = 3) + sb_bone_rot : bpy.props.BoolProperty(name="Bone Rot", default=False) + sb_bone_collider: bpy.props.BoolProperty(name="Bone collider", default=False) + sb_bone_colliding: bpy.props.BoolProperty(name="Bone colliding", default=True) + sb_collider_dist : bpy.props.FloatProperty(name="Bone collider distance", default=0.5) + sb_collider_force : bpy.props.FloatProperty(name="Bone collider force", default=1.0) + matrix_offset = Matrix() + initial_matrix = Matrix() + +class mesh_collec(bpy.types.PropertyGroup): + test : bpy.props.StringProperty(default="") + + +classes = (SB_PT_Game_ui, SB_PT_ui, SB_PT_object_ui, bones_collec, mesh_collec, SB_OT_spring_modal, SB_OT_spring, SB_OT_select_bone) + +def register(): + from bpy.utils import register_class + + for cls in classes: + register_class(cls) + + bpy.app.handlers.frame_change_post.append(spring_bone_frame_mode) + bpy.app.handlers.game_pre.append(spring_bone_gamestart) + bpy.app.handlers.game_post.append(spring_bone_gameend) + bpy.types.Scene.sb_spring_bones = bpy.props.CollectionProperty(type=bones_collec) + bpy.types.Scene.sb_mesh_colliders = bpy.props.CollectionProperty(type=mesh_collec) + bpy.types.Scene.sb_global_spring = bpy.props.BoolProperty(name="Enable spring", default = False)#, update=update_global_spring) + bpy.types.Scene.armatr = bpy.props.PointerProperty(name="Armature",type=bpy.types.Object) + bpy.types.Scene.sb_global_spring_frame = bpy.props.BoolProperty(name="Enable Spring", description="Enable Spring on frame change only", default = False) + bpy.types.Scene.sb_spring_game = bpy.props.BoolProperty(name="Enable Spring in Game", description="Enable Spring in the game engine", default = True) + bpy.types.Scene.sb_frame_tickrate = bpy.props.IntProperty(name="Spring tick rate", description='Run Spring every x frames', default=1, min=1, max=20) + bpy.types.Scene.sb_lastexec = bpy.props.IntProperty(default=1) + bpy.types.Scene.sb_show_colliders = bpy.props.BoolProperty(name="Show Colliders", description="Show active colliders names", default = False) + bpy.types.PoseBone.sb_bone_spring = bpy.props.BoolProperty(name="Enabled", default=False, description="Enable spring effect on this bone") + bpy.types.PoseBone.sb_bone_collider = bpy.props.BoolProperty(name="Collider", default=False, description="Enable this bone as collider") + bpy.types.PoseBone.sb_collider_dist = bpy.props.FloatProperty(name="Collider Distance", default=0.5, description="Minimum distance to handle collision between the spring and collider bones") + bpy.types.PoseBone.sb_collider_force = bpy.props.FloatProperty(name="Collider Force", default=1.0, description="Amount of repulsion force when colliding") + bpy.types.PoseBone.sb_stiffness = bpy.props.FloatProperty(name="Stiffness", default=0.5, min = 0.01, max = 1.0, description="Bouncy/elasticity value, higher values lead to more bounciness") + bpy.types.PoseBone.sb_damp = bpy.props.FloatProperty(name="Damp", default=0.7, min=0.0, max = 10.0, description="Speed/damping force applied to the bone to go back to it initial position") + bpy.types.PoseBone.sb_gravity = bpy.props.FloatProperty(name="Gravity", description="Additional vertical force to simulate gravity", default=0.0, min=-100.0, max = 100.0) + bpy.types.PoseBone.sb_bone_rot = bpy.props.BoolProperty(name="Rotation", default=False, description="The spring effect will apply on the bone rotation instead of location") + bpy.types.PoseBone.sb_lock_axis = bpy.props.EnumProperty(items=(('NONE', 'None', ""), ('+X', '+X', ''), ('-X', '-X', ''), ('+Y', "+Y", ""), ('-Y', '-Y', ""), ('+Z', '+Z', ""), ('-Z', '-Z', '')), default="NONE") + bpy.types.Object.script_created = bpy.props.BoolProperty(default=False) + bpy.types.Object.sb_object_collider = bpy.props.BoolProperty(name="Collider", default=False, description="Enable this bone as collider") + bpy.types.Object.sb_collider_dist = bpy.props.FloatProperty(name="Collider Distance", default=0.5, description="Minimum distance to handle collision between the spring and collider bones") + bpy.types.Object.sb_collider_force = bpy.props.FloatProperty(name="Collider Force", default=1.0, description="Amount of repulsion force when colliding") + bpy.types.PoseBone.sb_collide = bpy.props.BoolProperty(name="Colliding", default = True, description="The bone will collide with other colliders")#, update=update_global_spring) + bpy.types.PoseBone.sb_global_influence = bpy.props.FloatProperty(name="Influence", default = 1.0, min=0.0, max=1.0, description="Global influence of spring motion")#, update=update_global_spring) + + +def unregister(): + from bpy.utils import unregister_class + + for cls in reversed(classes): + unregister_class(cls) + + bpy.app.handlers.frame_change_post.remove(spring_bone_frame_mode) + bpy.app.handlers.game_pre.remove(spring_bone_gamestart) + bpy.app.handlers.game_post.remove(spring_bone_gameend) + + del bpy.types.Scene.sb_spring_bones + del bpy.types.Scene.sb_mesh_colliders + del bpy.types.Scene.sb_global_spring + del bpy.types.Scene.sb_global_spring_frame + del bpy.types.Scene.sb_spring_game + del bpy.types.Scene.armatr + del bpy.types.Scene.sb_frame_tickrate + del bpy.types.Scene.sb_lastexec + del bpy.types.Scene.sb_show_colliders + del bpy.types.PoseBone.sb_bone_spring + del bpy.types.PoseBone.sb_bone_collider + del bpy.types.PoseBone.sb_collider_dist + del bpy.types.PoseBone.sb_collider_force + del bpy.types.PoseBone.sb_stiffness + del bpy.types.PoseBone.sb_damp + del bpy.types.PoseBone.sb_gravity + del bpy.types.PoseBone.sb_bone_rot + del bpy.types.PoseBone.sb_lock_axis + del bpy.types.Object.script_created + del bpy.types.Object.sb_object_collider + del bpy.types.Object.sb_collider_dist + del bpy.types.Object.sb_collider_force + del bpy.types.PoseBone.sb_collide + del bpy.types.PoseBone.sb_global_influence + + +if __name__ == "__main__": + register() + diff --git a/scripts/addons_core/normal_map_to_group.py b/scripts/addons_core/normal_map_to_group.py new file mode 100644 index 00000000000..f469e73dfa7 --- /dev/null +++ b/scripts/addons_core/normal_map_to_group.py @@ -0,0 +1,464 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Normal Map nodes to Custom", + "author": "Spooky spooky Ghostman, Kamikaze, crute, Mustard", + "description": "Replace Normal Nodes for better EEVEE Viewport-Performance", + "blender": (4, 00, 0), + "version": (0, 2, 0), + "location": "Tools Panel (T) in Shader Editor", + "warning": "", + "category": "Material", +} + + + +from mathutils import Color, Vector +import bpy + + + + +class MAT_OT_custom_normal(bpy.types.Operator): + bl_description = "Switch normal map nodes to a faster custom node" + bl_idname = 'node.normal_map_group' + bl_label = "Normal Map nodes to Custom" + bl_options = {'UNDO'} + + @classmethod + def poll(self, context): + return (bpy.data.materials or bpy.data.node_groups) + + def execute(self, context): + def mirror(new, old): + """Copy attributes of the old node to the new node""" + new.parent = old.parent + new.label = old.label + new.mute = old.mute + new.hide = old.hide + new.select = old.select + new.location = old.location + + # inputs + for (name, point) in old.inputs.items(): + input = new.inputs.get(name) + if input: + input.default_value = point.default_value + for link in point.links: + new.id_data.links.new(link.from_socket, input) + + # outputs + for (name, point) in old.outputs.items(): + output = new.outputs.get(name) + if output: + output.default_value = point.default_value + for link in point.links: + new.id_data.links.new(output, link.to_socket) + + def get_custom(): + name = 'Normal Map Optimized' + group = bpy.data.node_groups.get(name) + + if not group and self.custom: + group = default_custom_nodes() + + return group + + def set_custom(nodes): + group = get_custom() + if not group: + return + + for node in reversed(nodes): + new = None + if self.custom: + if isinstance(node, bpy.types.ShaderNodeNormalMap): + new = nodes.new(type='ShaderNodeGroup') + new.node_tree = group + else: + if isinstance(node, bpy.types.ShaderNodeGroup): + if node.node_tree == group: + new = nodes.new(type='ShaderNodeNormalMap') + + if new: + name = node.name + mirror(new, node) + + if isinstance(node, bpy.types.ShaderNodeNormalMap): + uvNode = nodes.new('ShaderNodeUVMap') + uvNode.uv_map = node.uv_map + uvNode.name = node.name+" UV" + uvNode.parent = new.parent + uvNode.mute = True + uvNode.hide = True + uvNode.select = False + uvNode.location = Vector((new.location.x-200., new.location.y-10.)) + uvNode.id_data.links.new(uvNode.outputs['UV'], new.inputs[2]) + else: + try: + for input in node.inputs: + if input and isinstance(input, bpy.types.NodeSocketVector) and input.is_linked: + if isinstance(input.links[0].from_node, bpy.types.ShaderNodeUVMap): + uvNode = input.links[0].from_node + new.uv_map = uvNode.uv_map + nodes.remove(uvNode) + except: + pass + + nodes.remove(node) + new.name = name + + for mat in bpy.data.materials: + set_custom(getattr(mat.node_tree, 'nodes', [])) + for group in bpy.data.node_groups: + set_custom(group.nodes) + + if (not self.custom) and get_custom(): + bpy.data.node_groups.remove(get_custom()) + + return {'FINISHED'} + + custom: bpy.props.BoolProperty( + name="To Custom", + description="Set all normals to custom group, or revert back to normal", + default=True, + ) + + +class MUT_PT_normal_map_nodes(bpy.types.Panel): + bl_category = "" + bl_label = "" + bl_options = {'HIDE_HEADER'} + bl_region_type = 'TOOLS' + bl_space_type = 'NODE_EDITOR' + + @classmethod + def poll(self, context): + return True + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True) + tog = MAT_OT_custom_normal.bl_idname + col.operator(tog, text="Custom").custom = True + col.operator(tog, text="Normal").custom = False + + +def default_custom_nodes(): + use_new_nodes = bpy.app.version >= (2, 81) and bpy.app.version < (3, 2, 0) + + group = bpy.data.node_groups.new('Normal Map Optimized', 'ShaderNodeTree') + + nodes = group.nodes + links = group.links + + # Input + input = group.interface.new_socket("Strength", in_out='INPUT', socket_type='NodeSocketFloat') + input.default_value = 1.0 + input.min_value = 0.0 + input.max_value = 1.0 + input = group.interface.new_socket("Color", in_out='INPUT', socket_type='NodeSocketColor') + input.default_value = ((0.5, 0.5, 1.0, 1.0)) + + # Input UV as Backup + input = group.interface.new_socket("UV", in_out='INPUT', socket_type='NodeSocketVector') + + # Output + group.interface.new_socket("Normal", in_out='OUTPUT', socket_type='NodeSocketVector') + + # Add Nodes + frame = nodes.new('NodeFrame') + frame.name = 'Matrix * Normal Map' + frame.label = 'Matrix * Normal Map' + frame.location = Vector((540.0, -80.0)) + frame.hide = False + frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node = nodes.new('ShaderNodeVectorMath') + node.name = 'Vector Math' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-60.0, 20.0)) + node.operation = 'DOT_PRODUCT' + node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector + node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector + if use_new_nodes: + node.inputs[2].default_value = (1.0, 1.0, 1.0) # Scale + node = nodes.new('ShaderNodeVectorMath') + node.name = 'Vector Math.001' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-60.0, -20.0)) + node.operation = 'DOT_PRODUCT' + node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector + node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector + if use_new_nodes: + node.inputs[2].default_value = (1.0, 1.0, 1.0) # Scale + node = nodes.new('ShaderNodeVectorMath') + node.name = 'Vector Math.002' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-60.0, -60.0)) + node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector + node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector + if use_new_nodes: + node.inputs[2].default_value = (1.0, 1.0, 1.0) # Scale + node.operation = 'DOT_PRODUCT' + node = nodes.new('ShaderNodeCombineXYZ') + node.name = 'Combine XYZ' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((100.0, -20.0)) + node.inputs[0].default_value = 0.0 # X + node.inputs[1].default_value = 0.0 # Y + node.inputs[2].default_value = 0.0 # Z + + frame = nodes.new('NodeFrame') + frame.name = 'Generate TBN from Bump Node' + frame.label = 'Generate TBN from Bump Node' + frame.location = Vector((-192.01412963867188, -77.50459289550781)) + frame.hide = False + frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node = nodes.new('ShaderNodeUVMap') + node.name = 'UV Map' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-247.98587036132812, -2.4954071044921875)) + node = nodes.new('ShaderNodeSeparateXYZ') + node.name = 'UV Gradients' + node.label = 'UV Gradients' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-87.98587036132812, -2.4954071044921875)) + node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector + # node.outputs.remove((node.outputs['Z'])) + node = nodes.new('ShaderNodeNewGeometry') + node.name = 'Normal' + node.label = 'Normal' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((72.01412963867188, -62.49540710449219)) + # for out in node.outputs: + # if out.name not in ['Normal']: + # node.outputs.remove(out) + node = nodes.new('ShaderNodeBump') + node.name = 'Bi-Tangent' + node.label = 'Bi-Tangent' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((72.01412963867188, -22.495407104492188)) + node.invert = True + node.inputs[0].default_value = 1.0 # Strength + node.inputs[1].default_value = 1000.0 # Distance + node.inputs[2].default_value = 1.0 # Height + if use_new_nodes: + node.inputs[3].default_value = 1.0 # Height_dx + node.inputs[4].default_value = 1.0 # Height_dy + node.inputs[5].default_value = (0.0, 0.0, 0.0) # Normal + else: + node.inputs[3].default_value = (0.0, 0.0, 0.0) # Normal + # for inp in node.inputs: + # if inp.name not in ['Height']: + # node.inputs.remove(inp) + node = nodes.new('ShaderNodeBump') + node.name = 'Tangent' + node.label = 'Tangent' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((72.01412963867188, 17.504592895507812)) + node.invert = True + # for inp in node.inputs: + # if inp.name not in ['Height']: + # node.inputs.remove(inp) + + frame = nodes.new('NodeFrame') + frame.name = 'Node' + frame.label = 'Normal Map Processing' + frame.location = Vector((180.0, -260.0)) + frame.hide = False + frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node = nodes.new('NodeGroupInput') + node.name = 'Group Input' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-400.0, 20.0)) + node = nodes.new('ShaderNodeMixRGB') + node.name = 'Influence' + node.label = '' + node.parent = frame + node.hide = True + node.location = Vector((-240.0, 20.0)) + node.inputs[1].default_value = (0.5, 0.5, 1.0, 1.0) # Color1 + node = nodes.new('ShaderNodeVectorMath') + node.name = 'Vector Math.003' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-80.0, 20.0)) + node.operation = 'SUBTRACT' + node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector + node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector + if use_new_nodes: + node.inputs[2].default_value = (1.0, 1.0, 1.0) # Scale + # node.inputs.remove(node.inputs[1]) + node = nodes.new('ShaderNodeVectorMath') + node.name = 'Vector Math.004' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((80.0, 20.0)) + node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector + node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector + if use_new_nodes: + node.inputs[2].default_value = (1.0, 1.0, 1.0) # Scale + + frame = nodes.new('NodeFrame') + frame.name = 'Transpose Matrix' + frame.label = 'Transpose Matrix' + frame.location = Vector((180.0, -80.0)) + frame.hide = False + frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node = nodes.new('ShaderNodeCombineXYZ') + node.name = 'Combine XYZ.001' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((80.0, 20.0)) + node.inputs[0].default_value = 0.0 # X + node.inputs[1].default_value = 0.0 # Y + node.inputs[2].default_value = 0.0 # Z + node = nodes.new('ShaderNodeCombineXYZ') + node.name = 'Combine XYZ.002' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((80.0, -20.0)) + node.inputs[0].default_value = 0.0 # X + node.inputs[1].default_value = 0.0 # Y + node.inputs[2].default_value = 0.0 # Z + node = nodes.new('ShaderNodeCombineXYZ') + node.name = 'Combine XYZ.003' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((80.0, -60.0)) + node.inputs[0].default_value = 0.0 # X + node.inputs[1].default_value = 0.0 # Y + node.inputs[2].default_value = 0.0 # Z + node = nodes.new('ShaderNodeSeparateXYZ') + node.name = 'Separate XYZ.001' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-80.0, 20.0)) + node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector + node = nodes.new('ShaderNodeSeparateXYZ') + node.name = 'Separate XYZ.002' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-80.0, -20.0)) + node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector + node = nodes.new('ShaderNodeSeparateXYZ') + node.name = 'Separate XYZ.003' + node.label = '' + node.parent = frame + node.hide = True + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.location = Vector((-80.0, -60.0)) + node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector + + node = nodes.new('NodeGroupOutput') + node.name = 'Group Output' + node.label = '' + node.location = Vector((840.0, -80.0)) + node.hide = False + node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765)) + node.inputs[0].default_value = (0.0, 0.0, 0.0) # Normal + + # Connect the nodes + links.new(nodes['Group Input'].outputs['Strength'], nodes['Influence'].inputs[0]) + links.new(nodes['Group Input'].outputs['Color'], nodes['Influence'].inputs[2]) + links.new(nodes['Influence'].outputs['Color'], nodes['Vector Math.003'].inputs[0]) + links.new(nodes['UV Gradients'].outputs['X'], nodes['Tangent'].inputs['Height']) + links.new(nodes['UV Gradients'].outputs['Y'], nodes['Bi-Tangent'].inputs['Height']) + links.new(nodes['UV Map'].outputs['UV'], nodes['UV Gradients'].inputs['Vector']) + links.new(nodes['Tangent'].outputs['Normal'], nodes['Separate XYZ.001'].inputs[0]) + links.new(nodes['Bi-Tangent'].outputs['Normal'], nodes['Separate XYZ.002'].inputs[0]) + links.new(nodes['Normal'].outputs['Normal'], nodes['Separate XYZ.003'].inputs[0]) + links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math'].inputs[1]) + links.new(nodes['Combine XYZ.001'].outputs['Vector'], nodes['Vector Math'].inputs[0]) + links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math.001'].inputs[1]) + links.new(nodes['Combine XYZ.002'].outputs['Vector'], nodes['Vector Math.001'].inputs[0]) + links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math.002'].inputs[1]) + links.new(nodes['Combine XYZ.003'].outputs['Vector'], nodes['Vector Math.002'].inputs[0]) + links.new(nodes['Vector Math.003'].outputs['Vector'], nodes['Vector Math.004'].inputs[0]) + links.new(nodes['Vector Math.003'].outputs['Vector'], nodes['Vector Math.004'].inputs[1]) + links.new(nodes['Vector Math'].outputs['Value'], nodes['Combine XYZ'].inputs['X']) + links.new(nodes['Vector Math.001'].outputs['Value'], nodes['Combine XYZ'].inputs['Y']) + links.new(nodes['Vector Math.002'].outputs['Value'], nodes['Combine XYZ'].inputs['Z']) + links.new(nodes['Separate XYZ.001'].outputs['X'], nodes['Combine XYZ.001'].inputs['X']) + links.new(nodes['Separate XYZ.002'].outputs['X'], nodes['Combine XYZ.001'].inputs['Y']) + links.new(nodes['Separate XYZ.003'].outputs['X'], nodes['Combine XYZ.001'].inputs['Z']) + links.new(nodes['Separate XYZ.001'].outputs['Y'], nodes['Combine XYZ.002'].inputs['X']) + links.new(nodes['Separate XYZ.002'].outputs['Y'], nodes['Combine XYZ.002'].inputs['Y']) + links.new(nodes['Separate XYZ.003'].outputs['Y'], nodes['Combine XYZ.002'].inputs['Z']) + links.new(nodes['Separate XYZ.001'].outputs['Z'], nodes['Combine XYZ.003'].inputs['X']) + links.new(nodes['Separate XYZ.002'].outputs['Z'], nodes['Combine XYZ.003'].inputs['Y']) + links.new(nodes['Separate XYZ.003'].outputs['Z'], nodes['Combine XYZ.003'].inputs['Z']) + links.new(nodes['Combine XYZ'].outputs['Vector'], nodes['Group Output'].inputs['Normal']) + + return group + + +def register(): + bpy.utils.register_class(MAT_OT_custom_normal) + bpy.utils.register_class(MUT_PT_normal_map_nodes) + + +def unregister(): + bpy.utils.unregister_class(MAT_OT_custom_normal) + bpy.utils.unregister_class(MUT_PT_normal_map_nodes) + + +if __name__ == "__main__": + register()