From 57cd481731e5d81a3834301501d8455075b588c1 Mon Sep 17 00:00:00 2001 From: Forest Sharp Date: Sun, 19 Jan 2020 18:49:30 -0600 Subject: [PATCH 001/230] lights wouldn't render at all if use_shadow / cast_shadow was disabled --- Sources/armory/renderpath/Inc.hx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/armory/renderpath/Inc.hx b/Sources/armory/renderpath/Inc.hx index 731edd3e..9c57929a 100644 --- a/Sources/armory/renderpath/Inc.hx +++ b/Sources/armory/renderpath/Inc.hx @@ -41,7 +41,7 @@ class Inc { public static function bindShadowMap() { for (l in iron.Scene.active.lights) { - if (!l.visible || !l.data.raw.cast_shadow || l.data.raw.type != "sun") continue; + if (!l.visible || l.data.raw.type != "sun") continue; var n = "shadowMap"; path.bindTarget(n, n); break; @@ -109,7 +109,8 @@ class Inc { pointIndex = 0; spotIndex = 0; for (l in iron.Scene.active.lights) { - if (!l.visible || !l.data.raw.cast_shadow) continue; + if (!l.visible) continue; + path.light = l; var shadowmap = Inc.getShadowMap(l); var faces = l.data.raw.shadowmap_cube ? 6 : 1; @@ -117,7 +118,9 @@ class Inc { if (faces > 1) path.currentFace = i; path.setTarget(shadowmap); path.clearTarget(null, 1.0); - path.drawMeshes("shadowmap"); + if (l.data.raw.cast_shadow) { + path.drawMeshes("shadowmap"); + } } path.currentFace = -1; From 2d282c9582e20b08b064371abba9e7a0a3e5c699 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 8 Mar 2020 21:34:35 +0100 Subject: [PATCH 002/230] Fixed Bullet RigidBody memory leak --- .../armory/trait/physics/bullet/RigidBody.hx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/RigidBody.hx b/Sources/armory/trait/physics/bullet/RigidBody.hx index cfaf0923..63fec4c5 100644 --- a/Sources/armory/trait/physics/bullet/RigidBody.hx +++ b/Sources/armory/trait/physics/bullet/RigidBody.hx @@ -41,6 +41,7 @@ class RigidBody extends iron.Trait { var currentScaleX: Float; var currentScaleY: Float; var currentScaleZ: Float; + var meshInterface: bullet.Bt.TriangleMesh; public var body: bullet.Bt.RigidBody = null; public var motionState: bullet.Bt.MotionState; @@ -95,7 +96,7 @@ class RigidBody extends iron.Trait { this.mask = mask; if (params == null) params = [0.04, 0.1, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]; - if (flags == null) flags = [false, false, false]; + if (flags == null) flags = [false, false, false, false]; this.linearDamping = params[0]; this.angularDamping = params[1]; @@ -166,7 +167,7 @@ class RigidBody extends iron.Trait { btshape = caps; } else if (shape == Shape.Mesh) { - var meshInterface = fillTriangleMesh(transform.scale); + meshInterface = fillTriangleMesh(transform.scale); if (mass > 0) { var shapeGImpact = new bullet.Bt.GImpactMeshShape(meshInterface); shapeGImpact.updateBound(); @@ -564,11 +565,18 @@ class RigidBody extends iron.Trait { var data = cast(object, MeshObject).data; var i = usersCache.get(data) - 1; usersCache.set(data, i); + if(shape == Shape.Mesh) deleteShape(); if (i <= 0) { - deleteShape(); - shape == Shape.ConvexHull ? - convexHullCache.remove(data) : + if(shape == Shape.ConvexHull) + { + deleteShape(); + convexHullCache.remove(data); + } + else + { triangleMeshCache.remove(data); + if(meshInterface != null) bullet.Bt.Ammo.destroy(meshInterface); + } } } else deleteShape(); From e40ae5ac4ed730ff495b98d0f326d6f3e89c9df5 Mon Sep 17 00:00:00 2001 From: N8n5h Date: Thu, 12 Mar 2020 20:02:37 -0300 Subject: [PATCH 003/230] Fix geometry in published projects Updated the opt_exporter to fit new vertex format --- blender/arm/exporter_opt.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index 0564112d..79486f97 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -171,7 +171,7 @@ def export_mesh_data(self, exportMesh, bobject, o, has_armature=False): o['scale_pos'] = 1.0 if has_armature: # Allow up to 2x bigger bounds for skinned mesh o['scale_pos'] *= 2.0 - + scale_pos = o['scale_pos'] invscale_pos = (1 / scale_pos) * 32767 @@ -265,16 +265,16 @@ def export_mesh_data(self, exportMesh, bobject, o, has_armature=False): # Output o['vertex_arrays'] = [] - o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata }) - o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata }) + o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' }) + o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' }) if has_tex: - o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data }) + o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data, 'data': 'short2norm' }) if has_tex1: - o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data }) + o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data, 'data': 'short2norm' }) if has_col: - o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata }) + o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata, 'data': 'short4norm', 'padding': 1 }) if has_tang: - o['vertex_arrays'].append({ 'attrib': 'tang', 'values': tangdata }) + o['vertex_arrays'].append({ 'attrib': 'tang', 'values': tangdata, 'data': 'short4norm', 'padding': 1 }) return vert_list @@ -348,7 +348,7 @@ def export_skin(self, bobject, armature, vert_list, o): bone_count = 4 bone_values.sort(reverse=True) bone_values = bone_values[:4] - + bone_count_array[index] = bone_count for bv in bone_values: bone_weight_array[count] = bv[0] * 32767 From d1765ab2068601ac8134f18e85c46edbbfeb221d Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Fri, 13 Mar 2020 16:28:55 +0100 Subject: [PATCH 004/230] Update cycles.py --- blender/arm/material/cycles.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 4c4fedd0..2f65e036 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -529,16 +529,15 @@ def parse_vector(node, socket): assets_add(get_sdk_path() + '/armory/Assets/' + 'noise256.png') assets_add_embedded_data('noise256.png') curshader.add_uniform('sampler2D snoise256', link='$noise256.png') - curshader.add_function(c_functions.str_tex_noise) if node.inputs[0].is_linked: co = parse_vector_input(node.inputs[0]) else: co = 'bposition' - scale = parse_value_input(node.inputs[1]) + scale = parse_value_input(node.inputs[2]) # detail = parse_value_input(node.inputs[2]) # distortion = parse_value_input(node.inputs[3]) # Slow.. - res = 'vec3(tex_noise({0} * {1}), tex_noise({0} * {1} + 0.33), tex_noise({0} * {1} + 0.66))'.format(co, scale) + res = 'vec3(tex_noise({0} * {1}), tex_noise({0} * {1} + 5.0), tex_noise({0} * {1} + 8.0))'.format(co, scale) if sample_bump: write_bump(node, res, 0.1) return res From 3aaba5fe62257aecb47a2aeeb7b93afe4ac8e514 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Fri, 13 Mar 2020 16:30:42 +0100 Subject: [PATCH 005/230] Don't use the Z coordinate in the shader --- blender/arm/material/cycles_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index cfc74122..2db3c549 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -68,7 +68,8 @@ float tex_noise_f(vec3 x) { mix(mix(hash(n + dot(step, vec3(0, 0, 1))), hash(n + dot(step, vec3(1, 0, 1))), u.x), mix(hash(n + dot(step, vec3(0, 1, 1))), hash(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z); } -float tex_noise(vec3 p) { +float tex_noise(vec3 k) { + vec3 p = vec3(k.xy, 0); p *= 1.25; float f = 0.5 * tex_noise_f(p); p *= 2.01; f += 0.25 * tex_noise_f(p); p *= 2.02; From d1a0fc69c8db13ad657c307f5118ae56ffd61a1c Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Sat, 14 Mar 2020 20:58:56 +0900 Subject: [PATCH 006/230] Addressing issues reported in the Armory forum https://forums.armory3d.org/t/something-wrong-with-translate-on-local-axis/2772 https://forums.armory3d.org/t/how-to-avoid-camera-going-through-rigid-bodies/3945 --- .../armory/logicnode/TranslateObjectNode.hx | 9 +++----- .../logicnode/TranslateOnLocalAxisNode.hx | 21 ++++--------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/Sources/armory/logicnode/TranslateObjectNode.hx b/Sources/armory/logicnode/TranslateObjectNode.hx index d5d8c576..b3ede564 100644 --- a/Sources/armory/logicnode/TranslateObjectNode.hx +++ b/Sources/armory/logicnode/TranslateObjectNode.hx @@ -22,12 +22,9 @@ class TranslateObjectNode extends LogicNode { object.transform.buildMatrix(); } else { - var look = object.transform.world.look().mult(vec.y); - var right = object.transform.world.right().mult(vec.x); - var up = object.transform.world.up().mult(vec.z); - object.transform.loc.add(look); - object.transform.loc.add(right); - object.transform.loc.add(up); + object.transform.move(object.transform.local.look(),vec.y); + object.transform.move(object.transform.local.up(),vec.z); + object.transform.move(object.transform.local.right(),vec.x); object.transform.buildMatrix(); } diff --git a/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx b/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx index 2182affb..4906fce1 100644 --- a/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx +++ b/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx @@ -1,14 +1,10 @@ package armory.logicnode; import iron.object.Object; -import iron.math.Vec4; import armory.trait.physics.RigidBody; class TranslateOnLocalAxisNode extends LogicNode { - var loc = new Vec4(); - var vec = new Vec4(); - public function new(tree: LogicTree) { super(tree); } @@ -21,21 +17,12 @@ class TranslateOnLocalAxisNode extends LogicNode { if (object == null) return; - if (l == 1) loc.setFrom(object.transform.world.look()); - else if (l == 2) loc.setFrom(object.transform.world.up()); - else if (l == 3) loc.setFrom(object.transform.world.right()); + if (ini) sp *= -1; - if (ini) { - loc.x = -loc.x; - loc.y = -loc.y; - loc.z = -loc.z; - } + if (l == 1) object.transform.move(object.transform.local.look(),sp); + else if (l == 2) object.transform.move(object.transform.local.up(),sp); + else if (l == 3) object.transform.move(object.transform.local.right(),sp); - vec.x = loc.x * sp; - vec.y = loc.y * sp; - vec.z = loc.z * sp; - - object.transform.loc.add(vec); object.transform.buildMatrix(); #if arm_physics From 3f77ff397dbad6f202703310243e484048610f3d Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Sat, 14 Mar 2020 21:03:07 +0900 Subject: [PATCH 007/230] Add new Input of "Get Distance" is Object, but input of "Distance" is Vector. --- Sources/armory/logicnode/DistanceNode.hx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Sources/armory/logicnode/DistanceNode.hx diff --git a/Sources/armory/logicnode/DistanceNode.hx b/Sources/armory/logicnode/DistanceNode.hx new file mode 100644 index 00000000..b7f68a70 --- /dev/null +++ b/Sources/armory/logicnode/DistanceNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class DistanceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vector1: Vec4 = inputs[0].get(); + var vector2: Vec4 = inputs[1].get(); + + if (vector1 == null || vector2 == null) return 0; + + return iron.math.Vec4.distance(vector1, vector2); + } +} From 30d22a264b74ab3287563984cb5d9ee4156d67d6 Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Sat, 14 Mar 2020 21:04:51 +0900 Subject: [PATCH 008/230] Add new Input of "Get Distance" is Object, but input of "Distance" is Vector. --- blender/arm/logicnode/value_distance.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 blender/arm/logicnode/value_distance.py diff --git a/blender/arm/logicnode/value_distance.py b/blender/arm/logicnode/value_distance.py new file mode 100644 index 00000000..9a08db2c --- /dev/null +++ b/blender/arm/logicnode/value_distance.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class DistanceNode(Node, ArmLogicTreeNode): + '''Distance node''' + bl_idname = 'LNDistanceNode' + bl_label = 'Distance' + bl_icon = 'QUESTION' + + def init(self, context): + self.inputs.new('NodeSocketVector', 'Vector') + self.inputs.new('NodeSocketVector', 'Vector') + self.outputs.new('NodeSocketFloat', 'Distance') + +add_node(DistanceNode, category='Value') From 1770aeb00271b06d06239a1a39cf3d916fee67ec Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 14 Mar 2020 23:30:09 +0100 Subject: [PATCH 009/230] Added nodes to start animation from a partivular frame index --- .../armory/logicnode/PlayActionFromNode.hx | 30 +++++++++++++++++++ .../logicnode/animation_play_action_from.py | 22 ++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 Sources/armory/logicnode/PlayActionFromNode.hx create mode 100644 blender/arm/logicnode/animation_play_action_from.py diff --git a/Sources/armory/logicnode/PlayActionFromNode.hx b/Sources/armory/logicnode/PlayActionFromNode.hx new file mode 100644 index 00000000..581caf13 --- /dev/null +++ b/Sources/armory/logicnode/PlayActionFromNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.Scene; + +class PlayActionFromNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var action: String = inputs[2].get(); + var startFrame:Int = inputs[3].get(); + var blendTime: Float = inputs[4].get(); + + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.play(action, function() { + runOutput(1); + },blendTime); + animation.update(startFrame*Scene.active.raw.frame_time); + + runOutput(0); + } +} diff --git a/blender/arm/logicnode/animation_play_action_from.py b/blender/arm/logicnode/animation_play_action_from.py new file mode 100644 index 00000000..6d55b6c1 --- /dev/null +++ b/blender/arm/logicnode/animation_play_action_from.py @@ -0,0 +1,22 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class PlayActionFromNode(Node, ArmLogicTreeNode): + '''Play action from node''' + bl_idname = 'LNPlayActionFromNode' + bl_label = 'Play Action From' + bl_icon = 'QUESTION' + + def init(self, context): + self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('ArmNodeSocketObject', 'Object') + self.inputs.new('ArmNodeSocketAnimAction', 'Action') + self.inputs.new('NodeSocketInt', 'Start Frame') + self.inputs.new('NodeSocketFloat', 'Blend') + self.inputs[-1].default_value = 0.2 + self.outputs.new('ArmNodeSocketAction', 'Out') + self.outputs.new('ArmNodeSocketAction', 'Done') + +add_node(PlayActionFromNode, category='Animation') From decf89305c7cc251c1899eaca09c3d8836101a50 Mon Sep 17 00:00:00 2001 From: N8n5h Date: Sat, 14 Mar 2020 14:08:54 -0300 Subject: [PATCH 010/230] Fix support for box/triplanar mapping I modified the shader parser so that triplanar mapping of textures is supported. Normals are supported. Currently tested with Armory PBR and Principled BSDF. --- Shaders/std/mapping.glsl | 41 ++++++++++++++++++++++++++++++++++ blender/arm/material/cycles.py | 27 +++++++++++++--------- blender/arm/material/shader.py | 9 +++++--- 3 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 Shaders/std/mapping.glsl diff --git a/Shaders/std/mapping.glsl b/Shaders/std/mapping.glsl new file mode 100644 index 00000000..3bc80cf6 --- /dev/null +++ b/Shaders/std/mapping.glsl @@ -0,0 +1,41 @@ +/* +https://github.com/JonasFolletete/glsl-triplanar-mapping + +MIT License + +Copyright (c) 2018 Jonas Folletête + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +vec3 blendNormal(vec3 normal) { + vec3 blending = abs(normal); + blending = normalize(max(blending, 0.00001)); + blending /= vec3(blending.x + blending.y + blending.z); + return blending; +} + +vec3 triplanarMapping (sampler2D ImageTexture, vec3 normal, vec3 position) { + vec3 normalBlend = blendNormal(normal); + vec3 xColor = texture(ImageTexture, position.yz).rgb; + vec3 yColor = texture(ImageTexture, position.xz).rgb; + vec3 zColor = texture(ImageTexture, position.xy).rgb; + + return (xColor * normalBlend.x + yColor * normalBlend.y + zColor * normalBlend.z); +} diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 2f65e036..b8f6e02d 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -168,6 +168,10 @@ def parse_shader(node, socket): if node.type == 'GROUP': if node.node_tree.name.startswith('Armory PBR'): if parse_surface: + # Normal + if node.inputs[5].is_linked and node.inputs[5].links[0].from_node.type == 'NORMAL_MAP': + warn(mat_name() + ' - Do not use Normal Map node with Armory PBR, connect Image Texture directly') + parse_normal_map_color_input(node.inputs[5]) # Base color out_basecol = parse_vector_input(node.inputs[0]) # Occlusion @@ -176,10 +180,6 @@ def parse_shader(node, socket): out_roughness = parse_value_input(node.inputs[3]) # Metallic out_metallic = parse_value_input(node.inputs[4]) - # Normal - if node.inputs[5].is_linked and node.inputs[5].links[0].from_node.type == 'NORMAL_MAP': - warn(mat_name() + ' - Do not use Normal Map node with Armory PBR, connect Image Texture directly') - parse_normal_map_color_input(node.inputs[5]) # Emission if node.inputs[6].is_linked or node.inputs[6].default_value != 0.0: out_emission = parse_value_input(node.inputs[6]) @@ -1495,18 +1495,23 @@ def texture_store(node, tex, tex_name, to_linear=False, tex_link=None): mat_bind_texture(tex) con.add_elem('tex', 'short2norm') curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link) + triplanar = node.projection == 'BOX' if node.inputs[0].is_linked: uv_name = parse_vector_input(node.inputs[0]) - uv_name = 'vec2({0}.x, 1.0 - {0}.y)'.format(uv_name) + if triplanar: + uv_name = 'vec3({0}.x, 1.0 - {0}.y, {0}.z)'.format(uv_name) + else: + uv_name = 'vec2({0}.x, 1.0 - {0}.y)'.format(uv_name) else: uv_name = 'texCoord' - triplanar = node.projection == 'BOX' if triplanar: - curshader.write(f'vec3 texCoordBlend = vec3(0.0); vec2 {uv_name}1 = vec2(0.0); vec2 {uv_name}2 = vec2(0.0);') # Temp - curshader.write(f'vec4 {tex_store} = vec4(0.0, 0.0, 0.0, 0.0);') - curshader.write(f'if (texCoordBlend.x > 0) {tex_store} += texture({tex_name}, {uv_name}.xy) * texCoordBlend.x;') - curshader.write(f'if (texCoordBlend.y > 0) {tex_store} += texture({tex_name}, {uv_name}1.xy) * texCoordBlend.y;') - curshader.write(f'if (texCoordBlend.z > 0) {tex_store} += texture({tex_name}, {uv_name}2.xy) * texCoordBlend.z;') + if not curshader.has_include('std/mapping.glsl'): + curshader.add_include('std/mapping.glsl') + if normal_parsed: + nor = 'TBN[2]' + else: + nor = 'n' + curshader.write('vec4 {0} = vec4(triplanarMapping({1}, {2}, {3}), 0.0);'.format(tex_store, tex_name, nor, uv_name)) else: if mat_texture_grad(): curshader.write('vec4 {0} = textureGrad({1}, {2}.xy, g2.xy, g2.zw);'.format(tex_store, tex_name, uv_name)) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 54ac9e8c..60c0817a 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -80,7 +80,7 @@ class ShaderContext: def sort_vs(self): vs = [] ar = ['pos', 'nor', 'tex', 'tex1', 'col', 'tang', 'bone', 'weight', 'ipos', 'irot', 'iscl'] - for ename in ar: + for ename in ar: elem = self.get_elem(ename) if elem != None: vs.append(elem) @@ -125,7 +125,7 @@ class ShaderContext: def make_vert(self): self.data['vertex_shader'] = self.matname + '_' + self.data['name'] + '.vert' - self.vert = Shader(self, 'vert') + self.vert = Shader(self, 'vert') return self.vert def make_frag(self): @@ -174,6 +174,9 @@ class Shader: self.is_linked = False # Use already generated shader self.noprocessing = False + def has_include(self, s): + return s in self.includes + def add_include(self, s): self.includes.append(s) @@ -295,7 +298,7 @@ class Shader: if self.shader_type == 'vert': self.vstruct_to_vsin() - + elif self.shader_type == 'tesc': in_ext = '[]' out_ext = '[]' From 5cde1487ca20260b5bd245167c8ee88ef72a2444 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Mon, 16 Mar 2020 09:05:42 +0100 Subject: [PATCH 011/230] Update CanvasScript.hx --- Sources/armory/trait/internal/CanvasScript.hx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/armory/trait/internal/CanvasScript.hx b/Sources/armory/trait/internal/CanvasScript.hx index fbc2652c..1fc25258 100644 --- a/Sources/armory/trait/internal/CanvasScript.hx +++ b/Sources/armory/trait/internal/CanvasScript.hx @@ -60,7 +60,8 @@ class CanvasScript extends Trait { notifyOnRender2D(function(g: kha.graphics2.Graphics) { if (canvas == null) return; - + + setCanvasDimensions(kha.System.windowWidth(), kha.System.windowHeight()); var events = Canvas.draw(cui, canvas, g); for (e in events) { @@ -110,7 +111,16 @@ class CanvasScript extends Trait { public function setCanvasVisibility(visible: Bool){ for (e in canvas.elements) e.visible = visible; } - + + /** + * Set dimensions of canvas + * @param x Width + * @param y Height + */ + public function setCanvasDimensions(x: Int, y: Int){ + canvas.width = x; + canvas.height = y; + } /** * Set font size of the canvas * @param fontSize Size of font to be setted From 2e529deb19e79801fbed7940ce25ffff201a8903 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Mon, 16 Mar 2020 11:51:41 +0100 Subject: [PATCH 012/230] Upgrade Wave texture node + Fix noise Value output --- blender/arm/material/cycles.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index b8f6e02d..8925e35f 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -575,7 +575,18 @@ def parse_vector(node, socket): else: co = 'bposition' scale = parse_value_input(node.inputs[1]) - res = 'vec3(tex_wave_f({0} * {1}))'.format(co, scale) + distortion = parse_value_input(node.inputs[2]) + detail = parse_value_input(node.inputs[3]) + detail_scale = parse_value_input(node.inputs[4]) + if node.wave_profile == 'SIN': + wave_profile = 0 + else: + wave_profile = 1 + if node.wave_type == 'BANDS': + wave_type = 0 + else: + wave_type = 1 + res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) if sample_bump: write_bump(node, res) return res @@ -1263,7 +1274,7 @@ def parse_value(node, socket): co = parse_vector_input(node.inputs[0]) else: co = 'bposition' - scale = parse_value_input(node.inputs[1]) + scale = parse_value_input(node.inputs[2]) # detail = parse_value_input(node.inputs[2]) # distortion = parse_value_input(node.inputs[3]) res = 'tex_noise({0} * {1})'.format(co, scale) @@ -1299,7 +1310,18 @@ def parse_value(node, socket): else: co = 'bposition' scale = parse_value_input(node.inputs[1]) - res = 'tex_wave_f({0} * {1})'.format(co, scale) + distortion = parse_value_input(node.inputs[2]) + detail = parse_value_input(node.inputs[3]) + detail_scale = parse_value_input(node.inputs[4]) + if node.wave_profile == 'SIN': + wave_profile = 0 + else: + wave_profile = 1 + if node.wave_type == 'BANDS': + wave_type = 0 + else: + wave_type = 1 + res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) if sample_bump: write_bump(node, res) return res From 2df762e3db404059f0500cafa8d21f98d572505b Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Mon, 16 Mar 2020 11:53:12 +0100 Subject: [PATCH 013/230] Upgrade Wave texture shader --- blender/arm/material/cycles_functions.py | 66 +++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index 2db3c549..cbf9956e 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -189,8 +189,70 @@ float tex_brick_f(vec3 p) { """ str_tex_wave = """ -float tex_wave_f(const vec3 p) { - return 1.0 - sin((p.x + p.y) * 10.0); +// +// By Morgan McGuire @morgan3d, http://graphicscodex.com +float hash(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } + +float noise(vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// Shader-code from adapted from Blender +// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl +float fractal_noise(vec2 p, float octaves) +{ + float fscale = 1.0; + float amp = 1.0; + float sum = 0.0; + octaves = clamp(octaves, 0.0, 16.0); + int n = int(octaves); + for (int i = 0; i <= n; i++) { + float t = noise(fscale * p); + sum += t * amp; + amp *= 0.5; + fscale *= 2.0; + } + float rmd = octaves - floor(octaves); + if (rmd != 0.0) { + float t = noise(fscale * p); + float sum2 = sum + t * amp; + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); + sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); + return (1.0 - rmd) * sum + rmd * sum2; + } + else { + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); + return sum; + } +} + +float tex_wave_f(const vec3 p, int type, int profile, float dist, float detail, float detail_scale) { + float n; + if(type == 0) n = (p.x + p.y) * 9.5; + else n = length(p.xy) * 13.0; + if(dist != 0.0) n += dist * fractal_noise(vec2(p.xy)*detail_scale,detail) * 2.0 - 1.0; + if(profile == 0) { return 0.5 + 0.5 * sin(n - 3.14159265359); } + else { + n /= 2.0 * 3.14159265359; + return n - floor(n); + } } """ From 37e04689d5cbad76b74239b422e8a0ddd81eb82f Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Mon, 16 Mar 2020 22:22:47 +0900 Subject: [PATCH 014/230] Delete Distance Node Remove this because the same can be achieved with "Vector Math" --- Sources/armory/logicnode/DistanceNode.hx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 Sources/armory/logicnode/DistanceNode.hx diff --git a/Sources/armory/logicnode/DistanceNode.hx b/Sources/armory/logicnode/DistanceNode.hx deleted file mode 100644 index b7f68a70..00000000 --- a/Sources/armory/logicnode/DistanceNode.hx +++ /dev/null @@ -1,19 +0,0 @@ -package armory.logicnode; - -import iron.math.Vec4; - -class DistanceNode extends LogicNode { - - public function new(tree: LogicTree) { - super(tree); - } - - override function get(from: Int): Dynamic { - var vector1: Vec4 = inputs[0].get(); - var vector2: Vec4 = inputs[1].get(); - - if (vector1 == null || vector2 == null) return 0; - - return iron.math.Vec4.distance(vector1, vector2); - } -} From 7d901014480e3a47b715a19ebdca3365fb8338a5 Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Mon, 16 Mar 2020 22:24:42 +0900 Subject: [PATCH 015/230] Delete Distance Node Remove this because the same can be achieved with "Vector Math" --- blender/arm/logicnode/value_distance.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 blender/arm/logicnode/value_distance.py diff --git a/blender/arm/logicnode/value_distance.py b/blender/arm/logicnode/value_distance.py deleted file mode 100644 index 9a08db2c..00000000 --- a/blender/arm/logicnode/value_distance.py +++ /dev/null @@ -1,17 +0,0 @@ -import bpy -from bpy.props import * -from bpy.types import Node, NodeSocket -from arm.logicnode.arm_nodes import * - -class DistanceNode(Node, ArmLogicTreeNode): - '''Distance node''' - bl_idname = 'LNDistanceNode' - bl_label = 'Distance' - bl_icon = 'QUESTION' - - def init(self, context): - self.inputs.new('NodeSocketVector', 'Vector') - self.inputs.new('NodeSocketVector', 'Vector') - self.outputs.new('NodeSocketFloat', 'Distance') - -add_node(DistanceNode, category='Value') From c33d886caa53da69024ab4f7f8a57fc063dee683 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Tue, 17 Mar 2020 09:51:34 +0100 Subject: [PATCH 016/230] Update cycles.py --- blender/arm/material/cycles.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 8925e35f..8ab0b1b1 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -534,10 +534,9 @@ def parse_vector(node, socket): else: co = 'bposition' scale = parse_value_input(node.inputs[2]) - # detail = parse_value_input(node.inputs[2]) - # distortion = parse_value_input(node.inputs[3]) - # Slow.. - res = 'vec3(tex_noise({0} * {1}), tex_noise({0} * {1} + 5.0), tex_noise({0} * {1} + 8.0))'.format(co, scale) + detail = parse_value_input(node.inputs[3]) + distortion = parse_value_input(node.inputs[4]) + res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion) if sample_bump: write_bump(node, res, 0.1) return res @@ -1275,9 +1274,9 @@ def parse_value(node, socket): else: co = 'bposition' scale = parse_value_input(node.inputs[2]) - # detail = parse_value_input(node.inputs[2]) - # distortion = parse_value_input(node.inputs[3]) - res = 'tex_noise({0} * {1})'.format(co, scale) + detail = parse_value_input(node.inputs[3]) + distortion = parse_value_input(node.inputs[4]) + res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion) if sample_bump: write_bump(node, res, 0.1) return res From 93492ea6926e35fe66f9581c8f2a56057e004392 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Tue, 17 Mar 2020 09:53:21 +0100 Subject: [PATCH 017/230] Upgrade noise texture --- blender/arm/material/cycles_functions.py | 78 ++++++++++++++++++------ 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index cbf9956e..5318f062 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -56,26 +56,66 @@ vec4 tex_voronoi(const vec3 x) { # By Morgan McGuire @morgan3d, http://graphicscodex.com Reuse permitted under the BSD license. # https://www.shadertoy.com/view/4dS3Wd str_tex_noise = """ -float hash(float n) { return fract(sin(n) * 1e4); } -float tex_noise_f(vec3 x) { - const vec3 step = vec3(110, 241, 171); - vec3 i = floor(x); - vec3 f = fract(x); - float n = dot(i, step); - vec3 u = f * f * (3.0 - 2.0 * f); - return mix(mix(mix(hash(n + dot(step, vec3(0, 0, 0))), hash(n + dot(step, vec3(1, 0, 0))), u.x), - mix(hash(n + dot(step, vec3(0, 1, 0))), hash(n + dot(step, vec3(1, 1, 0))), u.x), u.y), - mix(mix(hash(n + dot(step, vec3(0, 0, 1))), hash(n + dot(step, vec3(1, 0, 1))), u.x), - mix(hash(n + dot(step, vec3(0, 1, 1))), hash(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z); +// +// By Morgan McGuire @morgan3d, http://graphicscodex.com +float hash(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } + +float noise(vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; } -float tex_noise(vec3 k) { - vec3 p = vec3(k.xy, 0); - p *= 1.25; - float f = 0.5 * tex_noise_f(p); p *= 2.01; - f += 0.25 * tex_noise_f(p); p *= 2.02; - f += 0.125 * tex_noise_f(p); p *= 2.03; - f += 0.0625 * tex_noise_f(p); - return 1.0 - f; + +// Shader-code from adapted from Blender +// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl +float fractal_noise(vec2 p, float octaves) +{ + float fscale = 1.0; + float amp = 1.0; + float sum = 0.0; + octaves = clamp(octaves, 0.0, 16.0); + int n = int(octaves); + for (int i = 0; i <= n; i++) { + float t = noise(fscale * p); + sum += t * amp; + amp *= 0.5; + fscale *= 2.0; + } + float rmd = octaves - floor(octaves); + if (rmd != 0.0) { + float t = noise(fscale * p); + float sum2 = sum + t * amp; + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); + sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); + return (1.0 - rmd) * sum + rmd * sum2; + } + else { + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); + return sum; + } +} + +float tex_noise(vec3 co, float detail, float distortion) { + vec2 p = co.xy * 2; + if (distortion != 0.0) { + p += vec2(noise(p) * distortion,noise(p) * distortion); + } + return fractal_noise(p, detail); } """ From e1ecccf69f7677de5e9a59f0df7e7a8c43763378 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Wed, 18 Mar 2020 09:35:23 +0100 Subject: [PATCH 018/230] Update cycles.py --- blender/arm/material/cycles.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 8ab0b1b1..7216ab8c 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -51,6 +51,7 @@ def parse_output(node, _con, _vert, _frag, _geom, _tesc, _tese, _parse_surface, global particle_info global sample_bump global sample_bump_res + global procedurals_written con = _con vert = _vert frag = _frag @@ -71,6 +72,7 @@ def parse_output(node, _con, _vert, _frag, _geom, _tesc, _tese, _parse_surface, particle_info['angular_velocity'] = False sample_bump = False sample_bump_res = '' + procedurals_written = False wrd = bpy.data.worlds['Arm'] # Surface @@ -525,6 +527,7 @@ def parse_vector(node, socket): return res elif node.type == 'TEX_NOISE': + write_procedurals() curshader.add_function(c_functions.str_tex_noise) assets_add(get_sdk_path() + '/armory/Assets/' + 'noise256.png') assets_add_embedded_data('noise256.png') @@ -568,6 +571,7 @@ def parse_vector(node, socket): return res elif node.type == 'TEX_WAVE': + write_procedurals() curshader.add_function(c_functions.str_tex_wave) if node.inputs[0].is_linked: co = parse_vector_input(node.inputs[0]) @@ -1265,6 +1269,7 @@ def parse_value(node, socket): return res elif node.type == 'TEX_NOISE': + write_procedurals() curshader.add_function(c_functions.str_tex_noise) assets_add(get_sdk_path() + '/armory/Assets/' + 'noise256.png') assets_add_embedded_data('noise256.png') @@ -1303,6 +1308,7 @@ def parse_value(node, socket): return res elif node.type == 'TEX_WAVE': + write_procedurals() curshader.add_function(c_functions.str_tex_wave) if node.inputs[0].is_linked: co = parse_vector_input(node.inputs[0]) @@ -1491,6 +1497,13 @@ def write_result(l): return None return res_var +def write_procedurals(): + global procedurals_written + if(not procedurals_written): + curshader.add_function(c_functions.str_tex_proc) + procedurals_written = True + return + def glsl_type(t): if t == 'RGB' or t == 'RGBA' or t == 'VECTOR': return 'vec3' From a544194d4ae6c5da873340c7b5788e434317c94a Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Wed, 18 Mar 2020 09:38:18 +0100 Subject: [PATCH 019/230] Separate procedural functions --- blender/arm/material/cycles_functions.py | 168 ++++++++--------------- 1 file changed, 58 insertions(+), 110 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index 5318f062..c30a0255 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -1,3 +1,59 @@ +str_tex_proc = """ +// +// By Morgan McGuire @morgan3d, http://graphicscodex.com +float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } + +float noise(const vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash_f(i); + float b = hash_f(i + vec2(1.0, 0.0)); + float c = hash_f(i + vec2(0.0, 1.0)); + float d = hash_f(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// Shader-code from adapted from Blender +// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl +float fractal_noise(const vec2 p, const float o) +{ + float fscale = 1.0; + float amp = 1.0; + float sum = 0.0; + float octaves = clamp(o, 0.0, 16.0); + int n = int(octaves); + for (int i = 0; i <= n; i++) { + float t = noise(fscale * p); + sum += t * amp; + amp *= 0.5; + fscale *= 2.0; + } + float rmd = octaves - floor(octaves); + if (rmd != 0.0) { + float t = noise(fscale * p); + float sum2 = sum + t * amp; + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); + sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); + return (1.0 - rmd) * sum + rmd * sum2; + } + else { + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); + return sum; + } +} +""" + str_tex_checker = """ vec3 tex_checker(const vec3 co, const vec3 col1, const vec3 col2, const float scale) { // Prevent precision issues on unit coordinates @@ -56,61 +112,7 @@ vec4 tex_voronoi(const vec3 x) { # By Morgan McGuire @morgan3d, http://graphicscodex.com Reuse permitted under the BSD license. # https://www.shadertoy.com/view/4dS3Wd str_tex_noise = """ -// -// By Morgan McGuire @morgan3d, http://graphicscodex.com -float hash(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } - -float noise(vec2 x) { - vec2 i = floor(x); - vec2 f = fract(x); - - // Four corners in 2D of a tile - float a = hash(i); - float b = hash(i + vec2(1.0, 0.0)); - float c = hash(i + vec2(0.0, 1.0)); - float d = hash(i + vec2(1.0, 1.0)); - - // Simple 2D lerp using smoothstep envelope between the values. - // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), - // mix(c, d, smoothstep(0.0, 1.0, f.x)), - // smoothstep(0.0, 1.0, f.y))); - - // Same code, with the clamps in smoothstep and common subexpressions - // optimized away. - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; -} - -// Shader-code from adapted from Blender -// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl -float fractal_noise(vec2 p, float octaves) -{ - float fscale = 1.0; - float amp = 1.0; - float sum = 0.0; - octaves = clamp(octaves, 0.0, 16.0); - int n = int(octaves); - for (int i = 0; i <= n; i++) { - float t = noise(fscale * p); - sum += t * amp; - amp *= 0.5; - fscale *= 2.0; - } - float rmd = octaves - floor(octaves); - if (rmd != 0.0) { - float t = noise(fscale * p); - float sum2 = sum + t * amp; - sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); - sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); - return (1.0 - rmd) * sum + rmd * sum2; - } - else { - sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); - return sum; - } -} - -float tex_noise(vec3 co, float detail, float distortion) { +float tex_noise(const vec3 co, const float detail, const float distortion) { vec2 p = co.xy * 2; if (distortion != 0.0) { p += vec2(noise(p) * distortion,noise(p) * distortion); @@ -229,61 +231,7 @@ float tex_brick_f(vec3 p) { """ str_tex_wave = """ -// -// By Morgan McGuire @morgan3d, http://graphicscodex.com -float hash(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } - -float noise(vec2 x) { - vec2 i = floor(x); - vec2 f = fract(x); - - // Four corners in 2D of a tile - float a = hash(i); - float b = hash(i + vec2(1.0, 0.0)); - float c = hash(i + vec2(0.0, 1.0)); - float d = hash(i + vec2(1.0, 1.0)); - - // Simple 2D lerp using smoothstep envelope between the values. - // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), - // mix(c, d, smoothstep(0.0, 1.0, f.x)), - // smoothstep(0.0, 1.0, f.y))); - - // Same code, with the clamps in smoothstep and common subexpressions - // optimized away. - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; -} - -// Shader-code from adapted from Blender -// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl -float fractal_noise(vec2 p, float octaves) -{ - float fscale = 1.0; - float amp = 1.0; - float sum = 0.0; - octaves = clamp(octaves, 0.0, 16.0); - int n = int(octaves); - for (int i = 0; i <= n; i++) { - float t = noise(fscale * p); - sum += t * amp; - amp *= 0.5; - fscale *= 2.0; - } - float rmd = octaves - floor(octaves); - if (rmd != 0.0) { - float t = noise(fscale * p); - float sum2 = sum + t * amp; - sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); - sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); - return (1.0 - rmd) * sum + rmd * sum2; - } - else { - sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); - return sum; - } -} - -float tex_wave_f(const vec3 p, int type, int profile, float dist, float detail, float detail_scale) { +float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) { float n; if(type == 0) n = (p.x + p.y) * 9.5; else n = length(p.xy) * 13.0; From 222ccd50d14eef827ec5e4e36d33143058cb1b42 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Thu, 19 Mar 2020 11:24:17 +0100 Subject: [PATCH 020/230] Fix div by 0 when mesh is flat --- blender/arm/material/make_finalize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/material/make_finalize.py b/blender/arm/material/make_finalize.py index 955b60e6..be282549 100644 --- a/blender/arm/material/make_finalize.py +++ b/blender/arm/material/make_finalize.py @@ -68,6 +68,7 @@ def make(con_mesh): vert.add_uniform('vec3 hdim', link='_halfDim') vert.add_uniform('float posUnpack', link='_posUnpack') vert.write_attrib('bposition = (spos.xyz * posUnpack + hdim) / dim;') + vert.write_attrib('if (dim.z == 0) bposition.z = 0;') if tese != None: if frag_bpos: From a0ad5061d00cd7a2a78f0465c27893178f683aa2 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Thu, 19 Mar 2020 11:29:04 +0100 Subject: [PATCH 021/230] Back to using 3d textures --- blender/arm/material/cycles_functions.py | 47 +++++++++++------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index c30a0255..ac62e6d5 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -1,32 +1,29 @@ str_tex_proc = """ // // By Morgan McGuire @morgan3d, http://graphicscodex.com +float hash_f(const float n) { return fract(sin(n) * 1e4); } float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } -float noise(const vec2 x) { - vec2 i = floor(x); - vec2 f = fract(x); +float noise(const vec3 x) { + const vec3 step = vec3(110, 241, 171); - // Four corners in 2D of a tile - float a = hash_f(i); - float b = hash_f(i + vec2(1.0, 0.0)); - float c = hash_f(i + vec2(0.0, 1.0)); - float d = hash_f(i + vec2(1.0, 1.0)); + vec3 i = floor(x); + vec3 f = fract(x); + + // For performance, compute the base input to a 1D hash from the integer part of the argument and the + // incremental change to the 1D based on the 3D -> 1D wrapping + float n = dot(i, step); - // Simple 2D lerp using smoothstep envelope between the values. - // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), - // mix(c, d, smoothstep(0.0, 1.0, f.x)), - // smoothstep(0.0, 1.0, f.y))); - - // Same code, with the clamps in smoothstep and common subexpressions - // optimized away. - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; + vec3 u = f * f * (3.0 - 2.0 * f); + return mix(mix(mix( hash_f(n + dot(step, vec3(0, 0, 0))), hash_f(n + dot(step, vec3(1, 0, 0))), u.x), + mix( hash_f(n + dot(step, vec3(0, 1, 0))), hash_f(n + dot(step, vec3(1, 1, 0))), u.x), u.y), + mix(mix( hash_f(n + dot(step, vec3(0, 0, 1))), hash_f(n + dot(step, vec3(1, 0, 1))), u.x), + mix( hash_f(n + dot(step, vec3(0, 1, 1))), hash_f(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z); } // Shader-code from adapted from Blender // https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl -float fractal_noise(const vec2 p, const float o) +float fractal_noise(const vec3 p, const float o) { float fscale = 1.0; float amp = 1.0; @@ -112,12 +109,12 @@ vec4 tex_voronoi(const vec3 x) { # By Morgan McGuire @morgan3d, http://graphicscodex.com Reuse permitted under the BSD license. # https://www.shadertoy.com/view/4dS3Wd str_tex_noise = """ -float tex_noise(const vec3 co, const float detail, const float distortion) { - vec2 p = co.xy * 2; +float tex_noise(const vec3 p, const float detail, const float distortion) { + vec3 pk = p; if (distortion != 0.0) { - p += vec2(noise(p) * distortion,noise(p) * distortion); + pk += vec3(noise(p) * distortion); } - return fractal_noise(p, detail); + return fractal_noise(pk, detail); } """ @@ -233,9 +230,9 @@ float tex_brick_f(vec3 p) { str_tex_wave = """ float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) { float n; - if(type == 0) n = (p.x + p.y) * 9.5; - else n = length(p.xy) * 13.0; - if(dist != 0.0) n += dist * fractal_noise(vec2(p.xy)*detail_scale,detail) * 2.0 - 1.0; + if(type == 0) n = (p.x + p.y + p.z) * 9.5; + else n = length(p) * 13.0; + if(dist != 0.0) n += dist * fractal_noise(p * detail_scale, detail) * 2.0 - 1.0; if(profile == 0) { return 0.5 + 0.5 * sin(n - 3.14159265359); } else { n /= 2.0 * 3.14159265359; From 97896c9fb20902af34fd36ad6ef5fffb28dd89c3 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Thu, 19 Mar 2020 12:08:56 +0100 Subject: [PATCH 022/230] Fix for other dimensions as well --- blender/arm/material/make_finalize.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/material/make_finalize.py b/blender/arm/material/make_finalize.py index be282549..939e4bf9 100644 --- a/blender/arm/material/make_finalize.py +++ b/blender/arm/material/make_finalize.py @@ -69,6 +69,8 @@ def make(con_mesh): vert.add_uniform('float posUnpack', link='_posUnpack') vert.write_attrib('bposition = (spos.xyz * posUnpack + hdim) / dim;') vert.write_attrib('if (dim.z == 0) bposition.z = 0;') + vert.write_attrib('if (dim.y == 0) bposition.y = 0;') + vert.write_attrib('if (dim.x == 0) bposition.x = 0;') if tese != None: if frag_bpos: From b2946ec87d53dd2f5d13ff046e79bc457f63fe02 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Fri, 20 Mar 2020 15:16:08 +0100 Subject: [PATCH 023/230] Update cycles.py --- blender/arm/material/cycles.py | 50 +++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 7216ab8c..6e73ba97 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -553,19 +553,28 @@ def parse_vector(node, socket): return to_vec3([0.0, 0.0, 0.0]) elif node.type == 'TEX_VORONOI': + write_procedurals() + outp = 0 + if socket.type == 'RGBA': + outp = 1 + elif socket.type == 'VECTOR': + outp = 2 + m = 0 + if node.distance == 'MANHATTAN': + m = 1 + elif node.distance == 'CHEBYCHEV': + m = 2 + elif node.distance == 'MINKOWSKI': + m = 3 curshader.add_function(c_functions.str_tex_voronoi) - assets_add(get_sdk_path() + '/armory/Assets/' + 'noise256.png') - assets_add_embedded_data('noise256.png') - curshader.add_uniform('sampler2D snoise256', link='$noise256.png') if node.inputs[0].is_linked: co = parse_vector_input(node.inputs[0]) else: co = 'bposition' - scale = parse_value_input(node.inputs[1]) - if node.coloring == 'INTENSITY': - res = 'vec3(tex_voronoi({0} * {1}).a)'.format(co, scale) - else: # CELLS - res = 'tex_voronoi({0} * {1}).rgb'.format(co, scale) + scale = parse_value_input(node.inputs[2]) + exp = parse_value_input(node.inputs[4]) + randomness = parse_value_input(node.inputs[5]) + res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5})'.format(co, randomness, m, outp, scale, exp) if sample_bump: write_bump(node, res) return res @@ -1290,19 +1299,28 @@ def parse_value(node, socket): return '0.0' elif node.type == 'TEX_VORONOI': + write_procedurals() + outp = 0 + if socket.type == 'RGBA': + outp = 1 + elif socket.type == 'VECTOR': + outp = 2 + m = 0 + if node.distance == 'MANHATTAN': + m = 1 + elif node.distance == 'CHEBYCHEV': + m = 2 + elif node.distance == 'MINKOWSKI': + m = 3 curshader.add_function(c_functions.str_tex_voronoi) - assets_add(get_sdk_path() + '/armory/Assets/' + 'noise256.png') - assets_add_embedded_data('noise256.png') - curshader.add_uniform('sampler2D snoise256', link='$noise256.png') if node.inputs[0].is_linked: co = parse_vector_input(node.inputs[0]) else: co = 'bposition' - scale = parse_value_input(node.inputs[1]) - if node.coloring == 'INTENSITY': - res = 'tex_voronoi({0} * {1}).a'.format(co, scale) - else: # CELLS - res = 'tex_voronoi({0} * {1}).r'.format(co, scale) + scale = parse_value_input(node.inputs[2]) + exp = parse_value_input(node.inputs[4]) + randomness = parse_value_input(node.inputs[5]) + res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5}).x'.format(co, randomness, m, outp, scale, exp) if sample_bump: write_bump(node, res) return res From d9d6c3e1d95c21f186f66de83265a0c586b7a16d Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Fri, 20 Mar 2020 15:19:08 +0100 Subject: [PATCH 024/230] Update cycles_functions.py --- blender/arm/material/cycles_functions.py | 74 +++++++++++++++++------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index ac62e6d5..8bad6549 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -21,7 +21,7 @@ float noise(const vec3 x) { mix( hash_f(n + dot(step, vec3(0, 1, 1))), hash_f(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z); } -// Shader-code from adapted from Blender +// Shader-code adapted from Blender // https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl float fractal_noise(const vec3 p, const float o) { @@ -70,28 +70,62 @@ float tex_checker_f(const vec3 co, const float scale) { } """ -# Created by inigo quilez - iq/2013 -# License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License str_tex_voronoi = """ -vec4 tex_voronoi(const vec3 x) { - vec3 p = floor(x); - vec3 f = fract(x); - float id = 0.0; - float res = 100.0; - for (int k = -1; k <= 1; k++) - for (int j = -1; j <= 1; j++) - for (int i = -1; i <= 1; i++) { - vec3 b = vec3(float(i), float(j), float(k)); - vec3 pb = p + b; - vec3 r = vec3(b) - f + texture(snoise256, (pb.xy + vec2(3.0, 1.0) * pb.z + 0.5) / 256.0).xyz; - float d = dot(r, r); - if (d < res) { - id = dot(p + b, vec3(1.0, 57.0, 113.0)); - res = d; +//Shader-code from adapted from Blender +//https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_voronoi.glsl +float voronoi_distance(const vec3 a, const vec3 b, const int metric, const float exponent) +{ + if (metric == 0) // SHD_VORONOI_EUCLIDEAN + { + return distance(a, b); + } + else if (metric == 1) // SHD_VORONOI_MANHATTAN + { + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); + } + else if (metric == 2) // SHD_VORONOI_CHEBYCHEV + { + return max(abs(a.x - b.x), max(abs(a.y - b.y), abs(a.z - b.z))); + } + else if (metric == 3) // SHD_VORONOI_MINKOWSKI + { + return pow(pow(abs(a.x - b.x), exponent) + pow(abs(a.y - b.y), exponent) + + pow(abs(a.z - b.z), exponent), + 1.0 / exponent); + } + else { + return 0.5; + } +} + +vec3 tex_voronoi(const vec3 coord, const float r, const int metric, const int outp, const float scale, const float exp) +{ + float randomness = clamp(r, 0.0, 1.0); + + vec3 scaledCoord = coord * scale; + vec3 cellPosition = floor(scaledCoord); + vec3 localPosition = scaledCoord - cellPosition; + + float minDistance = 8.0; + vec3 targetOffset, targetPosition; + for (int k = -1; k <= 1; k++) { + for (int j = -1; j <= 1; j++) { + for (int i = -1; i <= 1; i++) { + vec3 cellOffset = vec3(float(i), float(j), float(k)); + vec3 pointPosition = cellOffset + + (vec3(noise(cellPosition+cellOffset), noise(cellPosition+cellOffset+972.37), noise(cellPosition+cellOffset+342.48)) * randomness); + float distanceToPoint = voronoi_distance(pointPosition, localPosition, metric, exp); + if (distanceToPoint < minDistance) { + targetOffset = cellOffset; + minDistance = distanceToPoint; + targetPosition = pointPosition; } + } } - vec3 col = 0.5 + 0.5 * cos(id * 0.35 + vec3(0.0, 1.0, 2.0)); - return vec4(col, sqrt(res)); + } + if(outp == 0){return vec3(minDistance);} + else if(outp == 1) {return vec3(noise(cellPosition+targetOffset), noise(cellPosition+targetOffset+972.37), noise(cellPosition+targetOffset+342.48));} + return (targetPosition + cellPosition)/scale; } """ From 08b86e7eb930cc967daaf816a93c2ca47af45d82 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Fri, 20 Mar 2020 15:44:17 +0100 Subject: [PATCH 025/230] Fix typo --- blender/arm/material/cycles_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index 8bad6549..01851258 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -71,7 +71,7 @@ float tex_checker_f(const vec3 co, const float scale) { """ str_tex_voronoi = """ -//Shader-code from adapted from Blender +//Shader-code adapted from Blender //https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_voronoi.glsl float voronoi_distance(const vec3 a, const vec3 b, const int metric, const float exponent) { From 64ed9d8d9ef4d91f66ed84029bc10e34ed7e980b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valent=C3=ADn=20Barros?= Date: Sat, 21 Mar 2020 11:07:58 +0100 Subject: [PATCH 026/230] Fixed memory problem in `ArmoryExporter.execute` [fixes #1604] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Blender crash referenced in the issue happened always when starting 2nd iteration of the loop in line 2041. I'm not really sure, but I bet it had to do with `export_object` modifying `bpy.context.collection.objects` (line 818), wich in turn could invalidate the reference obtained in line 1926, if I understand a bit of Blender internals — wich I didn't some days ago, so this could be completely wrong. It no longer happens with this change. --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 910a5534..d4f58218 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1923,7 +1923,7 @@ class ArmoryExporter: # scene_objects = [] # for lay in self.scene.view_layers: # scene_objects += lay.objects - scene_objects = self.scene.collection.all_objects + scene_objects = self.scene.collection.all_objects.values() for bobject in scene_objects: # Map objects to game objects From 648d2b1bdabfdc94bbe83ee1316cbde4ff968901 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Sat, 21 Mar 2020 11:30:56 +0100 Subject: [PATCH 027/230] Optimize Voronoi --- blender/arm/material/cycles_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index 01851258..de64e6ec 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -124,7 +124,7 @@ vec3 tex_voronoi(const vec3 coord, const float r, const int metric, const int ou } } if(outp == 0){return vec3(minDistance);} - else if(outp == 1) {return vec3(noise(cellPosition+targetOffset), noise(cellPosition+targetOffset+972.37), noise(cellPosition+targetOffset+342.48));} + else if(outp == 1) {return targetPosition - targetOffset;} return (targetPosition + cellPosition)/scale; } """ From 8b5d000748fd802619f8471010f83dd0592fac81 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Sat, 21 Mar 2020 12:59:02 +0100 Subject: [PATCH 028/230] Optimise some more --- blender/arm/material/cycles_functions.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index de64e6ec..39e09eff 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -3,6 +3,7 @@ str_tex_proc = """ // By Morgan McGuire @morgan3d, http://graphicscodex.com float hash_f(const float n) { return fract(sin(n) * 1e4); } float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } +float hash_f(vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); } float noise(const vec3 x) { const vec3 step = vec3(110, 241, 171); @@ -112,8 +113,9 @@ vec3 tex_voronoi(const vec3 coord, const float r, const int metric, const int ou for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { vec3 cellOffset = vec3(float(i), float(j), float(k)); - vec3 pointPosition = cellOffset + - (vec3(noise(cellPosition+cellOffset), noise(cellPosition+cellOffset+972.37), noise(cellPosition+cellOffset+342.48)) * randomness); + vec3 pointPosition = cellOffset; + if(randomness != 0.) { + pointPosition += vec3(hash_f(cellPosition+cellOffset), hash_f(cellPosition+cellOffset+972.37), hash_f(cellPosition+cellOffset+342.48)) * randomness;} float distanceToPoint = voronoi_distance(pointPosition, localPosition, metric, exp); if (distanceToPoint < minDistance) { targetOffset = cellOffset; @@ -124,8 +126,11 @@ vec3 tex_voronoi(const vec3 coord, const float r, const int metric, const int ou } } if(outp == 0){return vec3(minDistance);} - else if(outp == 1) {return targetPosition - targetOffset;} - return (targetPosition + cellPosition)/scale; + else if(outp == 1) { + if(randomness == 0.) {return vec3(hash_f(cellPosition+targetOffset), hash_f(cellPosition+targetOffset+972.37), hash_f(cellPosition+targetOffset+342.48));} + return (targetPosition - targetOffset)/randomness; + } + return (targetPosition + cellPosition) / scale; } """ @@ -267,9 +272,9 @@ float tex_wave_f(const vec3 p, const int type, const int profile, const float di if(type == 0) n = (p.x + p.y + p.z) * 9.5; else n = length(p) * 13.0; if(dist != 0.0) n += dist * fractal_noise(p * detail_scale, detail) * 2.0 - 1.0; - if(profile == 0) { return 0.5 + 0.5 * sin(n - 3.14159265359); } + if(profile == 0) { return 0.5 + 0.5 * sin(n - PI); } else { - n /= 2.0 * 3.14159265359; + n /= 2.0 * PI; return n - floor(n); } } From 8f04e18e0752c0c124a034f1b61ec6e632b72955 Mon Sep 17 00:00:00 2001 From: Simonrazer Date: Sat, 21 Mar 2020 13:02:12 +0100 Subject: [PATCH 029/230] Fix for Fragment --- blender/arm/material/cycles_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py index 39e09eff..4ff9fcc3 100644 --- a/blender/arm/material/cycles_functions.py +++ b/blender/arm/material/cycles_functions.py @@ -3,7 +3,7 @@ str_tex_proc = """ // By Morgan McGuire @morgan3d, http://graphicscodex.com float hash_f(const float n) { return fract(sin(n) * 1e4); } float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } -float hash_f(vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); } +float hash_f(const vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); } float noise(const vec3 x) { const vec3 step = vec3(110, 241, 171); From 4bd1d40a9c5fe7106dacf9f4b34e202d51013dc4 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Wed, 1 Apr 2020 10:44:47 +0200 Subject: [PATCH 030/230] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 6b4b39b7..af222807 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -12,7 +12,7 @@ import arm.proxy import arm.nodes_logic # Armory version -arm_version = '2020.3' +arm_version = '2020.4' arm_commit = '$Id$' def init_properties(): From f3b58c0f318eafa40cde8f01a3e646ae120de975 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 3 Apr 2020 17:05:13 +0200 Subject: [PATCH 031/230] Fixed Bullet Rigid Body C target issue --- Sources/armory/trait/physics/bullet/RigidBody.hx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/armory/trait/physics/bullet/RigidBody.hx b/Sources/armory/trait/physics/bullet/RigidBody.hx index 63fec4c5..650ee895 100644 --- a/Sources/armory/trait/physics/bullet/RigidBody.hx +++ b/Sources/armory/trait/physics/bullet/RigidBody.hx @@ -575,7 +575,14 @@ class RigidBody extends iron.Trait { else { triangleMeshCache.remove(data); - if(meshInterface != null) bullet.Bt.Ammo.destroy(meshInterface); + if(meshInterface != null) + { + #if js + bullet.Bt.Ammo.destroy(meshInterface); + #else + meshInterface.delete(); + #end + } } } } From 4c3332858ef0f397d898b3050fd86f80e47868eb Mon Sep 17 00:00:00 2001 From: luboslenco Date: Tue, 7 Apr 2020 10:31:32 +0200 Subject: [PATCH 032/230] Mark return value for CanvasGetCheckbox and CanvasGetSlider nodes --- Sources/armory/logicnode/CanvasGetCheckboxNode.hx | 2 +- Sources/armory/logicnode/CanvasGetSliderNode.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/CanvasGetCheckboxNode.hx b/Sources/armory/logicnode/CanvasGetCheckboxNode.hx index 6bb2f4d8..5dabc261 100644 --- a/Sources/armory/logicnode/CanvasGetCheckboxNode.hx +++ b/Sources/armory/logicnode/CanvasGetCheckboxNode.hx @@ -12,7 +12,7 @@ class CanvasGetCheckboxNode extends LogicNode { } #if arm_ui - override function get(from: Int) { + override function get(from: Int): Dynamic { // Null if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); if (canvas == null || !canvas.ready) return null; diff --git a/Sources/armory/logicnode/CanvasGetSliderNode.hx b/Sources/armory/logicnode/CanvasGetSliderNode.hx index e8da408d..660d9f40 100644 --- a/Sources/armory/logicnode/CanvasGetSliderNode.hx +++ b/Sources/armory/logicnode/CanvasGetSliderNode.hx @@ -12,7 +12,7 @@ class CanvasGetSliderNode extends LogicNode { } #if arm_ui - override function get(from: Int) { + override function get(from: Int): Dynamic { // Null if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); if (canvas == null || !canvas.ready) return null; From f035b3aaba61c0375c84647b1753d272f59d812d Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Wed, 8 Apr 2020 23:41:01 +0900 Subject: [PATCH 033/230] Mark return value for CanvasGetPosition nodes --- Sources/armory/logicnode/CanvasGetPositionNode.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/CanvasGetPositionNode.hx b/Sources/armory/logicnode/CanvasGetPositionNode.hx index 4fa41a61..0f98660b 100644 --- a/Sources/armory/logicnode/CanvasGetPositionNode.hx +++ b/Sources/armory/logicnode/CanvasGetPositionNode.hx @@ -12,7 +12,7 @@ class CanvasGetPositionNode extends LogicNode { } #if arm_ui - override function get(from: Int) { + override function get(from: Int): Dynamic { // Null if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); if (canvas == null || !canvas.ready) return null; From 829aa20f088896d86fe36f546555aefa181871e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 8 Apr 2020 23:24:43 +0200 Subject: [PATCH 034/230] Fix camera export when multiple scenes are exported --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index d4f58218..128ba63a 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -713,7 +713,7 @@ class ArmoryExporter: elif type == NodeTypeCamera: if 'spawn' in o and not o['spawn']: - self.camera_spawned = False + self.camera_spawned |= False else: self.camera_spawned = True if objref not in self.cameraArray: From 2f3bcbf8c6bb3841dd7ded185963b3876dc3bf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 00:03:22 +0200 Subject: [PATCH 035/230] Better asset name conflict detection + use log.warn() for warnings --- blender/arm/assets.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/blender/arm/assets.py b/blender/arm/assets.py index c18efc3e..7b0d1bdc 100755 --- a/blender/arm/assets.py +++ b/blender/arm/assets.py @@ -48,20 +48,26 @@ def reset(): shader_cons['voxel_frag'] = [] shader_cons['voxel_geom'] = [] -def add(file): +def add(asset_file): global assets - if file in assets: + + # Asset already exists, do nothing + if asset_file in assets: return - base = os.path.basename(file) + + asset_file_base = os.path.basename(asset_file) for f in assets: - if f.endswith(base): - print('Armory Warning: Asset name "{0}" already exists, skipping'.format(base)) + f_file_base = os.path.basename(f) + if f_file_base == asset_file_base: + log.warn(f'Armory Warning: Asset name "{asset_file_base}" already exists, skipping') return - assets.append(file) + + assets.append(asset_file) + # Reserved file name for f in reserved_names: - if f in file: - print('Armory Warning: File "{0}" contains reserved keyword, this will break C++ builds!'.format(file)) + if f in asset_file: + log.warn(f'Armory Warning: File "{asset_file}" contains reserved keyword, this will break C++ builds!') def add_khafile_def(d): global khafile_defs From f242e0316e0aa014bb08f930b9e6d802fd357363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 00:20:54 +0200 Subject: [PATCH 036/230] Fix debug console for scenes with no world --- Sources/armory/trait/internal/DebugConsole.hx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 17754e24..9afbb89a 100755 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -352,8 +352,13 @@ class DebugConsole extends Trait { if (selectedObject.name == "Scene") { selectedType = "(Scene)"; - var p = iron.Scene.active.world.probe; - p.raw.strength = ui.slider(Id.handle({value: p.raw.strength}), "Env Strength", 0.0, 5.0, true); + if (iron.Scene.active.world != null) { + var p = iron.Scene.active.world.probe; + p.raw.strength = ui.slider(Id.handle({value: p.raw.strength}), "Env Strength", 0.0, 5.0, true); + } + else { + ui.text("This scene has no world data to edit."); + } } else if (Std.is(selectedObject, iron.object.LightObject)) { selectedType = "(Light)"; From 08b690adf82be80ad1ee948320ba56a071e09896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 00:21:35 +0200 Subject: [PATCH 037/230] DebugConsole: Fix outliner look --- Sources/armory/trait/internal/DebugConsole.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 9afbb89a..3c589e24 100755 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -167,7 +167,7 @@ class DebugConsole extends Trait { if (currentObject.children.length > 0) { ui.row([1 / 13, 12 / 13]); - b = ui.panel(listHandle.nest(lineCounter, {selected: true}), "", true); + b = ui.panel(listHandle.nest(lineCounter, {selected: true}), "", true, false, false); ui.text(currentObject.name); } else { From 6deedd7b350dc2092da1db5a650318c0b1dc7c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:19:07 +0200 Subject: [PATCH 038/230] Add type annotations to export_object() --- blender/arm/exporter.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 128ba63a..2acfc8ea 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -14,11 +14,12 @@ http://creativecommons.org/licenses/by-sa/3.0/deed.en_US import math import os import time +from typing import Any, Dict import numpy as np -from mathutils import * import bpy +from mathutils import * import arm.assets as assets import arm.exporter_opt as exporter_opt @@ -571,11 +572,13 @@ class ArmoryExporter: # self.indentLevel -= 1 # self.IndentWrite(B"}\n") - def export_object(self, bobject, scene, parento=None): - # This function exports a single object in the scene and includes its name, - # object reference, material references (for meshes), and transform. - # Subobjects are then exported recursively. - if self.preprocess_object(bobject) == False: + def export_object(self, bobject: bpy.types.Object, scene: bpy.types.Scene, + parent_export_data: Dict = None) -> None: + """This function exports a single object in the scene and + includes its name, object reference, material references (for + meshes), and transform. + Subobjects are then exported recursively. + """ return bobjectRef = self.bobjectArray.get(bobject) From f836e9a1a4dcf0263175537088a8038fa1e9ef6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:25:26 +0200 Subject: [PATCH 039/230] export_object(): pep8 cleanup --- blender/arm/exporter.py | 168 +++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 2acfc8ea..8cc9375a 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -579,80 +579,81 @@ class ArmoryExporter: meshes), and transform. Subobjects are then exported recursively. """ + if not self.preprocess_object(bobject): return - bobjectRef = self.bobjectArray.get(bobject) - if bobjectRef: - type = bobjectRef["objectType"] + bobject_ref = self.bobjectArray.get(bobject) + if bobject_ref is not None: + object_type = bobject_ref["objectType"] # Linked object, not present in scene if bobject not in self.objectToArmObjectDict: - o = {} - o['traits'] = [] - o['spawn'] = False - self.objectToArmObjectDict[bobject] = o + object_export_data = {} # type: Dict[str, Any] + object_export_data['traits'] = [] + object_export_data['spawn'] = False + self.objectToArmObjectDict[bobject] = object_export_data - o = self.objectToArmObjectDict[bobject] - o['type'] = structIdentifier[type] - o['name'] = bobjectRef["structName"] + object_export_data = self.objectToArmObjectDict[bobject] + object_export_data['type'] = structIdentifier[object_type] + object_export_data['name'] = bobject_ref["structName"] if bobject.parent_type == "BONE": - o['parent_bone'] = bobject.parent_bone + object_export_data['parent_bone'] = bobject.parent_bone - if bobject.hide_render or bobject.arm_visible == False: - o['visible'] = False + if bobject.hide_render or not bobject.arm_visible: + object_export_data['visible'] = False if not bobject.cycles_visibility.camera: - o['visible_mesh'] = False + object_export_data['visible_mesh'] = False if not bobject.cycles_visibility.shadow: - o['visible_shadow'] = False + object_export_data['visible_shadow'] = False - if bobject.arm_spawn == False: - o['spawn'] = False + if not bobject.arm_spawn: + object_export_data['spawn'] = False - o['mobile'] = bobject.arm_mobile + object_export_data['mobile'] = bobject.arm_mobile if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: - o['group_ref'] = bobject.instance_collection.name + object_export_data['group_ref'] = bobject.instance_collection.name if bobject.arm_tilesheet != '': - o['tilesheet_ref'] = bobject.arm_tilesheet - o['tilesheet_action_ref'] = bobject.arm_tilesheet_action + object_export_data['tilesheet_ref'] = bobject.arm_tilesheet + object_export_data['tilesheet_action_ref'] = bobject.arm_tilesheet_action if len(bobject.arm_propertylist) > 0: - o['properties'] = [] + object_export_data['properties'] = [] for p in bobject.arm_propertylist: po = {} po['name'] = p.name_prop po['value'] = getattr(p, p.type_prop + '_prop') - o['properties'].append(po) + object_export_data['properties'].append(po) # TODO: layer_found = True - if layer_found == False: - o['spawn'] = False + if not layer_found: + object_export_data['spawn'] = False # Export the object reference and material references objref = bobject.data if objref is not None: objname = arm.utils.asset_name(objref) - # Lods + # LOD if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0: - o['lods'] = [] + object_export_data['lods'] = [] for l in objref.arm_lodlist: - if l.enabled_prop == False: + if not l.enabled_prop: continue lod = {} lod['object_ref'] = l.name lod['screen_size'] = l.screen_size_prop - o['lods'].append(lod) + object_export_data['lods'].append(lod) if objref.arm_lod_material: - o['lod_material'] = True + object_export_data['lod_material'] = True - if type == NodeTypeMesh: - if not objref in self.meshArray: + if object_type == NodeTypeMesh: + if objref not in self.meshArray: self.meshArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.meshArray[objref]["objectTable"].append(bobject) @@ -661,76 +662,81 @@ class ArmoryExporter: wrd = bpy.data.worlds['Arm'] if wrd.arm_single_data_file: - o['data_ref'] = oid + object_export_data['data_ref'] = oid else: ext = '' if not self.is_compress() else '.lz4' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - o['data_ref'] = 'mesh_' + oid + ext + '/' + oid + object_export_data['data_ref'] = 'mesh_' + oid + ext + '/' + oid - o['material_refs'] = [] + object_export_data['material_refs'] = [] for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) # Export ref - self.export_material_ref(bobject, mat, i, o) + self.export_material_ref(bobject, mat, i, object_export_data) # Decal flag if mat != None and mat.arm_decal: - o['type'] = 'decal_object' + object_export_data['type'] = 'decal_object' # No material, mimic cycles and assign default - if len(o['material_refs']) == 0: - self.use_default_material(bobject, o) + if len(object_export_data['material_refs']) == 0: + self.use_default_material(bobject, object_export_data) num_psys = len(bobject.particle_systems) if num_psys > 0: - o['particle_refs'] = [] + object_export_data['particle_refs'] = [] for i in range(0, num_psys): - self.export_particle_system_ref(bobject.particle_systems[i], i, o) + self.export_particle_system_ref(bobject.particle_systems[i], i, object_export_data) aabb = bobject.data.arm_aabb if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: self.calc_aabb(bobject) - o['dimensions'] = [aabb[0], aabb[1], aabb[2]] + object_export_data['dimensions'] = [aabb[0], aabb[1], aabb[2]] - #shapeKeys = ArmoryExporter.get_shape_keys(objref) - #if shapeKeys: - # self.ExportMorphWeights(bobject, shapeKeys, scene, o) + # shapeKeys = ArmoryExporter.get_shape_keys(objref) + # if shapeKeys: + # self.ExportMorphWeights(bobject, shapeKeys, scene, object_export_data) - elif type == NodeTypeLight: + elif object_type == NodeTypeLight: if objref not in self.lightArray: self.lightArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.lightArray[objref]["objectTable"].append(bobject) - o['data_ref'] = self.lightArray[objref]["structName"] + object_export_data['data_ref'] = self.lightArray[objref]["structName"] - elif type == NodeTypeProbe: + elif object_type == NodeTypeProbe: if objref not in self.probeArray: self.probeArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.probeArray[objref]["objectTable"].append(bobject) - dist = bobject.data.influence_distance - if objref.type == "PLANAR": - o['dimensions'] = [1.0, 1.0, dist] - else: # GRID, CUBEMAP - o['dimensions'] = [dist, dist, dist] - o['data_ref'] = self.probeArray[objref]["structName"] - elif type == NodeTypeCamera: - if 'spawn' in o and not o['spawn']: + dist = bobject.data.influence_distance + + if objref.type == "PLANAR": + object_export_data['dimensions'] = [1.0, 1.0, dist] + + # GRID, CUBEMAP + else: + object_export_data['dimensions'] = [dist, dist, dist] + object_export_data['data_ref'] = self.probeArray[objref]["structName"] + + elif object_type == NodeTypeCamera: + if 'spawn' in object_export_data and not object_export_data['spawn']: self.camera_spawned |= False else: self.camera_spawned = True + if objref not in self.cameraArray: self.cameraArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.cameraArray[objref]["objectTable"].append(bobject) - o['data_ref'] = self.cameraArray[objref]["structName"] + object_export_data['data_ref'] = self.cameraArray[objref]["structName"] - elif type == NodeTypeSpeaker: + elif object_type == NodeTypeSpeaker: if objref not in self.speakerArray: self.speakerArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.speakerArray[objref]["objectTable"].append(bobject) - o['data_ref'] = self.speakerArray[objref]["structName"] + object_export_data['data_ref'] = self.speakerArray[objref]["structName"] # Export the transform. If object is animated, then animation tracks are exported here if bobject.type != 'ARMATURE' and bobject.animation_data is not None: @@ -746,28 +752,28 @@ class ArmoryExporter: orig_action = action for a in export_actions: bobject.animation_data.action = a - self.export_object_transform(bobject, o) + self.export_object_transform(bobject, object_export_data) if len(export_actions) >= 2 and export_actions[0] is None: # No action assigned - o['object_actions'].insert(0, 'null') + object_export_data['object_actions'].insert(0, 'null') bobject.animation_data.action = orig_action else: - self.export_object_transform(bobject, o) + self.export_object_transform(bobject, object_export_data) # If the object is parented to a bone and is not relative, then undo the bone's transform if bobject.parent_type == "BONE": armature = bobject.parent.data bone = armature.bones[bobject.parent_bone] # if not bone.use_relative_parent: - o['parent_bone_connected'] = bone.use_connect + object_export_data['parent_bone_connected'] = bone.use_connect if bone.use_connect: bone_translation = Vector((0, bone.length, 0)) + bone.head - o['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + object_export_data['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] else: bone_translation = bone.tail - bone.head - o['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + object_export_data['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] pose_bone = bobject.parent.pose.bones[bobject.parent_bone] bone_translation_pose = pose_bone.tail - pose_bone.head - o['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] + object_export_data['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] if bobject.type == 'ARMATURE' and bobject.data is not None: bdata = bobject.data # Armature data @@ -803,18 +809,18 @@ class ArmoryExporter: ext = '.lz4' if self.is_compress() else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - o['bone_actions'] = [] + object_export_data['bone_actions'] = [] for action in export_actions: aname = arm.utils.safestr(arm.utils.asset_name(action)) - o['bone_actions'].append('action_' + armatureid + '_' + aname + ext) + object_export_data['bone_actions'].append('action_' + armatureid + '_' + aname + ext) clear_op = set() skelobj = bobject baked_actions = [] orig_action = bobject.animation_data.action if bdata.arm_autobake and bobject.name not in bpy.context.collection.all_objects: - clear_op.add( 'unlink' ) - #clone bjobject and put it in the current scene so the bake operator can run + clear_op.add('unlink') + # Clone bjobject and put it in the current scene so the bake operator can run if bobject.library is not None: skelobj = bobject.copy() clear_op.add('rem') @@ -825,16 +831,18 @@ class ArmoryExporter: skelobj.animation_data.action = action fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=self.is_compress()) assets.add(fp) - if bdata.arm_cached == False or not os.path.exists(fp): + if not bdata.arm_cached or not os.path.exists(fp): #handle autobake if bdata.arm_autobake: sel = bpy.context.selected_objects[:] - for _o in sel: _o.select_set(False) + for _o in sel: + _o.select_set(False) skelobj.select_set(True) bpy.ops.nla.bake(frame_start = action.frame_range[0], frame_end=action.frame_range[1], step=1, only_selected=False, visual_keying=True) action = skelobj.animation_data.action skelobj.select_set(False) - for _o in sel: _o.select_set(True) + for _o in sel: + _o.select_set(True) baked_actions.append(action) wrd = bpy.data.worlds['Arm'] @@ -865,19 +873,19 @@ class ArmoryExporter: # TODO: cache per action bdata.arm_cached = True - if parento is None: - self.output['objects'].append(o) + if parent_export_data is None: + self.output['objects'].append(object_export_data) else: - parento['children'].append(o) + parent_export_data['children'].append(object_export_data) - self.post_export_object(bobject, o, type) + self.post_export_object(bobject, object_export_data, object_type) - if not hasattr(o, 'children') and len(bobject.children) > 0: - o['children'] = [] + if not hasattr(object_export_data, 'children') and len(bobject.children) > 0: + object_export_data['children'] = [] if bobject.arm_instanced == 'Off': for subbobject in bobject.children: - self.export_object(subbobject, scene, o) + self.export_object(subbobject, scene, object_export_data) def export_skin(self, bobject, armature, exportMesh, o): # This function exports all skinning data, which includes the skeleton From eae4747bce88f0ed75107d6adab4fe9b3b6da414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:40:18 +0200 Subject: [PATCH 040/230] Replace NodeType[...] with enum --- blender/arm/exporter.py | 70 ++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 8cc9375a..e796c442 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -11,10 +11,11 @@ This software is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License: http://creativecommons.org/licenses/by-sa/3.0/deed.en_US """ +from enum import Enum, unique import math import os import time -from typing import Any, Dict +from typing import Any, Dict, Union import numpy as np @@ -30,21 +31,26 @@ import arm.material.make as make_material import arm.material.mat_batch as mat_batch import arm.utils -NodeTypeEmpty = 0 -NodeTypeBone = 1 -NodeTypeMesh = 2 -NodeTypeLight = 3 -NodeTypeCamera = 4 -NodeTypeSpeaker = 5 -NodeTypeDecal = 6 -NodeTypeProbe = 7 + +@unique +class NodeType(Enum): + """Represents the type of an object.""" + EMPTY = 0 + BONE = 1 + MESH = 2 + LIGHT = 3 + CAMERA = 4 + SPEAKER = 5 + DECAL = 6 + PROBE = 7 + AnimationTypeSampled = 0 AnimationTypeLinear = 1 AnimationTypeBezier = 2 AnimationTypeConstant = 3 ExportEpsilon = 1.0e-6 -structIdentifier = ["object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object"] +struct_identifier = ["object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object"] current_output = None class ArmoryExporter: @@ -65,23 +71,21 @@ class ArmoryExporter: return mesh_fp + object_id + ext @staticmethod - def get_bobject_type(bobject): + def get_bobject_type(bobject: bpy.types.Object) -> NodeType: if bobject.type == "MESH": if bobject.data.polygons: - return NodeTypeMesh - elif bobject.type == "FONT": - return NodeTypeMesh - elif bobject.type == "META": - return NodeTypeMesh + return NodeType.MESH + elif bobject.type == "FONT" or bobject.type == "META": + return NodeType.MESH elif bobject.type == "LIGHT": - return NodeTypeLight + return NodeType.LIGHT elif bobject.type == "CAMERA": - return NodeTypeCamera + return NodeType.CAMERA elif bobject.type == "SPEAKER": - return NodeTypeSpeaker + return NodeType.SPEAKER elif bobject.type == "LIGHT_PROBE": - return NodeTypeProbe - return NodeTypeEmpty + return NodeType.PROBE + return NodeType.EMPTY @staticmethod def get_shape_keys(mesh): @@ -115,7 +119,7 @@ class ArmoryExporter: bobjectRef = self.bobjectBoneArray.get(bone) if bobjectRef: - o['type'] = structIdentifier[bobjectRef["objectType"]] + o['type'] = struct_identifier[bobjectRef["objectType"].value] o['name'] = bobjectRef["structName"] self.export_bone_transform(armature, bone, scene, o, action) @@ -327,7 +331,7 @@ class ArmoryExporter: if ArmoryExporter.export_all_flag or bobject.select: btype = ArmoryExporter.get_bobject_type(bobject) - if btype != NodeTypeMesh and ArmoryExporter.option_mesh_only: + if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only: return self.bobjectArray[bobject] = { @@ -348,14 +352,14 @@ class ArmoryExporter: def process_skinned_meshes(self): for bobjectRef in self.bobjectArray.items(): - if bobjectRef[1]["objectType"] == NodeTypeMesh: + if bobjectRef[1]["objectType"] is NodeType.MESH: armature = bobjectRef[0].find_armature() if armature: for bone in armature.data.bones: boneRef = self.find_bone(bone.name) if boneRef: # If an object is used as a bone, then we force its type to be a bone - boneRef[1]["objectType"] = NodeTypeBone + boneRef[1]["objectType"] = NodeType.BONE def export_bone_transform(self, armature, bone, scene, o, action): @@ -594,7 +598,7 @@ class ArmoryExporter: self.objectToArmObjectDict[bobject] = object_export_data object_export_data = self.objectToArmObjectDict[bobject] - object_export_data['type'] = structIdentifier[object_type] + object_export_data['type'] = struct_identifier[object_type.value] object_export_data['name'] = bobject_ref["structName"] if bobject.parent_type == "BONE": @@ -652,7 +656,7 @@ class ArmoryExporter: if objref.arm_lod_material: object_export_data['lod_material'] = True - if object_type == NodeTypeMesh: + if object_type is NodeType.MESH: if objref not in self.meshArray: self.meshArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: @@ -696,14 +700,14 @@ class ArmoryExporter: # if shapeKeys: # self.ExportMorphWeights(bobject, shapeKeys, scene, object_export_data) - elif object_type == NodeTypeLight: + elif object_type is NodeType.LIGHT: if objref not in self.lightArray: self.lightArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.lightArray[objref]["objectTable"].append(bobject) object_export_data['data_ref'] = self.lightArray[objref]["structName"] - elif object_type == NodeTypeProbe: + elif object_type is NodeType.PROBE: if objref not in self.probeArray: self.probeArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: @@ -719,7 +723,7 @@ class ArmoryExporter: object_export_data['dimensions'] = [dist, dist, dist] object_export_data['data_ref'] = self.probeArray[objref]["structName"] - elif object_type == NodeTypeCamera: + elif object_type is NodeType.CAMERA: if 'spawn' in object_export_data and not object_export_data['spawn']: self.camera_spawned |= False else: @@ -731,7 +735,7 @@ class ArmoryExporter: self.cameraArray[objref]["objectTable"].append(bobject) object_export_data['data_ref'] = self.cameraArray[objref]["structName"] - elif object_type == NodeTypeSpeaker: + elif object_type is NodeType.SPEAKER: if objref not in self.speakerArray: self.speakerArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: @@ -2424,9 +2428,9 @@ class ArmoryExporter: self.add_rigidbody_constraint(o, rbc) # Camera traits - if type == NodeTypeCamera: + if type is NodeType.CAMERA: # Viewport camera enabled, attach navigation to active camera - if self.scene.camera != None and bobject.name == self.scene.camera.name and bpy.data.worlds['Arm'].arm_play_camera != 'Scene': + if self.scene.camera is not None and bobject.name == self.scene.camera.name and bpy.data.worlds['Arm'].arm_play_camera != 'Scene': navigation_trait = {} navigation_trait['type'] = 'Script' navigation_trait['class_name'] = 'armory.trait.WalkNavigation' From 8d23458f92a24b452ea099efd46c46ec2242e95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:42:52 +0200 Subject: [PATCH 041/230] Fix process_bone() --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index e796c442..50dcc510 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -316,7 +316,7 @@ class ArmoryExporter: def process_bone(self, bone): if ArmoryExporter.export_all_flag or bone.select: - self.bobjectBoneArray[bone] = {"objectType" : NodeTypeBone, "structName" : bone.name} + self.bobjectBoneArray[bone] = {"objectType" : NodeType.BONE, "structName" : bone.name} for subbobject in bone.children: self.process_bone(subbobject) From 0a232417354c0e734ddfca288d87ca5788fec92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:50:36 +0200 Subject: [PATCH 042/230] Remove unused constants --- blender/arm/exporter.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 50dcc510..bb679d73 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -44,11 +44,6 @@ class NodeType(Enum): DECAL = 6 PROBE = 7 -AnimationTypeSampled = 0 -AnimationTypeLinear = 1 -AnimationTypeBezier = 2 -AnimationTypeConstant = 3 -ExportEpsilon = 1.0e-6 struct_identifier = ["object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object"] current_output = None From 6daf501eebff1f5509cbbe374556ac3bae3f6c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:51:33 +0200 Subject: [PATCH 043/230] Move get_bobject_type() into NodeTyp enum --- blender/arm/exporter.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index bb679d73..fdfbd493 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -44,10 +44,30 @@ class NodeType(Enum): DECAL = 6 PROBE = 7 + @classmethod + def get_bobject_type(cls, bobject: bpy.types.Object) -> "NodeType": + """Returns the NodeType enum member belonging to the type of + the given blender object.""" + if bobject.type == "MESH": + if bobject.data.polygons: + return cls.MESH + elif bobject.type == "FONT" or bobject.type == "META": + return cls.MESH + elif bobject.type == "LIGHT": + return cls.LIGHT + elif bobject.type == "CAMERA": + return cls.CAMERA + elif bobject.type == "SPEAKER": + return cls.SPEAKER + elif bobject.type == "LIGHT_PROBE": + return cls.PROBE + return cls.EMPTY + struct_identifier = ["object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object"] current_output = None + class ArmoryExporter: '''Export to Armory format''' @@ -65,23 +85,6 @@ class ArmoryExporter: ext = '.lz4' if compressed else '.arm' return mesh_fp + object_id + ext - @staticmethod - def get_bobject_type(bobject: bpy.types.Object) -> NodeType: - if bobject.type == "MESH": - if bobject.data.polygons: - return NodeType.MESH - elif bobject.type == "FONT" or bobject.type == "META": - return NodeType.MESH - elif bobject.type == "LIGHT": - return NodeType.LIGHT - elif bobject.type == "CAMERA": - return NodeType.CAMERA - elif bobject.type == "SPEAKER": - return NodeType.SPEAKER - elif bobject.type == "LIGHT_PROBE": - return NodeType.PROBE - return NodeType.EMPTY - @staticmethod def get_shape_keys(mesh): if not hasattr(mesh, 'shape_keys'): # Metaball @@ -324,7 +327,7 @@ class ArmoryExporter: after an "_". """ if ArmoryExporter.export_all_flag or bobject.select: - btype = ArmoryExporter.get_bobject_type(bobject) + btype = NodeType.get_bobject_type(bobject) if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only: return From 4a2ed852b5da905e8f36426b6489143492cfb9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:52:29 +0200 Subject: [PATCH 044/230] Rename struct_identifier + change it to tuple to make it clear it is a constant --- blender/arm/exporter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index fdfbd493..f94e318a 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -64,7 +64,10 @@ class NodeType(Enum): return cls.EMPTY -struct_identifier = ["object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object"] +STRUCT_IDENTIFIER = ("object", "bone_object", "mesh_object", + "light_object", "camera_object", "speaker_object", + "decal_object", "probe_object") + current_output = None @@ -117,7 +120,7 @@ class ArmoryExporter: bobjectRef = self.bobjectBoneArray.get(bone) if bobjectRef: - o['type'] = struct_identifier[bobjectRef["objectType"].value] + o['type'] = STRUCT_IDENTIFIER[bobjectRef["objectType"].value] o['name'] = bobjectRef["structName"] self.export_bone_transform(armature, bone, scene, o, action) @@ -596,7 +599,7 @@ class ArmoryExporter: self.objectToArmObjectDict[bobject] = object_export_data object_export_data = self.objectToArmObjectDict[bobject] - object_export_data['type'] = struct_identifier[object_type.value] + object_export_data['type'] = STRUCT_IDENTIFIER[object_type.value] object_export_data['name'] = bobject_ref["structName"] if bobject.parent_type == "BONE": From 37e4bc9f85a26929c742cf62c4d58951c82db083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Apr 2020 23:53:03 +0200 Subject: [PATCH 045/230] Some smaller style improvements --- blender/arm/exporter.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index f94e318a..b5ae17e2 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -90,14 +90,16 @@ class ArmoryExporter: @staticmethod def get_shape_keys(mesh): - if not hasattr(mesh, 'shape_keys'): # Metaball + # Metaball + if not hasattr(mesh, 'shape_keys'): return None + shape_keys = mesh.shape_keys if shape_keys and len(shape_keys.key_blocks) > 1: return shape_keys return None - def find_bone(self, name): + def find_bone(self, name: str): for bobject_ref in self.bobjectBoneArray.items(): if bobject_ref[0].name == name: return bobject_ref @@ -751,7 +753,7 @@ class ArmoryExporter: if track.strips is None: continue for strip in track.strips: - if strip.action == None or strip.action in export_actions: + if strip.action is None or strip.action in export_actions: continue export_actions.append(strip.action) orig_action = action @@ -1901,7 +1903,7 @@ class ArmoryExporter: global current_output profile_time = time.time() - self.scene = context.scene if scene == None else scene + self.scene = context.scene if scene is None else scene current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe print('Exporting ' + arm.utils.asset_name(self.scene)) @@ -1920,10 +1922,12 @@ class ArmoryExporter: self.speakerArray = {} self.materialArray = [] self.particleSystemArray = {} - self.worldArray = {} # Export all worlds + # Export all worlds + self.worldArray = {} self.boneParentArray = {} self.materialToObjectDict = dict() - self.defaultMaterialObjects = [] # If no material is assigned, provide default to mimic cycles + # If no material is assigned, provide default to mimic cycles + self.defaultMaterialObjects = [] self.defaultSkinMaterialObjects = [] self.defaultPartMaterialObjects = [] self.materialToArmObjectDict = dict() @@ -1933,7 +1937,7 @@ class ArmoryExporter: # for i in range(0, len(self.scene.view_layers)): # if self.scene.view_layers[i] == True: # self.active_layers.append(i) - self.depsgraph = context.evaluated_depsgraph_get() if depsgraph == None else depsgraph + self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph self.preprocess() # scene_objects = [] From a18c87c39285d1ae1cd9394a704cf4ea0c95fd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 00:23:03 +0200 Subject: [PATCH 046/230] Make write_matrix static --- blender/arm/exporter.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b5ae17e2..426510ff 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -72,8 +72,9 @@ current_output = None class ArmoryExporter: - '''Export to Armory format''' + """Export to Armory format""" + @staticmethod def write_matrix(self, matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], @@ -184,7 +185,7 @@ class ArmoryExporter: for i in range(begin_frame, end_frame): scene.frame_set(i) - tracko['values'] += self.write_matrix(bobject.matrix_local) # Continuos array of matrix transforms + tracko['values'] += ArmoryExporter.write_matrix(bobject.matrix_local) # Continuos array of matrix transforms oanim['tracks'] = [tracko] self.export_pose_markers(oanim, action) @@ -254,7 +255,7 @@ class ArmoryExporter: # Static transform o['transform'] = {} - o['transform']['values'] = self.write_matrix(bobject.matrix_local) + o['transform']['values'] = ArmoryExporter.write_matrix(bobject.matrix_local) # Animated transform if bobject.animation_data is not None and bobject.type != "ARMATURE": @@ -377,7 +378,7 @@ class ArmoryExporter: transform = (bone.parent.matrix_local.inverted_safe() @ transform) o['transform'] = {} - o['transform']['values'] = self.write_matrix(transform) + o['transform']['values'] = ArmoryExporter.write_matrix(transform) curve_array = self.collect_bone_animation(armature, bone.name) animation = len(curve_array) != 0 @@ -475,9 +476,9 @@ class ArmoryExporter: values, pose_bone = track[0], track[1] parent = pose_bone.parent if parent: - values += self.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix)) + values += ArmoryExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix)) else: - values += self.write_matrix(pose_bone.matrix) + values += ArmoryExporter.write_matrix(pose_bone.matrix) # print('Bone matrices exported in ' + str(time.time() - profile_time)) def has_baked_material(self, bobject, materials): @@ -903,7 +904,7 @@ class ArmoryExporter: # Write the skin bind pose transform otrans = {} oskin['transform'] = otrans - otrans['values'] = self.write_matrix(bobject.matrix_world) + otrans['values'] = ArmoryExporter.write_matrix(bobject.matrix_world) bone_array = armature.data.bones bone_count = len(bone_array) @@ -930,7 +931,7 @@ class ArmoryExporter: for i in range(bone_count): skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() skeletonI = (skeletonI @ bobject.matrix_world) - oskin['transformsI'].append(self.write_matrix(skeletonI)) + oskin['transformsI'].append(ArmoryExporter.write_matrix(skeletonI)) # Export the per-vertex bone influence data group_remap = [] @@ -2223,7 +2224,7 @@ class ArmoryExporter: o['transform'] = {} viewport_matrix = self.get_viewport_view_matrix() if viewport_matrix is not None: - o['transform']['values'] = self.write_matrix(viewport_matrix.inverted_safe()) + o['transform']['values'] = ArmoryExporter.write_matrix(viewport_matrix.inverted_safe()) o['local_only'] = True else: o['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] From 6e3f3dca98951200f7782a83dee31a725385651e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 00:30:16 +0200 Subject: [PATCH 047/230] More type annotations --- blender/arm/exporter.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 426510ff..a893caf1 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -81,11 +81,13 @@ class ArmoryExporter: matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3], matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]] - def get_meshes_file_path(self, object_id, compressed=False): + def get_meshes_file_path(self, object_id: str, compressed=False) -> str: index = self.filepath.rfind('/') mesh_fp = self.filepath[:(index + 1)] + 'meshes/' + if not os.path.exists(mesh_fp): os.makedirs(mesh_fp) + ext = '.lz4' if compressed else '.arm' return mesh_fp + object_id + ext @@ -142,8 +144,8 @@ class ArmoryExporter: oanim['marker_frames'].append(int(m.frame)) oanim['marker_names'].append(m.name) - def export_object_sampled_animation(self, bobject, scene, o): - # This function exports animation as full 4x4 matrices for each frame + def export_object_sampled_animation(self, bobject: bpy.types.Object,scene: bpy.types.Scene, o: Dict) -> None: + """Exports animation as full 4x4 matrices for each frame""" animation_flag = False animation_flag = bobject.animation_data != None and bobject.animation_data.action != None and bobject.type != 'ARMATURE' @@ -1913,7 +1915,9 @@ class ArmoryExporter: current_output = self.output self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) self.filepath = filepath - self.bobjectArray = {} + # Stores the object type ("objectType") and the asset name + # ("structName") in a dict for each object + self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str], Union[NodeType, str]] self.bobjectBoneArray = {} self.meshArray = {} self.lightArray = {} @@ -1932,7 +1936,9 @@ class ArmoryExporter: self.defaultSkinMaterialObjects = [] self.defaultPartMaterialObjects = [] self.materialToArmObjectDict = dict() - self.objectToArmObjectDict = dict() + # Stores the link between a blender object and its + # corresponding export data (arm object) + self.objectToArmObjectDict = dict() # type: Dict[bpy.types.Object, Dict] self.bone_tracks = [] # self.active_layers = [] # for i in range(0, len(self.scene.view_layers)): From d7e6ec1a7967813df90f997ec1da45b4c217bedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 00:48:49 +0200 Subject: [PATCH 048/230] Fix write_matrix() --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index a893caf1..cfe64732 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -75,7 +75,7 @@ class ArmoryExporter: """Export to Armory format""" @staticmethod - def write_matrix(self, matrix): + def write_matrix(matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3], From f5b37f99aa2e26ed499a655b848885f4c701340e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 00:56:16 +0200 Subject: [PATCH 049/230] Transform exporter into class (one instance per scene) This has two big advantages: - One place for variable initialization (and less warnings from pylint etc.) - No errors due to forgotten cleanups because every scene export now has its own class instance --- blender/arm/exporter.py | 101 +++++++++++++++++++++------------------- blender/arm/make.py | 6 +-- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index cfe64732..6aaa55f9 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -74,6 +74,55 @@ current_output = None class ArmoryExporter: """Export to Armory format""" + def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene=None, depsgraph:bpy.types.Depsgraph=None): + global current_output + + self.output = {} + current_output = self.output + + self.filepath = filepath + self.scene = context.scene if scene is None else scene + self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph + + self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) + + # Stores the object type ("objectType") and the asset name + # ("structName") in a dict for each object + self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str], Union[NodeType, str]] + self.bobjectBoneArray = {} + self.meshArray = {} + self.lightArray = {} + self.probeArray = {} + self.cameraArray = {} + self.speakerArray = {} + self.materialArray = [] + self.particleSystemArray = {} + # Export all worlds + self.worldArray = {} + self.boneParentArray = {} + + # `True` if there is at least one spawned camera in the scene + self.camera_spawned = False + + self.materialToObjectDict = dict() + # If no material is assigned, provide default to mimic cycles + self.defaultMaterialObjects = [] + self.defaultSkinMaterialObjects = [] + self.defaultPartMaterialObjects = [] + self.materialToArmObjectDict = dict() + # Stores the link between a blender object and its + # corresponding export data (arm object) + self.objectToArmObjectDict = dict() # type: Dict[bpy.types.Object, Dict] + + self.bone_tracks = [] + + self.preprocess() + + @classmethod + def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene=None, depsgraph: bpy.types.Depsgraph=None) -> None: + """Exports the given scene to the given filepath.""" + cls(context, filepath, scene, depsgraph).execute() + @staticmethod def write_matrix(matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], @@ -1902,56 +1951,14 @@ class ArmoryExporter: for mesh_ref in self.meshArray.items(): self.export_mesh(mesh_ref, scene) - def execute(self, context, filepath, scene=None, depsgraph=None): - global current_output + def execute(self): + """Exports the scene.""" profile_time = time.time() - - self.scene = context.scene if scene is None else scene - current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe - print('Exporting ' + arm.utils.asset_name(self.scene)) - self.output = {} - current_output = self.output - self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) - self.filepath = filepath - # Stores the object type ("objectType") and the asset name - # ("structName") in a dict for each object - self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str], Union[NodeType, str]] - self.bobjectBoneArray = {} - self.meshArray = {} - self.lightArray = {} - self.probeArray = {} - self.cameraArray = {} - self.camera_spawned = False - self.speakerArray = {} - self.materialArray = [] - self.particleSystemArray = {} - # Export all worlds - self.worldArray = {} - self.boneParentArray = {} - self.materialToObjectDict = dict() - # If no material is assigned, provide default to mimic cycles - self.defaultMaterialObjects = [] - self.defaultSkinMaterialObjects = [] - self.defaultPartMaterialObjects = [] - self.materialToArmObjectDict = dict() - # Stores the link between a blender object and its - # corresponding export data (arm object) - self.objectToArmObjectDict = dict() # type: Dict[bpy.types.Object, Dict] - self.bone_tracks = [] - # self.active_layers = [] - # for i in range(0, len(self.scene.view_layers)): - # if self.scene.view_layers[i] == True: - # self.active_layers.append(i) - self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph - self.preprocess() + current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe - # scene_objects = [] - # for lay in self.scene.view_layers: - # scene_objects += lay.objects scene_objects = self.scene.collection.all_objects.values() - for bobject in scene_objects: # Map objects to game objects o = {} @@ -2198,8 +2205,8 @@ class ArmoryExporter: bpy.data.materials.remove(mat, do_unlink=True) # Restore frame - if scene.frame_current != current_frame: - scene.frame_set(current_frame, subframe=current_subframe) + if self.scene.frame_current != current_frame: + self.scene.frame_set(current_frame, subframe=current_subframe) print('Scene exported in ' + str(time.time() - profile_time)) return {'FINISHED'} diff --git a/blender/arm/make.py b/blender/arm/make.py index 36748d3f..b5f35604 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -21,7 +21,6 @@ import arm.lib.make_datas import arm.lib.server from arm.exporter import ArmoryExporter -exporter = ArmoryExporter() scripts_mtime = 0 # Monitor source changes profile_time = 0 @@ -57,7 +56,6 @@ def remove_readonly(func, path, excinfo): func(path) def export_data(fp, sdk_path): - global exporter wrd = bpy.data.worlds['Arm'] print('\n' + '_' * 10 + ' [Armory] Compiling ' + '_' * 10) @@ -121,7 +119,7 @@ def export_data(fp, sdk_path): if scene.arm_export: ext = '.lz4' if ArmoryExporter.compress_enabled else '.arm' asset_path = build_dir + '/compiled/Assets/' + arm.utils.safestr(scene.name) + ext - exporter.execute(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) + ArmoryExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) if ArmoryExporter.export_physics: physics_found = True if ArmoryExporter.export_navigation: @@ -429,7 +427,7 @@ def patch(): fp = arm.utils.get_fp() os.chdir(fp) asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm' - exporter.execute(bpy.context, asset_path, scene=bpy.context.scene) + ArmoryExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene) if not os.path.isdir(arm.utils.build_dir() + '/compiled/Shaders/std'): raw_shaders_path = arm.utils.get_sdk_path() + '/armory/Shaders/' shutil.copytree(raw_shaders_path + 'std', arm.utils.build_dir() + '/compiled/Shaders/std') From 5859b11c2ec731c06015cd3b9b95b8e38762472c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 00:57:54 +0200 Subject: [PATCH 050/230] Fix type annotation --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 6aaa55f9..8a1eb48c 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -88,7 +88,7 @@ class ArmoryExporter: # Stores the object type ("objectType") and the asset name # ("structName") in a dict for each object - self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str], Union[NodeType, str]] + self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]] self.bobjectBoneArray = {} self.meshArray = {} self.lightArray = {} From 0e4c9f9f1808314067c6f61b85e4639c6124f975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:03:41 +0200 Subject: [PATCH 051/230] Improve docstring --- blender/arm/exporter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 8a1eb48c..d4da3a23 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -120,7 +120,9 @@ class ArmoryExporter: @classmethod def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene=None, depsgraph: bpy.types.Depsgraph=None) -> None: - """Exports the given scene to the given filepath.""" + """Exports the given scene to the given filepath. This is the + function that is called in make.py and the entry point of the + exporter.""" cls(context, filepath, scene, depsgraph).execute() @staticmethod From caee1a87a2edc5d6cf240bb4986621174d7f416a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:09:34 +0200 Subject: [PATCH 052/230] Small performance improvement --- blender/arm/make.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index b5f35604..daf6ef34 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -433,15 +433,11 @@ def patch(): shutil.copytree(raw_shaders_path + 'std', arm.utils.build_dir() + '/compiled/Shaders/std') node_path = arm.utils.get_node_path() khamake_path = arm.utils.get_khamake_path() + cmd = [node_path, khamake_path, 'krom'] - cmd.append('--shaderversion') - cmd.append('330') - cmd.append('--parallelAssetConversion') - cmd.append('4') - cmd.append('--to') - cmd.append(arm.utils.build_dir() + '/debug') - cmd.append('--nohaxe') - cmd.append('--noproject') + cmd.extend(('--shaderversion', '330', '--parallelAssetConversion', '4', + '--to', arm.utils.build_dir() + '/debug', '--nohaxe', '--noproject')) + assets.invalidate_enabled = True state.proc_build = run_proc(cmd, patch_done) From 754cb501cac80d4acd8833878e4703c256b5fd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:18:10 +0200 Subject: [PATCH 053/230] snake_case for ArmoryExporter member variables Also removed some unused variables --- blender/arm/exporter.py | 184 ++++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 94 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index d4da3a23..cf2664ae 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -74,45 +74,41 @@ current_output = None class ArmoryExporter: """Export to Armory format""" - def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene=None, depsgraph:bpy.types.Depsgraph=None): + def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): global current_output - self.output = {} - current_output = self.output - self.filepath = filepath self.scene = context.scene if scene is None else scene self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph + self.output = {} self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) + current_output = self.output # Stores the object type ("objectType") and the asset name # ("structName") in a dict for each object - self.bobjectArray = {} # type: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]] - self.bobjectBoneArray = {} - self.meshArray = {} - self.lightArray = {} - self.probeArray = {} - self.cameraArray = {} - self.speakerArray = {} - self.materialArray = [] - self.particleSystemArray = {} - # Export all worlds - self.worldArray = {} - self.boneParentArray = {} + self.bobject_array = {} # type: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]]] + self.bobject_bone_array = {} + self.mesh_array = {} + self.light_array = {} + self.probe_array = {} + self.camera_array = {} + self.speaker_array = {} + self.material_array = [] + self.particle_system_array = {} # `True` if there is at least one spawned camera in the scene self.camera_spawned = False - self.materialToObjectDict = dict() + self.material_to_object_dict = {} # If no material is assigned, provide default to mimic cycles - self.defaultMaterialObjects = [] - self.defaultSkinMaterialObjects = [] - self.defaultPartMaterialObjects = [] - self.materialToArmObjectDict = dict() + self.default_material_objects = [] + self.default_skin_material_objects = [] + self.default_part_material_objects = [] + self.material_to_arm_object_dict = {} # Stores the link between a blender object and its # corresponding export data (arm object) - self.objectToArmObjectDict = dict() # type: Dict[bpy.types.Object, Dict] + self.object_to_arm_object_dict = {} # type: Dict[bpy.types.Object, Dict] self.bone_tracks = [] @@ -154,7 +150,7 @@ class ArmoryExporter: return None def find_bone(self, name: str): - for bobject_ref in self.bobjectBoneArray.items(): + for bobject_ref in self.bobject_bone_array.items(): if bobject_ref[0].name == name: return bobject_ref return None @@ -173,7 +169,7 @@ class ArmoryExporter: return curve_array def export_bone(self, armature, bone, scene, o, action): - bobjectRef = self.bobjectBoneArray.get(bone) + bobjectRef = self.bobject_bone_array.get(bone) if bobjectRef: o['type'] = STRUCT_IDENTIFIER[bobjectRef["objectType"].value] @@ -373,13 +369,13 @@ class ArmoryExporter: def process_bone(self, bone): if ArmoryExporter.export_all_flag or bone.select: - self.bobjectBoneArray[bone] = {"objectType" : NodeType.BONE, "structName" : bone.name} + self.bobject_bone_array[bone] = {"objectType" : NodeType.BONE, "structName" : bone.name} for subbobject in bone.children: self.process_bone(subbobject) def process_bobject(self, bobject): - """Adds the given blender object to the bobjectArray dict and + """Adds the given blender object to the bobject_array dict and stores its type and its name. If an object is linked, the name of its library is appended @@ -391,7 +387,7 @@ class ArmoryExporter: if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only: return - self.bobjectArray[bobject] = { + self.bobject_array[bobject] = { "objectType": btype, "structName": arm.utils.asset_name(bobject) } @@ -408,7 +404,7 @@ class ArmoryExporter: self.process_bobject(subbobject) def process_skinned_meshes(self): - for bobjectRef in self.bobjectArray.items(): + for bobjectRef in self.bobject_array.items(): if bobjectRef[1]["objectType"] is NodeType.MESH: armature = bobjectRef[0].find_armature() if armature: @@ -453,10 +449,10 @@ class ArmoryExporter: def use_default_material(self, bobject, o): if arm.utils.export_bone_data(bobject): o['material_refs'].append('armdefaultskin') - self.defaultSkinMaterialObjects.append(bobject) + self.default_skin_material_objects.append(bobject) else: o['material_refs'].append('armdefault') - self.defaultMaterialObjects.append(bobject) + self.default_material_objects.append(bobject) def use_default_material_part(self): # Particle object with no material assigned @@ -464,29 +460,29 @@ class ArmoryExporter: if ps.render_type != 'OBJECT' or ps.instance_object is None: continue po = ps.instance_object - if po not in self.objectToArmObjectDict: + if po not in self.object_to_arm_object_dict: continue - o = self.objectToArmObjectDict[po] - if len(o['material_refs']) > 0 and o['material_refs'][0] == 'armdefault' and po not in self.defaultPartMaterialObjects: - self.defaultPartMaterialObjects.append(po) + o = self.object_to_arm_object_dict[po] + if len(o['material_refs']) > 0 and o['material_refs'][0] == 'armdefault' and po not in self.default_part_material_objects: + self.default_part_material_objects.append(po) o['material_refs'] = ['armdefaultpart'] # Replace armdefault def export_material_ref(self, bobject, material, index, o): if material is None: # Use default for empty mat slots self.use_default_material(bobject, o) return - if not material in self.materialArray: - self.materialArray.append(material) + if not material in self.material_array: + self.material_array.append(material) o['material_refs'].append(arm.utils.asset_name(material)) def export_particle_system_ref(self, psys, index, o): - if psys.settings in self.particleSystemArray: # or not modifier.show_render: + if psys.settings in self.particle_system_array: # or not modifier.show_render: return if psys.settings.instance_object == None or psys.settings.render_type != 'OBJECT': return - self.particleSystemArray[psys.settings] = {"structName" : psys.settings.name} + self.particle_system_array[psys.settings] = {"structName" : psys.settings.name} pref = {} pref['name'] = psys.name pref['seed'] = psys.seed @@ -643,18 +639,18 @@ class ArmoryExporter: if not self.preprocess_object(bobject): return - bobject_ref = self.bobjectArray.get(bobject) + bobject_ref = self.bobject_array.get(bobject) if bobject_ref is not None: object_type = bobject_ref["objectType"] # Linked object, not present in scene - if bobject not in self.objectToArmObjectDict: + if bobject not in self.object_to_arm_object_dict: object_export_data = {} # type: Dict[str, Any] object_export_data['traits'] = [] object_export_data['spawn'] = False - self.objectToArmObjectDict[bobject] = object_export_data + self.object_to_arm_object_dict[bobject] = object_export_data - object_export_data = self.objectToArmObjectDict[bobject] + object_export_data = self.object_to_arm_object_dict[bobject] object_export_data['type'] = STRUCT_IDENTIFIER[object_type.value] object_export_data['name'] = bobject_ref["structName"] @@ -714,12 +710,12 @@ class ArmoryExporter: object_export_data['lod_material'] = True if object_type is NodeType.MESH: - if objref not in self.meshArray: - self.meshArray[objref] = {"structName" : objname, "objectTable" : [bobject]} + if objref not in self.mesh_array: + self.mesh_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: - self.meshArray[objref]["objectTable"].append(bobject) + self.mesh_array[objref]["objectTable"].append(bobject) - oid = arm.utils.safestr(self.meshArray[objref]["structName"]) + oid = arm.utils.safestr(self.mesh_array[objref]["structName"]) wrd = bpy.data.worlds['Arm'] if wrd.arm_single_data_file: @@ -758,17 +754,17 @@ class ArmoryExporter: # self.ExportMorphWeights(bobject, shapeKeys, scene, object_export_data) elif object_type is NodeType.LIGHT: - if objref not in self.lightArray: - self.lightArray[objref] = {"structName" : objname, "objectTable" : [bobject]} + if objref not in self.light_array: + self.light_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: - self.lightArray[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.lightArray[objref]["structName"] + self.light_array[objref]["objectTable"].append(bobject) + object_export_data['data_ref'] = self.light_array[objref]["structName"] elif object_type is NodeType.PROBE: - if objref not in self.probeArray: - self.probeArray[objref] = {"structName" : objname, "objectTable" : [bobject]} + if objref not in self.probe_array: + self.probe_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: - self.probeArray[objref]["objectTable"].append(bobject) + self.probe_array[objref]["objectTable"].append(bobject) dist = bobject.data.influence_distance @@ -778,7 +774,7 @@ class ArmoryExporter: # GRID, CUBEMAP else: object_export_data['dimensions'] = [dist, dist, dist] - object_export_data['data_ref'] = self.probeArray[objref]["structName"] + object_export_data['data_ref'] = self.probe_array[objref]["structName"] elif object_type is NodeType.CAMERA: if 'spawn' in object_export_data and not object_export_data['spawn']: @@ -786,18 +782,18 @@ class ArmoryExporter: else: self.camera_spawned = True - if objref not in self.cameraArray: - self.cameraArray[objref] = {"structName" : objname, "objectTable" : [bobject]} + if objref not in self.camera_array: + self.camera_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: - self.cameraArray[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.cameraArray[objref]["structName"] + self.camera_array[objref]["objectTable"].append(bobject) + object_export_data['data_ref'] = self.camera_array[objref]["structName"] elif object_type is NodeType.SPEAKER: - if objref not in self.speakerArray: - self.speakerArray[objref] = {"structName" : objname, "objectTable" : [bobject]} + if objref not in self.speaker_array: + self.speaker_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: - self.speakerArray[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.speakerArray[objref]["structName"] + self.speaker_array[objref]["objectTable"].append(bobject) + object_export_data['data_ref'] = self.speaker_array[objref]["structName"] # Export the transform. If object is animated, then animation tracks are exported here if bobject.type != 'ARMATURE' and bobject.animation_data is not None: @@ -1710,22 +1706,22 @@ class ArmoryExporter: # Keep materials with fake user for m in bpy.data.materials: - if m.use_fake_user and m not in self.materialArray: - self.materialArray.append(m) + if m.use_fake_user and m not in self.material_array: + self.material_array.append(m) # Ensure the same order for merging materials - self.materialArray.sort(key=lambda x: x.name) + self.material_array.sort(key=lambda x: x.name) if wrd.arm_batch_materials: - mat_users = self.materialToObjectDict - mat_armusers = self.materialToArmObjectDict - mat_batch.build(self.materialArray, mat_users, mat_armusers) + mat_users = self.material_to_object_dict + mat_armusers = self.material_to_arm_object_dict + mat_batch.build(self.material_array, mat_users, mat_armusers) transluc_used = False overlays_used = False blending_used = False decals_used = False # sss_used = False - for material in self.materialArray: + for material in self.material_array: # If the material is unlinked, material becomes None if material is None: continue @@ -1756,8 +1752,8 @@ class ArmoryExporter: o['contexts'] = [] - mat_users = self.materialToObjectDict - mat_armusers = self.materialToArmObjectDict + mat_users = self.material_to_object_dict + mat_armusers = self.material_to_arm_object_dict sd, rpasses = make_material.parse(material, o, mat_users, mat_armusers) # Attach MovieTexture @@ -1808,8 +1804,8 @@ class ArmoryExporter: material.export_vcols = vcol_export material.export_tangents = tang_export - if material in self.materialToObjectDict: - mat_users = self.materialToObjectDict[material] + if material in self.material_to_object_dict: + mat_users = self.material_to_object_dict[material] for ob in mat_users: ob.data.arm_cached = False @@ -1838,9 +1834,9 @@ class ArmoryExporter: make_renderpath.build() def export_particle_systems(self): - if len(self.particleSystemArray) > 0: + if len(self.particle_system_array) > 0: self.output['particle_datas'] = [] - for particleRef in self.particleSystemArray.items(): + for particleRef in self.particle_system_array.items(): o = {} psettings = particleRef[0] @@ -1875,7 +1871,7 @@ class ArmoryExporter: o['mass'] = psettings.mass # Render o['instance_object'] = psettings.instance_object.name - self.objectToArmObjectDict[psettings.instance_object]['is_particle'] = True + self.object_to_arm_object_dict[psettings.instance_object]['is_particle'] = True # Field weights o['weight_gravity'] = psettings.effector_weights.gravity self.output['particle_datas'].append(o) @@ -1930,10 +1926,10 @@ class ArmoryExporter: self.output['camera_datas'] = [] self.output['speaker_datas'] = [] - for light_ref in self.lightArray.items(): + for light_ref in self.light_array.items(): self.export_light(light_ref) - for camera_ref in self.cameraArray.items(): + for camera_ref in self.camera_array.items(): self.export_camera(camera_ref) # Keep sounds with fake user @@ -1941,16 +1937,16 @@ class ArmoryExporter: if sound.use_fake_user: assets.add(arm.utils.asset_path(sound.filepath)) - for speaker_ref in self.speakerArray.items(): + for speaker_ref in self.speaker_array.items(): self.export_speaker(speaker_ref) if bpy.data.lightprobes: self.output['probe_datas'] = [] - for lightprobe_object in self.probeArray.items(): + for lightprobe_object in self.probe_array.items(): self.export_probe(lightprobe_object) self.output['mesh_datas'] = [] - for mesh_ref in self.meshArray.items(): + for mesh_ref in self.mesh_array.items(): self.export_mesh(mesh_ref, scene) def execute(self): @@ -1965,7 +1961,7 @@ class ArmoryExporter: # Map objects to game objects o = {} o['traits'] = [] - self.objectToArmObjectDict[bobject] = o + self.object_to_arm_object_dict[bobject] = o # Process # Skip objects that have a parent because children will be exported recursively if not bobject.parent: @@ -2061,7 +2057,7 @@ class ArmoryExporter: assets.add_khafile_def('arm_terrain') # Export material mat = self.scene.arm_terrain_object.children[0].data.materials[0] - self.materialArray.append(mat) + self.material_array.append(mat) # Terrain data terrain = {} terrain['name'] = 'Terrain' @@ -2097,14 +2093,14 @@ class ArmoryExporter: self.output['material_datas'] = [] # Object with no material assigned in the scene - if len(self.defaultMaterialObjects) > 0: - self.make_default_mat('armdefault', self.defaultMaterialObjects) - if len(self.defaultSkinMaterialObjects) > 0: - self.make_default_mat('armdefaultskin', self.defaultSkinMaterialObjects) + if len(self.default_material_objects) > 0: + self.make_default_mat('armdefault', self.default_material_objects) + if len(self.default_skin_material_objects) > 0: + self.make_default_mat('armdefaultskin', self.default_skin_material_objects) if len(bpy.data.particles) > 0: self.use_default_material_part() - if len(self.defaultPartMaterialObjects) > 0: - self.make_default_mat('armdefaultpart', self.defaultPartMaterialObjects, is_particle=True) + if len(self.default_part_material_objects) > 0: + self.make_default_mat('armdefaultpart', self.default_part_material_objects, is_particle=True) self.export_materials() self.export_particle_systems() @@ -2460,12 +2456,12 @@ class ArmoryExporter: # Map objects to materials, can be used in later stages for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) - if mat in self.materialToObjectDict: - self.materialToObjectDict[mat].append(bobject) - self.materialToArmObjectDict[mat].append(o) + if mat in self.material_to_object_dict: + self.material_to_object_dict[mat].append(bobject) + self.material_to_arm_object_dict[mat].append(o) else: - self.materialToObjectDict[mat] = [bobject] - self.materialToArmObjectDict[mat] = [o] + self.material_to_object_dict[mat] = [bobject] + self.material_to_arm_object_dict[mat] = [o] # Export constraints if len(bobject.constraints) > 0: From 7e5342ef8d922f4568d0d210c2dda39b443c6339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:20:08 +0200 Subject: [PATCH 054/230] Replace Python 3.5 annotations with Python 3.6 annotations --- blender/arm/exporter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index cf2664ae..8c8dd4ba 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -87,7 +87,7 @@ class ArmoryExporter: # Stores the object type ("objectType") and the asset name # ("structName") in a dict for each object - self.bobject_array = {} # type: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]]] + self.bobject_array: Dict[bpy.types.Object, Dict, str, Union[NodeType, str]] = {} self.bobject_bone_array = {} self.mesh_array = {} self.light_array = {} @@ -108,7 +108,7 @@ class ArmoryExporter: self.material_to_arm_object_dict = {} # Stores the link between a blender object and its # corresponding export data (arm object) - self.object_to_arm_object_dict = {} # type: Dict[bpy.types.Object, Dict] + self.object_to_arm_object_dict: Dict[bpy.types.Object, Dict] = {} self.bone_tracks = [] @@ -645,7 +645,7 @@ class ArmoryExporter: # Linked object, not present in scene if bobject not in self.object_to_arm_object_dict: - object_export_data = {} # type: Dict[str, Any] + object_export_data: Dict[str, Any] = {} object_export_data['traits'] = [] object_export_data['spawn'] = False self.object_to_arm_object_dict[bobject] = object_export_data From 3d1f65071c270c82e2d6c62319bc1205d665334a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:26:54 +0200 Subject: [PATCH 055/230] Fix type hint (again) --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 8c8dd4ba..674c4b93 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -87,7 +87,7 @@ class ArmoryExporter: # Stores the object type ("objectType") and the asset name # ("structName") in a dict for each object - self.bobject_array: Dict[bpy.types.Object, Dict, str, Union[NodeType, str]] = {} + self.bobject_array: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]]] = {} self.bobject_bone_array = {} self.mesh_array = {} self.light_array = {} From d4974d1340bb7c6f92a9dc04ea9a232a8dcf6587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 01:56:12 +0200 Subject: [PATCH 056/230] Change methods to staticmethods or classmethods if possible + some small style improvements --- blender/arm/exporter.py | 174 ++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 674c4b93..8bc9a9dc 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -112,15 +112,35 @@ class ArmoryExporter: self.bone_tracks = [] - self.preprocess() + ArmoryExporter.preprocess() @classmethod - def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene=None, depsgraph: bpy.types.Depsgraph=None) -> None: + def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None) -> None: """Exports the given scene to the given filepath. This is the function that is called in make.py and the entry point of the exporter.""" cls(context, filepath, scene, depsgraph).execute() + @classmethod + def preprocess(cls): + wrd = bpy.data.worlds['Arm'] + + cls.export_all_flag = True + cls.export_physics = False # Indicates whether rigid body is exported + if wrd.arm_physics == 'Enabled': + cls.export_physics = True + cls.export_navigation = False + if wrd.arm_navigation == 'Enabled': + cls.export_navigation = True + cls.export_ui = False + if not hasattr(cls, 'compress_enabled'): + cls.compress_enabled = False + if not hasattr(cls, 'optimize_enabled'): + cls.optimize_enabled = False + if not hasattr(cls, 'import_traits'): + cls.import_traits = [] # Referenced traits + cls.option_mesh_only = False + @staticmethod def write_matrix(matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], @@ -169,11 +189,11 @@ class ArmoryExporter: return curve_array def export_bone(self, armature, bone, scene, o, action): - bobjectRef = self.bobject_bone_array.get(bone) + bobject_ref = self.bobject_bone_array.get(bone) - if bobjectRef: - o['type'] = STRUCT_IDENTIFIER[bobjectRef["objectType"].value] - o['name'] = bobjectRef["structName"] + if bobject_ref: + o['type'] = STRUCT_IDENTIFIER[bobject_ref["objectType"].value] + o['name'] = bobject_ref["structName"] self.export_bone_transform(armature, bone, scene, o, action) o['children'] = [] @@ -182,14 +202,17 @@ class ArmoryExporter: self.export_bone(armature, subbobject, scene, so, action) o['children'].append(so) - def export_pose_markers(self, oanim, action): - if action.pose_markers == None or len(action.pose_markers) == 0: + @staticmethod + def export_pose_markers(oanim, action): + if action.pose_markers is None or len(action.pose_markers) == 0: return + oanim['marker_frames'] = [] oanim['marker_names'] = [] - for m in action.pose_markers: - oanim['marker_frames'].append(int(m.frame)) - oanim['marker_names'].append(m.name) + + for pos_marker in action.pose_markers: + oanim['marker_frames'].append(int(pos_marker.frame)) + oanim['marker_names'].append(pos_marker.name) def export_object_sampled_animation(self, bobject: bpy.types.Object,scene: bpy.types.Scene, o: Dict) -> None: """Exports animation as full 4x4 matrices for each frame""" @@ -204,9 +227,9 @@ class ArmoryExporter: action = bobject.animation_data.action aname = arm.utils.safestr(arm.utils.asset_name(action)) - fp = self.get_meshes_file_path('action_' + aname, compressed=self.is_compress()) + fp = self.get_meshes_file_path('action_' + aname, compressed=ArmoryExporter.compress_enabled) assets.add(fp) - ext = '.lz4' if self.is_compress() else '' + ext = '.lz4' if ArmoryExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' o['object_actions'].append('action_' + aname + ext) @@ -252,7 +275,8 @@ class ArmoryExporter: oaction['transform'] = None arm.utils.write_arm(fp, actionf) - def calculate_animation_length(self, action): + @staticmethod + def calculate_animation_length(action): """Calculates the length of the given action.""" start = action.frame_range[0] end = action.frame_range[1] @@ -272,7 +296,8 @@ class ArmoryExporter: return (int(start), int(end)) - def export_animation_track(self, fcurve, frame_range, target): + @staticmethod + def export_animation_track(fcurve, frame_range, target): """This function exports a single animation track.""" data_ttrack = {} @@ -316,9 +341,9 @@ class ArmoryExporter: if 'object_actions' not in o: o['object_actions'] = [] - fp = self.get_meshes_file_path('action_' + action_name, compressed=self.is_compress()) + fp = self.get_meshes_file_path('action_' + action_name, compressed=ArmoryExporter.compress_enabled) assets.add(fp) - ext = '.lz4' if self.is_compress() else '' + ext = '.lz4' if ArmoryExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' o['object_actions'].append('action_' + action_name + ext) @@ -489,7 +514,8 @@ class ArmoryExporter: pref['particle'] = psys.settings.name o['particle_refs'].append(pref) - def get_view3d_area(self): + @staticmethod + def get_view3d_area(): screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': @@ -530,7 +556,8 @@ class ArmoryExporter: values += ArmoryExporter.write_matrix(pose_bone.matrix) # print('Bone matrices exported in ' + str(time.time() - profile_time)) - def has_baked_material(self, bobject, materials): + @staticmethod + def has_baked_material(bobject, materials): for mat in materials: if mat is None: continue @@ -539,7 +566,8 @@ class ArmoryExporter: return True return False - def slot_to_material(self, bobject, slot): + @staticmethod + def slot_to_material(bobject, slot): mat = slot.material # Pick up backed material if present if mat is not None: @@ -636,7 +664,7 @@ class ArmoryExporter: meshes), and transform. Subobjects are then exported recursively. """ - if not self.preprocess_object(bobject): + if not bobject.arm_export: return bobject_ref = self.bobject_array.get(bobject) @@ -711,7 +739,7 @@ class ArmoryExporter: if object_type is NodeType.MESH: if objref not in self.mesh_array: - self.mesh_array[objref] = {"structName" : objname, "objectTable" : [bobject]} + self.mesh_array[objref] = {"structName": objname, "objectTable": [bobject]} else: self.mesh_array[objref]["objectTable"].append(bobject) @@ -721,7 +749,7 @@ class ArmoryExporter: if wrd.arm_single_data_file: object_export_data['data_ref'] = oid else: - ext = '' if not self.is_compress() else '.lz4' + ext = '' if not ArmoryExporter.compress_enabled else '.lz4' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' object_export_data['data_ref'] = 'mesh_' + oid + ext + '/' + oid @@ -732,7 +760,7 @@ class ArmoryExporter: # Export ref self.export_material_ref(bobject, mat, i, object_export_data) # Decal flag - if mat != None and mat.arm_decal: + if mat is not None and mat.arm_decal: object_export_data['type'] = 'decal_object' # No material, mimic cycles and assign default if len(object_export_data['material_refs']) == 0: @@ -863,7 +891,7 @@ class ArmoryExporter: export_actions.append(strip.action) armatureid = arm.utils.safestr(arm.utils.asset_name(bdata)) - ext = '.lz4' if self.is_compress() else '' + ext = '.lz4' if ArmoryExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' object_export_data['bone_actions'] = [] @@ -886,7 +914,7 @@ class ArmoryExporter: for action in export_actions: aname = arm.utils.safestr(arm.utils.asset_name(action)) skelobj.animation_data.action = action - fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=self.is_compress()) + fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=ArmoryExporter.compress_enabled) assets.add(fp) if not bdata.arm_cached or not os.path.exists(fp): #handle autobake @@ -1053,7 +1081,8 @@ class ArmoryExporter: arm.utils.write_arm(fp, mesh_obj) bobject.data.arm_cached = True - def calc_aabb(self, bobject): + @staticmethod + def calc_aabb(bobject): aabb_center = 0.125 * sum((Vector(b) for b in bobject.bound_box), Vector()) bobject.data.arm_aabb = [ \ abs((bobject.bound_box[6][0] - bobject.bound_box[0][0]) / 2 + abs(aabb_center[0])) * 2, \ @@ -1308,7 +1337,7 @@ class ArmoryExporter: if wrd.arm_single_data_file: fp = None else: - fp = self.get_meshes_file_path('mesh_' + oid, compressed=self.is_compress()) + fp = self.get_meshes_file_path('mesh_' + oid, compressed=ArmoryExporter.compress_enabled) assets.add(fp) # No export necessary if bobject.data.arm_cached and os.path.exists(fp): @@ -1570,7 +1599,8 @@ class ArmoryExporter: else: return [0.051, 0.051, 0.051, 1.0] - def extract_projection(self, o, proj, with_planes=True): + @staticmethod + def extract_projection(o, proj, with_planes=True): a = proj[0][0] b = proj[1][1] c = proj[2][2] @@ -1581,7 +1611,8 @@ class ArmoryExporter: o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) o['far_plane'] = k * o['near_plane'] - def extract_ortho(self, o, proj): + @staticmethod + def extract_ortho(o, proj): # left, right, bottom, top o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \ (1 - proj[3][0]) / proj[0][0], \ @@ -1905,9 +1936,6 @@ class ArmoryExporter: self.post_export_world(w, o) self.output['world_datas'].append(o) - def is_compress(self): - return ArmoryExporter.compress_enabled - def export_objects(self, scene): """Exports all supported blender objects. @@ -2248,25 +2276,29 @@ class ArmoryExporter: self.output['objects'].append(o) self.output['camera_ref'] = 'DefaultCamera' - def get_export_tangents(self, mesh): - for m in mesh.materials: - if m != None and m.export_tangents == True: + @staticmethod + def get_export_tangents(mesh): + for material in mesh.materials: + if material is not None and material.export_tangents: return True return False - def get_export_vcols(self, mesh): - for m in mesh.materials: - if m != None and m.export_vcols == True: + @staticmethod + def get_export_vcols(mesh): + for material in mesh.materials: + if material is not None and material.export_vcols == True: return True return False - def get_export_uvs(self, mesh): - for m in mesh.materials: - if m != None and m.export_uvs == True: + @staticmethod + def get_export_uvs(mesh): + for material in mesh.materials: + if material is not None and material.export_uvs == True: return True return False - def object_process_instancing(self, refs, scale_pos): + @staticmethod + def object_process_instancing(refs, scale_pos): instanced_type = 0 instanced_data = None for bobject in refs: @@ -2318,33 +2350,6 @@ class ArmoryExporter: return instanced_type, instanced_data - def preprocess(self): - wrd = bpy.data.worlds['Arm'] - ArmoryExporter.export_all_flag = True - ArmoryExporter.export_physics = False # Indicates whether rigid body is exported - if wrd.arm_physics == 'Enabled': - ArmoryExporter.export_physics = True - ArmoryExporter.export_navigation = False - if wrd.arm_navigation == 'Enabled': - ArmoryExporter.export_navigation = True - ArmoryExporter.export_ui = False - if not hasattr(ArmoryExporter, 'compress_enabled'): - ArmoryExporter.compress_enabled = False - if not hasattr(ArmoryExporter, 'optimize_enabled'): - ArmoryExporter.optimize_enabled = False - if not hasattr(ArmoryExporter, 'import_traits'): - ArmoryExporter.import_traits = [] # Referenced traits - ArmoryExporter.option_mesh_only = False - - def preprocess_object(self, bobject): # Returns false if object should not be exported - export_object = True - - # Disabled object - if bobject.arm_export == False: - return False - - return export_object - def post_export_object(self, bobject, o, type): # Export traits self.export_traits(bobject, o) @@ -2471,7 +2476,8 @@ class ArmoryExporter: for x in o['traits']: ArmoryExporter.import_traits.append(x['class_name']) - def add_constraints(self, bobject, o, bone=False): + @staticmethod + def add_constraints(bobject, o, bone=False): for con in bobject.constraints: if con.mute: continue @@ -2597,7 +2603,8 @@ class ArmoryExporter: o['traits'].append(x) - def export_canvas_themes(self): + @staticmethod + def export_canvas_themes(): path_themes = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas') file_theme = os.path.join(path_themes, "_themes.json") @@ -2624,7 +2631,8 @@ class ArmoryExporter: if soft_type == 0: self.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) - def add_hook_mod(self, o, bobject, target_name, group_name): + @staticmethod + def add_hook_mod(o, bobject, target_name, group_name): ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' trait = {} @@ -2642,10 +2650,11 @@ class ArmoryExporter: trait['parameters'] = ["'" + target_name + "'", str(verts)] o['traits'].append(trait) - def add_rigidbody_constraint(self, o, rbc): + @staticmethod + def add_rigidbody_constraint(o, rbc): rb1 = rbc.object1 rb2 = rbc.object2 - if rb1 == None or rb2 is None: + if rb1 is None or rb2 is None: return ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' @@ -2742,7 +2751,8 @@ class ArmoryExporter: trait['parameters'].append(str(limits)) o['traits'].append(trait) - def post_export_world(self, world, o): + @staticmethod + def post_export_world(world, o): wrd = bpy.data.worlds['Arm'] bgcol = world.arm_envtex_color if '_LDR' in wrd.world_defs: # No compositor used @@ -2796,15 +2806,19 @@ class ArmoryExporter: po['strength'] = strength o['probe'] = po - # https://blender.stackexchange.com/questions/70629 - def mod_equal(self, mod1, mod2): + @staticmethod + def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier): + """Compares whether the given modifiers are equal.""" + # https://blender.stackexchange.com/questions/70629 return all([getattr(mod1, prop, True) == getattr(mod2, prop, False) for prop in mod1.bl_rna.properties.keys()]) - def mod_equal_stack(self, obj1, obj2): + @staticmethod + def mod_equal_stack(obj1, obj2): + """Returns `True` if the given objects have the same modifiers.""" if len(obj1.modifiers) == 0 and len(obj2.modifiers) == 0: return True if len(obj1.modifiers) == 0 or len(obj2.modifiers) == 0: return False if len(obj1.modifiers) != len(obj2.modifiers): return False - return all([self.mod_equal(m, obj2.modifiers[i]) for i,m in enumerate(obj1.modifiers)]) + return all([ArmoryExporter.mod_equal(m, obj2.modifiers[i]) for i, m in enumerate(obj1.modifiers)]) From 7541ca86a24369bd8257c3498e1dfcf7a33fe34c Mon Sep 17 00:00:00 2001 From: Sandy <48373918+Sandy10000@users.noreply.github.com> Date: Fri, 10 Apr 2020 21:22:04 +0900 Subject: [PATCH 057/230] fix https://github.com/armory3d/armory/issues/1576 --- blender/arm/logicnode/action_call_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/action_call_group.py b/blender/arm/logicnode/action_call_group.py index da32f3ff..b2f57da5 100644 --- a/blender/arm/logicnode/action_call_group.py +++ b/blender/arm/logicnode/action_call_group.py @@ -12,7 +12,7 @@ class CallGroupNode(Node, ArmLogicTreeNode): @property def property0(self): - return arm.utils.safesrc(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + arm.utils.safesrc(self.property0_) + return arm.utils.safesrc(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + arm.utils.safesrc(self.property0_.name) property0_: PointerProperty(name='Group', type=bpy.types.NodeTree) From 3593092c59327647c3d7ae0b0589eb879ed544c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 19:21:09 +0200 Subject: [PATCH 058/230] Remove "== True" and replace "== False" with "not" --- blender/arm/exporter.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 8bc9a9dc..32a8b6a8 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -262,7 +262,7 @@ class ArmoryExporter: oanim['tracks'] = [tracko] self.export_pose_markers(oanim, action) - if True: #action.arm_cached == False or not os.path.exists(fp): + if True: #not action.arm_cached or not os.path.exists(fp): wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting object action ' + aname) @@ -379,7 +379,7 @@ class ArmoryExporter: oanim['tracks'].append(data_ttrack) - if True: #action.arm_cached == False or not os.path.exists(fp): + if True: # not action.arm_cached or not os.path.exists(fp): wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting object action ' + action_name) @@ -1324,7 +1324,7 @@ class ArmoryExporter: # bpy.data.meshes.remove(morphMesh) def has_tangents(self, exportMesh): - return self.get_export_uvs(exportMesh) == True and self.get_export_tangents(exportMesh) == True and len(exportMesh.uv_layers) > 0 + return self.get_export_uvs(exportMesh) and self.get_export_tangents(exportMesh) and len(exportMesh.uv_layers) > 0 def export_mesh(self, objectRef, scene): """Exports a single mesh object.""" @@ -1658,7 +1658,7 @@ class ArmoryExporter: if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + objref.sound.name - if os.path.isfile(unpack_filepath) == False or os.path.getsize(unpack_filepath) != objref.sound.packed_file.size: + if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != objref.sound.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(objref.sound.packed_file.data) assets.add(unpack_filepath) @@ -1701,7 +1701,7 @@ class ArmoryExporter: self.output['material_datas'].append(o) bpy.data.materials.remove(mat) rpdat = arm.utils.get_rp() - if rpdat.arm_culling == False: + if not rpdat.arm_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' @@ -1736,9 +1736,10 @@ class ArmoryExporter: wrd = bpy.data.worlds['Arm'] # Keep materials with fake user - for m in bpy.data.materials: - if m.use_fake_user and m not in self.material_array: - self.material_array.append(m) + for material in bpy.data.materials: + if m.use_fake_user and material not in self.material_array: + self.material_array.append(material) + # Ensure the same order for merging materials self.material_array.sort(key=lambda x: x.name) @@ -1752,6 +1753,7 @@ class ArmoryExporter: blending_used = False decals_used = False # sss_used = False + for material in self.material_array: # If the material is unlinked, material becomes None if material is None: @@ -1774,7 +1776,7 @@ class ArmoryExporter: o['skip_context'] = material.arm_skip_context rpdat = arm.utils.get_rp() - if material.arm_two_sided or rpdat.arm_culling == False: + if material.arm_two_sided or not rpdat.arm_culling: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' elif material.arm_cull_mode != 'clockwise': @@ -2286,14 +2288,14 @@ class ArmoryExporter: @staticmethod def get_export_vcols(mesh): for material in mesh.materials: - if material is not None and material.export_vcols == True: + if material is not None and material.export_vcols: return True return False @staticmethod def get_export_uvs(mesh): for material in mesh.materials: - if material is not None and material.export_uvs == True: + if material is not None and material.export_uvs: return True return False @@ -2318,7 +2320,7 @@ class ArmoryExporter: instanced_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0] for child in bobject.children: - if child.arm_export == False or child.hide_render: + if not child.arm_export or child.hide_render: continue if 'Loc' in inst: loc = child.matrix_local.to_translation() # Without parent matrix @@ -2505,7 +2507,7 @@ class ArmoryExporter: def export_traits(self, bobject, o): if hasattr(bobject, 'arm_traitlist'): for t in bobject.arm_traitlist: - if t.enabled_prop == False: + if not t.enabled_prop: continue x = {} if t.type_prop == 'Logic Nodes' and t.node_tree_prop != None and t.node_tree_prop.name != '': @@ -2554,7 +2556,7 @@ class ArmoryExporter: nav_filepath = nav_path + '/nav_' + bobject.data.name + '.arm' assets.add(nav_filepath) # TODO: Implement cache - #if os.path.isfile(nav_filepath) == False: + #if not os.path.isfile(nav_filepath): # override = {'selected_objects': [bobject]} # bobject.scale.y *= -1 # mesh = obj.data From b5b8c4f993141a59bbde5a7269668169368f1541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 19:22:56 +0200 Subject: [PATCH 059/230] Fix last commit --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 32a8b6a8..804c3519 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1737,7 +1737,7 @@ class ArmoryExporter: # Keep materials with fake user for material in bpy.data.materials: - if m.use_fake_user and material not in self.material_array: + if material.use_fake_user and material not in self.material_array: self.material_array.append(material) # Ensure the same order for merging materials From 3882c56fc4ff74deee2112d8fd8928117450628f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 19:25:34 +0200 Subject: [PATCH 060/230] Replace "==/!= None" with "is/is not None" --- blender/arm/exporter.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 804c3519..da182359 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -218,7 +218,7 @@ class ArmoryExporter: """Exports animation as full 4x4 matrices for each frame""" animation_flag = False - animation_flag = bobject.animation_data != None and bobject.animation_data.action != None and bobject.type != 'ARMATURE' + animation_flag = bobject.animation_data is not None and bobject.animation_data.action is not None and bobject.type != 'ARMATURE' # Font out if animation_flag: @@ -504,10 +504,10 @@ class ArmoryExporter: if psys.settings in self.particle_system_array: # or not modifier.show_render: return - if psys.settings.instance_object == None or psys.settings.render_type != 'OBJECT': - return + if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT': + return - self.particle_system_array[psys.settings] = {"structName" : psys.settings.name} + self.particle_system_array[psys.settings] = {"structName": psys.settings.name} pref = {} pref['name'] = psys.name pref['seed'] = psys.seed @@ -1876,7 +1876,7 @@ class ArmoryExporter: if psettings is None: continue - if psettings.instance_object == None or psettings.render_type != 'OBJECT': + if psettings.instance_object is None or psettings.render_type != 'OBJECT': continue o['name'] = particleRef[1]["structName"] @@ -2046,10 +2046,10 @@ class ArmoryExporter: # Particle and non-particle objects can not share material for psys in bpy.data.particles: bo = psys.instance_object - if bo == None or psys.render_type != 'OBJECT': + if bo is None or psys.render_type != 'OBJECT': continue for slot in bo.material_slots: - if slot.material == None or slot.material.library is not None: + if slot.material is None or slot.material.library is not None: continue if slot.material.name.endswith('_armpart'): continue @@ -2176,7 +2176,7 @@ class ArmoryExporter: phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld' rbw = self.scene.rigidbody_world - if rbw != None and rbw.enabled: + if rbw is not None and rbw.enabled: x['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] self.output['traits'].append(x) if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: @@ -2361,7 +2361,7 @@ class ArmoryExporter: phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' # Rigid body trait - if bobject.rigid_body != None and phys_enabled: + if bobject.rigid_body is not None and phys_enabled: ArmoryExporter.export_physics = True rb = bobject.rigid_body shape = 0 # BOX @@ -2448,7 +2448,7 @@ class ArmoryExporter: self.add_hook_mod(o, bobject, m.object.name, m.vertex_group) # Rigid body constraint rbc = bobject.rigid_body_constraint - if rbc != None and rbc.enabled: + if rbc is not None and rbc.enabled: self.add_rigidbody_constraint(o, rbc) # Camera traits @@ -2510,7 +2510,7 @@ class ArmoryExporter: if not t.enabled_prop: continue x = {} - if t.type_prop == 'Logic Nodes' and t.node_tree_prop != None and t.node_tree_prop.name != '': + if t.type_prop == 'Logic Nodes' and t.node_tree_prop is not None and t.node_tree_prop.name != '': x['type'] = 'Script' group_name = arm.utils.safesrc(t.node_tree_prop.name[0].upper() + t.node_tree_prop.name[1:]) x['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name From eefcefbf9103791dc4280935a70a9e65e9af92fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 19:38:28 +0200 Subject: [PATCH 061/230] Static initialization for some variables --- blender/arm/exporter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index da182359..99120901 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -74,6 +74,11 @@ current_output = None class ArmoryExporter: """Export to Armory format""" + compress_enabled = False + optimize_enabled = False + # Referenced traits + import_traits = [] + def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): global current_output @@ -133,12 +138,6 @@ class ArmoryExporter: if wrd.arm_navigation == 'Enabled': cls.export_navigation = True cls.export_ui = False - if not hasattr(cls, 'compress_enabled'): - cls.compress_enabled = False - if not hasattr(cls, 'optimize_enabled'): - cls.optimize_enabled = False - if not hasattr(cls, 'import_traits'): - cls.import_traits = [] # Referenced traits cls.option_mesh_only = False @staticmethod From 403e52109c0dc762580bfdcb14044243c52cfe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 20:43:59 +0200 Subject: [PATCH 062/230] Add "fake user" setting for traits --- Sources/armory/logicnode/TraitNode.hx | 1 + blender/arm/exporter.py | 6 +++++- blender/arm/props_traits.py | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/TraitNode.hx b/Sources/armory/logicnode/TraitNode.hx index f5b29a63..245216ac 100644 --- a/Sources/armory/logicnode/TraitNode.hx +++ b/Sources/armory/logicnode/TraitNode.hx @@ -14,6 +14,7 @@ class TraitNode extends LogicNode { var cname = Type.resolveClass(Main.projectPackage + "." + property0); if (cname == null) cname = Type.resolveClass(Main.projectPackage + ".node." + property0); + if (cname == null) throw 'No trait with the name "$property0" found, make sure that the trait is exported!'; value = Type.createInstance(cname, []); return value; } diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 128ba63a..b930fe8e 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2467,8 +2467,12 @@ class ArmoryExporter: def export_traits(self, bobject, o): if hasattr(bobject, 'arm_traitlist'): for t in bobject.arm_traitlist: - if t.enabled_prop == False: + # Don't export disabled traits but still export those + # with fake user enabled so that nodes like `TraitNode` + # still work + if not t.enabled_prop and not t.fake_user: continue + x = {} if t.type_prop == 'Logic Nodes' and t.node_tree_prop != None and t.node_tree_prop.name != '': x['type'] = 'Script' diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index fdf97bce..1e53c429 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -54,6 +54,7 @@ class ArmTraitListItem(bpy.types.PropertyGroup): name: StringProperty(name="Name", description="A name for this item", default="") enabled_prop: BoolProperty(name="", description="A name for this item", default=True, update=trigger_recompile) is_object: BoolProperty(name="", default=True) + fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False) type_prop: EnumProperty( items = [('Haxe Script', 'Haxe', 'Haxe Script'), ('WebAssembly', 'Wasm', 'WebAssembly'), @@ -88,12 +89,16 @@ class ARM_UL_TraitList(bpy.types.UIList): # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.prop(item, "enabled_prop") - layout.label(text=item.name, icon=custom_icon, icon_value=custom_icon_value) + # Display " " for props without a name to right-align the + # fake_user button + layout.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) + layout.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") + class ArmTraitListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "arm_traitlist.new_item" From c9b89e6036d84dc3f434c1adfa193a1589ffe874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 20:44:35 +0200 Subject: [PATCH 063/230] Reorder trait UI --- blender/arm/props_traits.py | 59 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 1e53c429..65675af4 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -567,33 +567,8 @@ def draw_traits(layout, obj, is_object): if obj.arm_traitlist_index >= 0 and len(obj.arm_traitlist) > 0: item = obj.arm_traitlist[obj.arm_traitlist_index] - # Default props + if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': - item.name = item.class_name_prop - row = layout.row() - if item.type_prop == 'Haxe Script': - row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_scripts_list", text="Class") - else: - # Bundled scripts not yet fetched - if not bpy.data.worlds['Arm'].arm_bundled_scripts_list: - arm.utils.fetch_bundled_script_names() - row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_bundled_scripts_list", text="Class") - - # Props - if item.arm_traitpropslist: - layout.label(text="Trait Properties:") - if item.arm_traitpropswarnings: - box = layout.box() - box.label(text=f"Warnings ({len(item.arm_traitpropswarnings)}):", icon="ERROR") - - for warning in item.arm_traitpropswarnings: - box.label(text=warning.warning) - - propsrow = layout.row() - propsrows = max(len(item.arm_traitpropslist), 6) - row = layout.row() - row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) - if item.type_prop == 'Haxe Script': row = layout.row(align=True) row.alignment = 'EXPAND' @@ -620,6 +595,17 @@ def draw_traits(layout, obj, is_object): op.is_object = is_object op = row.operator("arm.refresh_scripts") + # Default props + item.name = item.class_name_prop + row = layout.row() + if item.type_prop == 'Haxe Script': + row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_scripts_list", text="Class") + else: + # Bundled scripts not yet fetched + if not bpy.data.worlds['Arm'].arm_bundled_scripts_list: + arm.utils.fetch_bundled_script_names() + row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_bundled_scripts_list", text="Class") + elif item.type_prop == 'WebAssembly': item.name = item.webassembly_prop row = layout.row() @@ -638,8 +624,6 @@ def draw_traits(layout, obj, is_object): elif item.type_prop == 'UI Canvas': item.name = item.canvas_name_prop - row = layout.row() - row.prop_search(item, "canvas_name_prop", bpy.data.worlds['Arm'], "arm_canvas_list", text="Canvas") row = layout.row(align=True) row.alignment = 'EXPAND' @@ -653,10 +637,29 @@ def draw_traits(layout, obj, is_object): op.is_object = is_object op = row.operator("arm.refresh_canvas_list") + row = layout.row() + row.prop_search(item, "canvas_name_prop", bpy.data.worlds['Arm'], "arm_canvas_list", text="Canvas") + elif item.type_prop == 'Logic Nodes': row = layout.row() row.prop_search(item, "node_tree_prop", bpy.data, "node_groups", text="Tree") + if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': + # Props + if item.arm_traitpropslist: + layout.label(text="Trait Properties:") + if item.arm_traitpropswarnings: + box = layout.box() + box.label(text=f"Warnings ({len(item.arm_traitpropswarnings)}):", icon="ERROR") + + for warning in item.arm_traitpropswarnings: + box.label(text=warning.warning) + + propsrow = layout.row() + propsrows = max(len(item.arm_traitpropslist), 6) + row = layout.row() + row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + def register(): global icons_dict bpy.utils.register_class(ArmTraitListItem) From af8a7615b3b37c4fed618ee97e896a451ddf5cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 21:46:28 +0200 Subject: [PATCH 064/230] Fix lod deletion --- blender/arm/props_lod.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index 2de6612f..ef1b2e86 100755 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -18,7 +18,7 @@ class ArmLodListItem(bpy.types.PropertyGroup): name="Name", description="A name for this item", default="") - + enabled_prop: BoolProperty( name="", description="A name for this item", @@ -82,9 +82,9 @@ class ArmLodListDeleteItem(bpy.types.Operator): index = mdata.arm_lodlist_index n = lodlist[index].name - if n in context.scene.objects: + if n in context.scene.collection.objects: obj = bpy.data.objects[n] - context.scene.objects.unlink(obj) + context.scene.collection.objects.unlink(obj) lodlist.remove(index) From b1572e316e516e0c2b4a5d962a77e9f95a86ecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 23:06:37 +0200 Subject: [PATCH 065/230] Proxy: Add option to keep local trait properties when syncing --- blender/arm/props.py | 1 + blender/arm/props_ui.py | 5 +++++ blender/arm/proxy.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index af222807..e11302a9 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -124,6 +124,7 @@ def init_properties(): bpy.types.Object.arm_proxy_sync_materials = BoolProperty(name="Materials", description="Keep materials synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_materials) bpy.types.Object.arm_proxy_sync_modifiers = BoolProperty(name="Modifiers", description="Keep modifiers synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_modifiers) bpy.types.Object.arm_proxy_sync_traits = BoolProperty(name="Traits", description="Keep traits synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_traits) + bpy.types.Object.arm_proxy_sync_trait_props = BoolProperty(name="Trait Property Values", description="Keep trait property values synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_traits) # For speakers bpy.types.Speaker.arm_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False) bpy.types.Speaker.arm_loop = BoolProperty(name="Loop", description="Loop this sound", default=False) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 65b6c313..bea19b16 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1320,6 +1320,9 @@ class ARM_PT_ProxyPanel(bpy.types.Panel): layout.prop(obj, "arm_proxy_sync_materials") layout.prop(obj, "arm_proxy_sync_modifiers") layout.prop(obj, "arm_proxy_sync_traits") + row = layout.row() + row.enabled = obj.arm_proxy_sync_traits + row.prop(obj, "arm_proxy_sync_trait_props") layout.operator("arm.proxy_toggle_all") layout.operator("arm.proxy_apply_all") @@ -1349,6 +1352,7 @@ class ArmProxyToggleAllButton(bpy.types.Operator): obj.arm_proxy_sync_materials = b obj.arm_proxy_sync_modifiers = b obj.arm_proxy_sync_traits = b + obj.arm_proxy_sync_trait_props = b return{'FINISHED'} class ArmProxyApplyAllButton(bpy.types.Operator): @@ -1365,6 +1369,7 @@ class ArmProxyApplyAllButton(bpy.types.Operator): obj.arm_proxy_sync_materials = context.object.arm_proxy_sync_materials obj.arm_proxy_sync_modifiers = context.object.arm_proxy_sync_modifiers obj.arm_proxy_sync_traits = context.object.arm_proxy_sync_traits + obj.arm_proxy_sync_trait_props = context.object.arm_proxy_sync_trait_props return{'FINISHED'} class ArmSyncProxyButton(bpy.types.Operator): diff --git a/blender/arm/proxy.py b/blender/arm/proxy.py index 558bd68f..3304d979 100644 --- a/blender/arm/proxy.py +++ b/blender/arm/proxy.py @@ -1,3 +1,5 @@ +from typing import Any, Dict + import bpy def proxy_sync_loc(self, context): @@ -107,11 +109,36 @@ def sync_collection(cSrc, cDst): for prop in properties: setattr(mDst, prop, getattr(mSrc, prop)) -def sync_traits(obj): +def sync_traits(obj: bpy.types.Object): + """Synchronizes the traits of the given object with the traits of + its proxy. + If `arm.proxy_sync_trait_props` is `False`, the values of the trait + properties are kept where possible. + """ + # (Optionally) keep the old property values + for i in range(len(obj.arm_traitlist)): + values: Dict[str, Dict[str, Any]] = {} + + if not obj.arm_proxy_sync_trait_props: + for prop in obj.arm_traitlist[i].arm_traitpropslist: + values[obj.name][prop.name] = prop.get_value() + sync_collection(obj.proxy.arm_traitlist, obj.arm_traitlist) - for i in range(0, len(obj.arm_traitlist)): + + for i in range(len(obj.arm_traitlist)): sync_collection(obj.proxy.arm_traitlist[i].arm_traitpropslist, obj.arm_traitlist[i].arm_traitpropslist) + # Set stored property values + if not obj.arm_proxy_sync_trait_props: + for prop in obj.arm_traitlist[i].arm_traitpropslist: + if value.get(obj.name) is None: + continue + + value = values[obj.name].get(prop.name) + if value is not None: + prop.set_value(value) + + def sync_materials(obj): # Blender likes to crash here:( pass From df30e9b68158265e1fa181d3c2b43c91ece01a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 23:10:01 +0200 Subject: [PATCH 066/230] Set `arm_proxy_sync_trait_props` to `False` by default --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index e11302a9..0cdb249d 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -124,7 +124,7 @@ def init_properties(): bpy.types.Object.arm_proxy_sync_materials = BoolProperty(name="Materials", description="Keep materials synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_materials) bpy.types.Object.arm_proxy_sync_modifiers = BoolProperty(name="Modifiers", description="Keep modifiers synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_modifiers) bpy.types.Object.arm_proxy_sync_traits = BoolProperty(name="Traits", description="Keep traits synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_traits) - bpy.types.Object.arm_proxy_sync_trait_props = BoolProperty(name="Trait Property Values", description="Keep trait property values synchronized with proxy object", default=True, update=arm.proxy.proxy_sync_traits) + bpy.types.Object.arm_proxy_sync_trait_props = BoolProperty(name="Trait Property Values", description="Keep trait property values synchronized with proxy object", default=False, update=arm.proxy.proxy_sync_traits) # For speakers bpy.types.Speaker.arm_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False) bpy.types.Speaker.arm_loop = BoolProperty(name="Loop", description="Loop this sound", default=False) From c3eac025b35d44851b3d0464a3a1be19ce6212f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 11 Apr 2020 14:22:22 +0200 Subject: [PATCH 067/230] Small improvements + fix for multiple traits per object --- blender/arm/proxy.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/blender/arm/proxy.py b/blender/arm/proxy.py index 3304d979..fab18aed 100644 --- a/blender/arm/proxy.py +++ b/blender/arm/proxy.py @@ -116,12 +116,11 @@ def sync_traits(obj: bpy.types.Object): properties are kept where possible. """ # (Optionally) keep the old property values + values: Dict[bpy.types.Object, Dict[str, Dict[str, Any]]] = {} for i in range(len(obj.arm_traitlist)): - values: Dict[str, Dict[str, Any]] = {} - if not obj.arm_proxy_sync_trait_props: for prop in obj.arm_traitlist[i].arm_traitpropslist: - values[obj.name][prop.name] = prop.get_value() + values[obj][obj.arm_traitlist[i].name][prop.name] = prop.get_value() sync_collection(obj.proxy.arm_traitlist, obj.arm_traitlist) @@ -130,11 +129,16 @@ def sync_traits(obj: bpy.types.Object): # Set stored property values if not obj.arm_proxy_sync_trait_props: - for prop in obj.arm_traitlist[i].arm_traitpropslist: - if value.get(obj.name) is None: - continue + if values.get(obj) is None: + continue - value = values[obj.name].get(prop.name) + value = values[obj].get(obj.arm_traitlist[i].name) + if value is None: + continue + + for prop in obj.arm_traitlist[i].arm_traitpropslist: + + value = values[obj].get(prop.name) if value is not None: prop.set_value(value) From 6a16cebadf0068d59ad7e99be0d6cfa688e7a54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 11 Apr 2020 16:44:06 +0200 Subject: [PATCH 068/230] Refactor mapping node --- blender/arm/material/cycles.py | 87 ++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 6e73ba97..d7822bab 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -14,16 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import math -import bpy import os +import shutil + +import bpy +from mathutils import Euler, Vector + import arm.assets import arm.utils import arm.make_state import arm.log import arm.material.mat_state as mat_state import arm.material.cycles_functions as c_functions -import shutil emission_found = False particle_info = None # Particle info export @@ -373,7 +375,7 @@ def parse_vector_input(inp): else: return to_vec3(inp.default_value) -def parse_vector(node, socket): +def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: global particle_info global sample_bump global sample_bump_res @@ -920,32 +922,59 @@ def parse_vector(node, socket): return res elif node.type == 'MAPPING': - out = parse_vector_input(node.inputs[0]) - scale = node.inputs['Scale'].default_value - rotation = node.inputs['Rotation'].default_value - location = node.inputs['Location'].default_value if node.inputs['Location'].enabled else [0.0, 0.0, 0.0] - if scale[0] != 1.0 or scale[1] != 1.0 or scale[2] != 1.0: - out = '({0} * vec3({1}, {2}, {3}))'.format(out, scale[0], scale[1], scale[2]) - if rotation[2] != 0.0: - # ZYX rotation, Z axis for now.. - a = rotation[2] - # x * cos(theta) - y * sin(theta) - # x * sin(theta) + y * cos(theta) - out = 'vec3({0}.x * {1} - ({0}.y) * {2}, {0}.x * {2} + ({0}.y) * {1}, 0.0)'.format(out, math.cos(a), math.sin(a)) - # if node.rotation[1] != 0.0: - # a = node.rotation[1] - # out = 'vec3({0}.x * {1} - {0}.z * {2}, {0}.x * {2} + {0}.z * {1}, 0.0)'.format(out, math.cos(a), math.sin(a)) - # if node.rotation[0] != 0.0: - # a = node.rotation[0] - # out = 'vec3({0}.y * {1} - {0}.z * {2}, {0}.y * {2} + {0}.z * {1}, 0.0)'.format(out, math.cos(a), math.sin(a)) + # Only "Point", "Texture" and "Vector" types supported for now.. + # More information about the order of operations for this node: + # https://docs.blender.org/manual/en/latest/render/shader_nodes/vector/mapping.html#properties + + input_vector: bpy.types.NodeSocket = node.inputs[0] + input_location: bpy.types.NodeSocket = node.inputs['Location'] + input_rotation: bpy.types.NodeSocket = node.inputs['Rotation'] + input_scale: bpy.types.NodeSocket = node.inputs['Scale'] + out: str = parse_vector_input(input_vector) if input_vector.is_linked else to_vec3(input_vector.default_value) + location: str = parse_vector_input(input_location) if input_location.is_linked else to_vec3(input_location.default_value) + rotation: str = parse_vector_input(input_rotation) if input_rotation.is_linked else to_vec3(input_rotation.default_value) + scale: str = parse_vector_input(input_scale) if input_scale.is_linked else to_vec3(input_scale.default_value) + + # Use inner functions because the order of operations varies between mapping node vector types. This adds a + # slight overhead but makes the code much more readable. + # "Point" and "Vector" use Scale -> Rotate -> Translate, "Texture" uses Translate -> Rotate -> Scale + def calc_location(output: str) -> str: + # Vectors and Eulers support the "!=" operator + if input_scale.is_linked or input_scale.default_value != Vector((1, 1, 1)): + if node.vector_type == 'TEXTURE': + output = f'({output} / {scale})' + else: + output = f'({output} * {scale})' + + return output + + def calc_scale(output: str) -> str: + if input_location.is_linked or input_location.default_value != Vector((0, 0, 0)): + # z location is a little off sometimes?... + if node.vector_type == 'TEXTURE': + output = f'({output} - {location})' + else: + output = f'({output} + {location})' + return output + + out = calc_location(out) if node.vector_type == 'TEXTURE' else calc_scale(out) + + if input_rotation.is_linked or input_rotation.default_value != Euler((0, 0, 0)): + if node.vector_type == 'TEXTURE': + curshader.write(f'mat3 rotationX = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));') + curshader.write(f'mat3 rotationY = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));') + curshader.write(f'mat3 rotationZ = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);') + else: + # A little bit redundant, but faster than 12 more multiplications to make it work dynamically + curshader.write(f'mat3 rotationX = mat3(1.0, 0.0, 0.0, 0.0, cos(-{rotation}.x), sin(-{rotation}.x), 0.0, -sin(-{rotation}.x), cos(-{rotation}.x));') + curshader.write(f'mat3 rotationY = mat3(cos(-{rotation}.y), 0.0, -sin(-{rotation}.y), 0.0, 1.0, 0.0, sin(-{rotation}.y), 0.0, cos(-{rotation}.y));') + curshader.write(f'mat3 rotationZ = mat3(cos(-{rotation}.z), sin(-{rotation}.z), 0.0, -sin(-{rotation}.z), cos(-{rotation}.z), 0.0, 0.0, 0.0, 1.0);') + + # XYZ-order euler rotation + out = f'{out} * rotationX * rotationY * rotationZ' + + out = calc_scale(out) if node.vector_type == 'TEXTURE' else calc_location(out) - if location[0] != 0.0 or location[1] != 0.0 or location[2] != 0.0: - out = '({0} + vec3({1}, {2}, {3}))'.format(out, location[0], location[1], location[2]) - # use Extension parameter from the Texture node instead - # if node.use_min: - # out = 'max({0}, vec3({1}, {2}, {3}))'.format(out, node.min[0], node.min[1]) - # if node.use_max: - # out = 'min({0}, vec3({1}, {2}, {3}))'.format(out, node.max[0], node.max[1]) return out elif node.type == 'NORMAL': From a9d2a10d0bff0be86d44eb2bad7c6492b7691ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 11 Apr 2020 17:49:17 +0200 Subject: [PATCH 069/230] Fix ArrayLoopNode socket (dynamic value) + add "Index" output --- Sources/armory/logicnode/ArrayLoopNode.hx | 9 +++++++-- blender/arm/logicnode/logic_array_loop.py | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/armory/logicnode/ArrayLoopNode.hx b/Sources/armory/logicnode/ArrayLoopNode.hx index eb993e90..d5a90303 100644 --- a/Sources/armory/logicnode/ArrayLoopNode.hx +++ b/Sources/armory/logicnode/ArrayLoopNode.hx @@ -3,6 +3,7 @@ package armory.logicnode; class ArrayLoopNode extends LogicNode { var value: Dynamic; + var index: Int; public function new(tree: LogicTree) { super(tree); @@ -12,8 +13,10 @@ class ArrayLoopNode extends LogicNode { var ar: Array = inputs[1].get(); if (ar == null) return; + index = -1; for (val in ar) { value = val; + index++; runOutput(0); if (tree.loopBreak) { @@ -21,10 +24,12 @@ class ArrayLoopNode extends LogicNode { break; } } - runOutput(2); + runOutput(3); } override function get(from: Int): Dynamic { - return value; + if (from == 1) + return value; + return index; } } diff --git a/blender/arm/logicnode/logic_array_loop.py b/blender/arm/logicnode/logic_array_loop.py index 4e3d161c..5ca6bd33 100644 --- a/blender/arm/logicnode/logic_array_loop.py +++ b/blender/arm/logicnode/logic_array_loop.py @@ -8,12 +8,13 @@ class ArrayLoopNode(Node, ArmLogicTreeNode): bl_idname = 'LNArrayLoopNode' bl_label = 'Array Loop' bl_icon = 'CURVE_PATH' - + def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') self.inputs.new('ArmNodeSocketArray', 'Array') self.outputs.new('ArmNodeSocketAction', 'Loop') - self.outputs.new('NodeSocketInt', 'Value') + self.outputs.new('NodeSocketShader', 'Value') + self.outputs.new('NodeSocketInt', 'Index') self.outputs.new('ArmNodeSocketAction', 'Done') add_node(ArrayLoopNode, category='Logic') From dda23963131b5e09e856f5b938f946b642669e2d Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 11 Apr 2020 18:33:53 +0200 Subject: [PATCH 070/230] Added set checkbox and OnCanvasElement Node --- .../armory/logicnode/CanvasSetCheckBoxNode.hx | 42 ++++++++++++ .../armory/logicnode/OnCanvasElementNode.hx | 64 +++++++++++++++++++ blender/arm/logicnode/canvas_set_checkbox.py | 18 ++++++ .../arm/logicnode/input_on_canvas_element.py | 30 +++++++++ 4 files changed, 154 insertions(+) create mode 100644 Sources/armory/logicnode/CanvasSetCheckBoxNode.hx create mode 100644 Sources/armory/logicnode/OnCanvasElementNode.hx create mode 100644 blender/arm/logicnode/canvas_set_checkbox.py create mode 100644 blender/arm/logicnode/input_on_canvas_element.py diff --git a/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx b/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx new file mode 100644 index 00000000..4dee6c3b --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetCheckBoxNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + var value: Bool; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + function update() { + if (!canvas.ready) return; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + canvas.getHandle(element).selected = value; + tree.removeUpdate(update); + } + catch (e: Dynamic) {} + + runOutput(0); + } + + override function run(from: Int) { + element = inputs[1].get(); + value = inputs[2].get(); + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + // Ensure canvas is ready + tree.notifyOnUpdate(update); + update(); + } +#end +} diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx new file mode 100644 index 00000000..313f5860 --- /dev/null +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -0,0 +1,64 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class OnCanvasElementNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + + // Ensure canvas is ready + tree.notifyOnUpdate(update); + } + +#if arm_ui + function update() { + + element = inputs[0].get(); + + if(!Scene.active.ready) return; + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if(canvas == null) return; + if (!canvas.ready) return; + if(canvas.getElement(element) == null) return; + + var mouse = iron.system.Input.getMouse(); + var b = false; + switch (property0) { + case "Down": + b = mouse.down(property1); + case "Started": + b = mouse.started(property1); + case "Released": + b = mouse.released(property1); + } + if (b) + { + + var x1 = canvas.getElement(element).x; + var y1 = canvas.getElement(element).y; + var x2 = x1 + canvas.getElement(element).width; + var y2 = y1 + canvas.getElement(element).height; + + var mouseX = mouse.x; + var mouseY = mouse.y; + + if((mouseX >= x1) && (mouseX <= x2)) + { + if((mouseY >= y1) && (mouseY <= y2)) + { + runOutput(0); + } + } + } + } +#end +} diff --git a/blender/arm/logicnode/canvas_set_checkbox.py b/blender/arm/logicnode/canvas_set_checkbox.py new file mode 100644 index 00000000..1ddaee21 --- /dev/null +++ b/blender/arm/logicnode/canvas_set_checkbox.py @@ -0,0 +1,18 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class CanvasSetCheckBoxNode(Node, ArmLogicTreeNode): + '''Set canvas check box''' + bl_idname = 'LNCanvasSetCheckBoxNode' + bl_label = 'Canvas Set Check Box' + bl_icon = 'QUESTION' + + def init(self, context): + self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('NodeSocketString', 'Element') + self.inputs.new('NodeSocketBool', 'Value') + self.outputs.new('ArmNodeSocketAction', 'Out') + +add_node(CanvasSetCheckBoxNode, category='Canvas') diff --git a/blender/arm/logicnode/input_on_canvas_element.py b/blender/arm/logicnode/input_on_canvas_element.py new file mode 100644 index 00000000..9076b72f --- /dev/null +++ b/blender/arm/logicnode/input_on_canvas_element.py @@ -0,0 +1,30 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class OnCanvasElementNode(Node, ArmLogicTreeNode): + '''On canvas element node''' + bl_idname = 'LNOnCanvasElementNode' + bl_label = 'On Canvas Element' + bl_icon = 'CURVE_PATH' + property0: EnumProperty( + items = [('Down', 'Down', 'Down'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released')], + name='', default='Down') + property1: EnumProperty( + items = [('left', 'left', 'left'), + ('right', 'right', 'right'), + ('middle', 'middle', 'middle')], + name='Mouse button', default='left') + + def init(self, context): + self.inputs.new('NodeSocketString','Element') + self.outputs.new('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + +add_node(OnCanvasElementNode, category='Input') From f5c624895a56731cad2047257dfdbfcd5d6f0fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 10 Apr 2020 20:43:59 +0200 Subject: [PATCH 071/230] Cherry Pick "Add "fake user" setting for traits" # Resolved Conflicts: # blender/arm/exporter.py --- Sources/armory/logicnode/TraitNode.hx | 1 + blender/arm/exporter.py | 6 +++++- blender/arm/props_traits.py | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/TraitNode.hx b/Sources/armory/logicnode/TraitNode.hx index f5b29a63..245216ac 100644 --- a/Sources/armory/logicnode/TraitNode.hx +++ b/Sources/armory/logicnode/TraitNode.hx @@ -14,6 +14,7 @@ class TraitNode extends LogicNode { var cname = Type.resolveClass(Main.projectPackage + "." + property0); if (cname == null) cname = Type.resolveClass(Main.projectPackage + ".node." + property0); + if (cname == null) throw 'No trait with the name "$property0" found, make sure that the trait is exported!'; value = Type.createInstance(cname, []); return value; } diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 99120901..de932076 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2506,8 +2506,12 @@ class ArmoryExporter: def export_traits(self, bobject, o): if hasattr(bobject, 'arm_traitlist'): for t in bobject.arm_traitlist: - if not t.enabled_prop: + # Don't export disabled traits but still export those + # with fake user enabled so that nodes like `TraitNode` + # still work + if not t.enabled_prop and not t.fake_user: continue + x = {} if t.type_prop == 'Logic Nodes' and t.node_tree_prop is not None and t.node_tree_prop.name != '': x['type'] = 'Script' diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index fdf97bce..1e53c429 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -54,6 +54,7 @@ class ArmTraitListItem(bpy.types.PropertyGroup): name: StringProperty(name="Name", description="A name for this item", default="") enabled_prop: BoolProperty(name="", description="A name for this item", default=True, update=trigger_recompile) is_object: BoolProperty(name="", default=True) + fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False) type_prop: EnumProperty( items = [('Haxe Script', 'Haxe', 'Haxe Script'), ('WebAssembly', 'Wasm', 'WebAssembly'), @@ -88,12 +89,16 @@ class ARM_UL_TraitList(bpy.types.UIList): # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.prop(item, "enabled_prop") - layout.label(text=item.name, icon=custom_icon, icon_value=custom_icon_value) + # Display " " for props without a name to right-align the + # fake_user button + layout.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) + layout.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") + class ArmTraitListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "arm_traitlist.new_item" From 4a3d3a58d6b042c6b739a806f01dc2e5de8685ef Mon Sep 17 00:00:00 2001 From: "Daniel B. Bruno" Date: Sat, 11 Apr 2020 17:57:21 -0300 Subject: [PATCH 072/230] Add "Has contact (Array)" node This node was requested on issue #1256. --- .../armory/logicnode/HasContactArrayNode.hx | 35 +++++++++++++++++++ .../logicnode/physics_has_contact_array.py | 17 +++++++++ 2 files changed, 52 insertions(+) create mode 100644 Sources/armory/logicnode/HasContactArrayNode.hx create mode 100644 blender/arm/logicnode/physics_has_contact_array.py diff --git a/Sources/armory/logicnode/HasContactArrayNode.hx b/Sources/armory/logicnode/HasContactArrayNode.hx new file mode 100644 index 00000000..b5efcdb8 --- /dev/null +++ b/Sources/armory/logicnode/HasContactArrayNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class HasContactArrayNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object1: Object = inputs[0].get(); + var objects: Array = inputs[1].get(); + if (object1 == null || objects == null) return false; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb1 = object1.getTrait(RigidBody); + var rbs = physics.getContacts(rb1); + + if (rb1 != null && rbs != null) { + for (object2 in objects) { + var rb2 = object2.getTrait(RigidBody); + for (rb in rbs) { + if (rb == rb2) { + return true; + } + } + } + } +#end + return false; + } +} diff --git a/blender/arm/logicnode/physics_has_contact_array.py b/blender/arm/logicnode/physics_has_contact_array.py new file mode 100644 index 00000000..1738c663 --- /dev/null +++ b/blender/arm/logicnode/physics_has_contact_array.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class HasContactArrayNode(Node, ArmLogicTreeNode): + '''Has contact array node''' + bl_idname = 'LNHasContactArrayNode' + bl_label = 'Has Contact (Array)' + bl_icon = 'QUESTION' + + def init(self, context): + self.inputs.new('ArmNodeSocketObject', 'Object 1') + self.inputs.new('ArmNodeSocketArray', 'Objects') + self.outputs.new('NodeSocketBool', 'Bool') + +add_node(HasContactArrayNode, category='Physics') From a4dba65a7a28626e72cf20d7202c38b4d9e3df46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 17:32:52 +0200 Subject: [PATCH 073/230] "Fix" vector node --- Sources/armory/logicnode/VectorNode.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/armory/logicnode/VectorNode.hx b/Sources/armory/logicnode/VectorNode.hx index 7e0c11ab..2c186907 100644 --- a/Sources/armory/logicnode/VectorNode.hx +++ b/Sources/armory/logicnode/VectorNode.hx @@ -17,6 +17,7 @@ class VectorNode extends LogicNode { } override function get(from: Int): Dynamic { + value = new Vec4(); value.x = inputs[0].get(); value.y = inputs[1].get(); value.z = inputs[2].get(); From 247e0b4c3bd36c8f805c82ed23cbd464aa4ff2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 17:33:27 +0200 Subject: [PATCH 074/230] Refactor ArrayAddNode + combine it with ArrayAddUnique --- Sources/armory/logicnode/ArrayAddNode.hx | 25 +++++++++++++--- .../armory/logicnode/ArrayAddUniqueNode.hx | 22 -------------- blender/arm/logicnode/array_add.py | 5 +++- blender/arm/logicnode/array_add_unique.py | 30 ------------------- 4 files changed, 25 insertions(+), 57 deletions(-) delete mode 100644 Sources/armory/logicnode/ArrayAddUniqueNode.hx delete mode 100644 blender/arm/logicnode/array_add_unique.py diff --git a/Sources/armory/logicnode/ArrayAddNode.hx b/Sources/armory/logicnode/ArrayAddNode.hx index fb4787ec..01def102 100644 --- a/Sources/armory/logicnode/ArrayAddNode.hx +++ b/Sources/armory/logicnode/ArrayAddNode.hx @@ -2,21 +2,38 @@ package armory.logicnode; class ArrayAddNode extends LogicNode { + var ar: Array; + public function new(tree: LogicTree) { super(tree); } override function run(from: Int) { - var ar: Array = inputs[1].get(); + ar = inputs[1].get(); if (ar == null) return; - if (inputs.length > 2) { - for (i in 2...inputs.length) { + // "Modify Original" == `false` -> Copy the input array + if (!inputs[3].get()) { + ar = ar.copy(); + } + + if (inputs.length > 4) { + for (i in 4...inputs.length) { var value: Dynamic = inputs[i].get(); - ar.push(value); + + // "Unique Values" options only supports primitive data types + // for now, a custom indexOf() or contains() method would be + // required to compare values of other types + if (!inputs[2].get() || ar.indexOf(value) == -1) { + ar.push(value); + } } } runOutput(0); } + + override function get(from: Int): Dynamic { + return ar; + } } diff --git a/Sources/armory/logicnode/ArrayAddUniqueNode.hx b/Sources/armory/logicnode/ArrayAddUniqueNode.hx deleted file mode 100644 index fafde3d6..00000000 --- a/Sources/armory/logicnode/ArrayAddUniqueNode.hx +++ /dev/null @@ -1,22 +0,0 @@ -package armory.logicnode; - -class ArrayAddUniqueNode extends LogicNode { - - public function new(tree: LogicTree) { - super(tree); - } - - override function run(from: Int) { - var ar: Array = inputs[1].get(); - if (ar == null) return; - - if (inputs.length > 2) { - for (i in 2...inputs.length) { - var value: Dynamic = inputs[i].get(); - if (ar.indexOf(value) == -1) ar.push(value); - } - } - - runOutput(0); - } -} diff --git a/blender/arm/logicnode/array_add.py b/blender/arm/logicnode/array_add.py index 8797c6e0..2eeb47e9 100644 --- a/blender/arm/logicnode/array_add.py +++ b/blender/arm/logicnode/array_add.py @@ -15,13 +15,16 @@ class ArrayAddNode(Node, ArmLogicTreeNode): def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') self.inputs.new('ArmNodeSocketArray', 'Array') + self.inputs.new('NodeSocketBool', 'Unique Values') + self.inputs.new('NodeSocketBool', 'Modify Original').default_value = True self.inputs.new('NodeSocketShader', 'Value') self.outputs.new('ArmNodeSocketAction', 'Out') + self.outputs.new('ArmNodeSocketArray', 'Array') def draw_buttons(self, context, layout): row = layout.row(align=True) - op = row.operator('arm.node_add_input_value', text='New', icon='PLUS', emboss=True) + op = row.operator('arm.node_add_input_value', text='Add Input', icon='PLUS', emboss=True) op.node_index = str(id(self)) op.socket_type = 'NodeSocketShader' op2 = row.operator('arm.node_remove_input_value', text='', icon='X', emboss=True) diff --git a/blender/arm/logicnode/array_add_unique.py b/blender/arm/logicnode/array_add_unique.py deleted file mode 100644 index e6048426..00000000 --- a/blender/arm/logicnode/array_add_unique.py +++ /dev/null @@ -1,30 +0,0 @@ -import bpy -from bpy.props import * -from bpy.types import Node, NodeSocket -from arm.logicnode.arm_nodes import * - -class ArrayAddUniqueNode(Node, ArmLogicTreeNode): - '''Array add unique node''' - bl_idname = 'LNArrayAddUniqueNode' - bl_label = 'Array Add Unique' - bl_icon = 'QUESTION' - - def __init__(self): - array_nodes[str(id(self))] = self - - def init(self, context): - self.inputs.new('ArmNodeSocketAction', 'In') - self.inputs.new('ArmNodeSocketArray', 'Array') - self.inputs.new('NodeSocketShader', 'Value') - self.outputs.new('ArmNodeSocketAction', 'Out') - - def draw_buttons(self, context, layout): - row = layout.row(align=True) - - op = row.operator('arm.node_add_input_value', text='New', icon='PLUS', emboss=True) - op.node_index = str(id(self)) - op.socket_type = 'NodeSocketShader' - op2 = row.operator('arm.node_remove_input_value', text='', icon='X', emboss=True) - op2.node_index = str(id(self)) - -add_node(ArrayAddUniqueNode, category='Array') From 9f99f05db4385cf4e617838c76c8f3b2972d7901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:03:27 +0200 Subject: [PATCH 075/230] Smaller improvements and more type annotations --- blender/arm/exporter.py | 63 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index de932076..38ed1f94 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -15,7 +15,7 @@ from enum import Enum, unique import math import os import time -from typing import Any, Dict, Union +from typing import Any, Dict, List, Union import numpy as np @@ -76,8 +76,8 @@ class ArmoryExporter: compress_enabled = False optimize_enabled = False - # Referenced traits - import_traits = [] + # Class names of referenced traits + import_traits: List[str] = [] def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): global current_output @@ -86,8 +86,7 @@ class ArmoryExporter: self.scene = context.scene if scene is None else scene self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph - self.output = {} - self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) + self.output = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} current_output = self.output # Stores the object type ("objectType") and the asset name @@ -121,7 +120,7 @@ class ArmoryExporter: @classmethod def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None) -> None: - """Exports the given scene to the given filepath. This is the + """Exports the given scene to the given file path. This is the function that is called in make.py and the entry point of the exporter.""" cls(context, filepath, scene, depsgraph).execute() @@ -131,7 +130,8 @@ class ArmoryExporter: wrd = bpy.data.worlds['Arm'] cls.export_all_flag = True - cls.export_physics = False # Indicates whether rigid body is exported + # Indicates whether rigid body is exported + cls.export_physics = False if wrd.arm_physics == 'Enabled': cls.export_physics = True cls.export_navigation = False @@ -196,9 +196,9 @@ class ArmoryExporter: self.export_bone_transform(armature, bone, scene, o, action) o['children'] = [] - for subbobject in bone.children: + for sub_bobject in bone.children: so = {} - self.export_bone(armature, subbobject, scene, so, action) + self.export_bone(armature, sub_bobject, scene, so, action) o['children'].append(so) @staticmethod @@ -221,7 +221,7 @@ class ArmoryExporter: # Font out if animation_flag: - if not 'object_actions' in o: + if 'object_actions' not in o: o['object_actions'] = [] action = bobject.animation_data.action @@ -261,7 +261,7 @@ class ArmoryExporter: oanim['tracks'] = [tracko] self.export_pose_markers(oanim, action) - if True: #not action.arm_cached or not os.path.exists(fp): + if True: # not action.arm_cached or not os.path.exists(fp): wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting object action ' + aname) @@ -313,7 +313,7 @@ class ArmoryExporter: return data_ttrack - def export_object_transform(self, bobject, o): + def export_object_transform(self, bobject: bpy.types.Object, o): # Internal target names for single FCurve data paths target_names = { "location": ("xloc", "yloc", "zloc"), @@ -398,7 +398,7 @@ class ArmoryExporter: for subbobject in bone.children: self.process_bone(subbobject) - def process_bobject(self, bobject): + def process_bobject(self, bobject: bpy.types.Object : bpy.types.Objectb: bpy.types.Objectp: bpy.types.Objecty: bpy.types.Object.: bpy.types.Objecttypes.Object): """Adds the given blender object to the bobject_array dict and stores its type and its name. @@ -470,7 +470,7 @@ class ArmoryExporter: tracko['values'] = [] self.bone_tracks.append((tracko['values'], pose_bone)) - def use_default_material(self, bobject, o): + def use_default_material(self, bobject: bpy.types.Object, o): if arm.utils.export_bone_data(bobject): o['material_refs'].append('armdefaultskin') self.default_skin_material_objects.append(bobject) @@ -489,28 +489,29 @@ class ArmoryExporter: o = self.object_to_arm_object_dict[po] if len(o['material_refs']) > 0 and o['material_refs'][0] == 'armdefault' and po not in self.default_part_material_objects: self.default_part_material_objects.append(po) - o['material_refs'] = ['armdefaultpart'] # Replace armdefault + o['material_refs'] = ['armdefaultpart'] # Replace armdefault - def export_material_ref(self, bobject, material, index, o): - if material is None: # Use default for empty mat slots + def export_material_ref(self, bobject: bpy.types.Object, material, index, o): + if material is None: # Use default for empty mat slots self.use_default_material(bobject, o) return - if not material in self.material_array: + if material not in self.material_array: self.material_array.append(material) o['material_refs'].append(arm.utils.asset_name(material)) def export_particle_system_ref(self, psys, index, o): - if psys.settings in self.particle_system_array: # or not modifier.show_render: + if psys.settings in self.particle_system_array: # or not modifier.show_render: return if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT': return self.particle_system_array[psys.settings] = {"structName": psys.settings.name} - pref = {} - pref['name'] = psys.name - pref['seed'] = psys.seed - pref['particle'] = psys.settings.name + pref = { + 'name': psys.name, + 'seed': psys.seed, + 'particle': psys.settings.name + } o['particle_refs'].append(pref) @staticmethod @@ -971,7 +972,7 @@ class ArmoryExporter: for subbobject in bobject.children: self.export_object(subbobject, scene, object_export_data) - def export_skin(self, bobject, armature, exportMesh, o): + def export_skin(self, bobject: bpy.types.Object, armature, exportMesh, o): # This function exports all skinning data, which includes the skeleton # and per-vertex bone influence data oskin = {} @@ -1070,7 +1071,7 @@ class ArmoryExporter: oskin['constraints'] = [] self.add_constraints(bone, oskin, bone=True) - def write_mesh(self, bobject, fp, o): + def write_mesh(self, bobject: bpy.types.Object, fp, o): wrd = bpy.data.worlds['Arm'] if wrd.arm_single_data_file: self.output['mesh_datas'].append(o) @@ -1089,7 +1090,7 @@ class ArmoryExporter: abs((bobject.bound_box[6][2] - bobject.bound_box[0][2]) / 2 + abs(aabb_center[2])) * 2 \ ] - def export_mesh_data(self, exportMesh, bobject, o, has_armature=False): + def export_mesh_data(self, exportMesh, bobject: bpy.types.Object, o, has_armature=False): exportMesh.calc_normals_split() exportMesh.calc_loop_triangles() @@ -2222,7 +2223,7 @@ class ArmoryExporter: # Remove created material variants for slot in matslots: # Set back to original material - orig_mat = bpy.data.materials[slot.material.name[:-8]] # _armskin, _armpart, _armtile + orig_mat = bpy.data.materials[slot.material.name[:-8]] # _armskin, _armpart, _armtile orig_mat.export_uvs = slot.material.export_uvs orig_mat.export_vcols = slot.material.export_vcols orig_mat.export_tangents = slot.material.export_tangents @@ -2351,7 +2352,7 @@ class ArmoryExporter: return instanced_type, instanced_data - def post_export_object(self, bobject, o, type): + def post_export_object(self, bobject: bpy.types.Object, o, type): # Export traits self.export_traits(bobject, o) @@ -2503,7 +2504,7 @@ class ArmoryExporter: co['influence'] = con.influence o['constraints'].append(co) - def export_traits(self, bobject, o): + def export_traits(self, bobject: bpy.types.Object, o): if hasattr(bobject, 'arm_traitlist'): for t in bobject.arm_traitlist: # Don't export disabled traits but still export those @@ -2620,7 +2621,7 @@ class ArmoryExporter: pass assets.add(file_theme) - def add_softbody_mod(self, o, bobject, soft_mod, soft_type): + def add_softbody_mod(self, o, bobject: bpy.types.Object, soft_mod, soft_type): ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' assets.add_khafile_def('arm_physics_soft') @@ -2637,7 +2638,7 @@ class ArmoryExporter: self.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) @staticmethod - def add_hook_mod(o, bobject, target_name, group_name): + def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' trait = {} From 3b1437aca5366e76b5005dcdcf40fb045f26f4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:04:16 +0200 Subject: [PATCH 076/230] Fix debug console look (again) --- Sources/armory/trait/internal/DebugConsole.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 3c589e24..4062cd17 100755 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -175,7 +175,7 @@ class DebugConsole extends Trait { // Draw line that shows parent relations ui.g.color = ui.t.ACCENT_COL; - ui.g.drawLine(ui._x - 16, ui._y + ui.ELEMENT_H() / 2, ui._x, ui._y + ui.ELEMENT_H() / 2); + ui.g.drawLine(ui._x - 10, ui._y + ui.ELEMENT_H() / 2, ui._x, ui._y + ui.ELEMENT_H() / 2); ui.g.color = 0xffffffff; ui.text(currentObject.name); From da07e721277880ec5f0e2fb03db2de7d6ce1063e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:04:28 +0200 Subject: [PATCH 077/230] Add icon for "Open Editor" operator --- blender/arm/props_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index bea19b16..eb7d7088 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -356,7 +356,7 @@ class ARM_PT_ArmoryProjectPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False row = layout.row(align=True) - row.operator("arm.open_editor") + row.operator("arm.open_editor", icon="DESKTOP") row.operator("arm.open_project_folder", icon="FILE_FOLDER") class ARM_PT_ProjectFlagsPanel(bpy.types.Panel): @@ -1358,6 +1358,7 @@ class ArmProxyToggleAllButton(bpy.types.Operator): class ArmProxyApplyAllButton(bpy.types.Operator): bl_idname = 'arm.proxy_apply_all' bl_label = 'Apply to All' + def execute(self, context): for obj in bpy.data.objects: if obj.proxy == None: From 22faa147b29b7a2ea5e367e55c7e16a520c5b5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:05:03 +0200 Subject: [PATCH 078/230] Fix possible error with sublime text project generation --- blender/arm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 318a3717..22648dae 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -690,7 +690,7 @@ def open_editor(hx_path=None): # Sublime Text if get_code_editor() == 'sublime': - project_name = bpy.data.worlds['Arm'].arm_project_name + project_name = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_name) subl_project_path = arm.utils.get_fp() + f'/{project_name}.sublime-project' if not os.path.exists(subl_project_path): From 2d309c042ea820edb06606d934fc4fa3c5d6ff51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:09:15 +0200 Subject: [PATCH 079/230] Fix exporter (editor mistake) --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 38ed1f94..3ade0480 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -398,7 +398,7 @@ class ArmoryExporter: for subbobject in bone.children: self.process_bone(subbobject) - def process_bobject(self, bobject: bpy.types.Object : bpy.types.Objectb: bpy.types.Objectp: bpy.types.Objecty: bpy.types.Object.: bpy.types.Objecttypes.Object): + def process_bobject(self, bobject: bpy.types.Object): """Adds the given blender object to the bobject_array dict and stores its type and its name. From a5b6a2308e7207363f0b45f25a29bd8bb789da61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:44:49 +0200 Subject: [PATCH 080/230] Refactor process_bobject(), process_bone() and parts of execute() --- blender/arm/exporter.py | 77 ++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 3ade0480..20ea00e4 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -75,7 +75,12 @@ class ArmoryExporter: """Export to Armory format""" compress_enabled = False + export_all_flag = True + # Indicates whether rigid body is exported + export_physics = False optimize_enabled = False + option_mesh_only = False + # Class names of referenced traits import_traits: List[str] = [] @@ -86,7 +91,7 @@ class ArmoryExporter: self.scene = context.scene if scene is None else scene self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph - self.output = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} + self.output: Dict[str, Any] = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} current_output = self.output # Stores the object type ("objectType") and the asset name @@ -129,16 +134,12 @@ class ArmoryExporter: def preprocess(cls): wrd = bpy.data.worlds['Arm'] - cls.export_all_flag = True - # Indicates whether rigid body is exported - cls.export_physics = False if wrd.arm_physics == 'Enabled': cls.export_physics = True cls.export_navigation = False if wrd.arm_navigation == 'Enabled': cls.export_navigation = True cls.export_ui = False - cls.option_mesh_only = False @staticmethod def write_matrix(matrix): @@ -393,20 +394,22 @@ class ArmoryExporter: def process_bone(self, bone): if ArmoryExporter.export_all_flag or bone.select: - self.bobject_bone_array[bone] = {"objectType" : NodeType.BONE, "structName" : bone.name} + self.bobject_bone_array[bone] = { + "objectType": NodeType.BONE, + "structName": bone.name + } for subbobject in bone.children: self.process_bone(subbobject) def process_bobject(self, bobject: bpy.types.Object): - """Adds the given blender object to the bobject_array dict and - stores its type and its name. - - If an object is linked, the name of its library is appended - after an "_". + """Stores some basic information about the given object (its + name and type). + If the given object is an armature, its bones are also + processed. """ - if ArmoryExporter.export_all_flag or bobject.select: - btype = NodeType.get_bobject_type(bobject) + if ArmoryExporter.export_all_flag or bobject.select_get(): + btype: NodeType = NodeType.get_bobject_type(bobject) if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only: return @@ -417,9 +420,9 @@ class ArmoryExporter: } if bobject.type == "ARMATURE": - skeleton = bobject.data - if skeleton: - for bone in skeleton.bones: + armature: bpy.types.Armature = bobject.data + if armature: + for bone in armature.bones: if not bone.parent: self.process_bone(bone) @@ -673,9 +676,10 @@ class ArmoryExporter: # Linked object, not present in scene if bobject not in self.object_to_arm_object_dict: - object_export_data: Dict[str, Any] = {} - object_export_data['traits'] = [] - object_export_data['spawn'] = False + object_export_data = { + 'traits': [], + 'spawn': False + } self.object_to_arm_object_dict[bobject] = object_export_data object_export_data = self.object_to_arm_object_dict[bobject] @@ -949,9 +953,9 @@ class ArmoryExporter: action_obj['objects'] = bones arm.utils.write_arm(fp, action_obj) - #restore settings + # restore settings skelobj.animation_data.action = orig_action - for a in baked_actions: bpy.data.actions.remove( a, do_unlink=True) + for a in baked_actions: bpy.data.actions.remove(a, do_unlink=True) if 'unlink' in clear_op: bpy.context.collection.objects.unlink(skelobj) if 'rem' in clear_op: bpy.data.objects.remove(skelobj, do_unlink=True) @@ -1551,7 +1555,7 @@ class ArmoryExporter: asset_name = arm.utils.asset_name(bobject) if collection.library is None: - #collection is in the same file, but (likely) on another scene + # collection is in the same file, but (likely) on another scene if asset_name not in scene_objects: self.process_bobject(bobject) self.export_object(bobject, self.scene) @@ -1987,13 +1991,14 @@ class ArmoryExporter: current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe scene_objects = self.scene.collection.all_objects.values() + # bobject = blender object for bobject in scene_objects: - # Map objects to game objects - o = {} - o['traits'] = [] - self.object_to_arm_object_dict[bobject] = o + # Initialize object export data (map objects to game objects) + object_export_data: Dict[str, Any] = {'traits': []} + self.object_to_arm_object_dict[bobject] = object_export_data + # Process - # Skip objects that have a parent because children will be exported recursively + # Skip objects that have a parent because children are processed recursively if not bobject.parent: self.process_bobject(bobject) # Softbody needs connected triangles, use optimized geometry export @@ -2012,9 +2017,9 @@ class ArmoryExporter: # Create unique material variants for skinning, tilesheets, particles matvars = [] matslots = [] - for bo in scene_objects: - if arm.utils.export_bone_data(bo): - for slot in bo.material_slots: + for bobject in scene_objects: + if arm.utils.export_bone_data(bobject): + for slot in bobject.material_slots: if slot.material is None or slot.material.library is not None: continue if slot.material.name.endswith('_armskin'): @@ -2045,10 +2050,10 @@ class ArmoryExporter: # Particle and non-particle objects can not share material for psys in bpy.data.particles: - bo = psys.instance_object - if bo is None or psys.render_type != 'OBJECT': + bobject = psys.instance_object + if bobject is None or psys.render_type != 'OBJECT': continue - for slot in bo.material_slots: + for slot in bobject.material_slots: if slot.material is None or slot.material.library is not None: continue if slot.material.name.endswith('_armpart'): @@ -2100,10 +2105,10 @@ class ArmoryExporter: self.output['terrain_ref'] = 'Terrain' self.output['objects'] = [] - for bo in scene_objects: + for bobject in scene_objects: # Skip objects that have a parent because children will be exported recursively - if not bo.parent: - self.export_object(bo, self.scene) + if not bobject.parent: + self.export_object(bobject, self.scene) if bpy.data.collections: self.output['groups'] = [] From c631676dcd20f3086bccb25a76eebe149465aabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 20:59:52 +0200 Subject: [PATCH 081/230] Fix exporter again --- blender/arm/exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 20ea00e4..b5a39d9c 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2032,8 +2032,8 @@ class ArmoryExporter: mat.name = mat_name matvars.append(mat) slot.material = mat - elif bo.arm_tilesheet != '': - for slot in bo.material_slots: + elif bobject.arm_tilesheet != '': + for slot in bobject.material_slots: if slot.material is None or slot.material.library is not None: continue if slot.material.name.endswith('_armtile'): From deba93d3daae22fc0f9ea3808a17c289331e229f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 21:16:16 +0200 Subject: [PATCH 082/230] Refactor bone processing --- blender/arm/exporter.py | 43 ++++++++++++++++++++++++----------------- blender/arm/utils.py | 5 ++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b5a39d9c..628db29e 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -15,7 +15,7 @@ from enum import Enum, unique import math import os import time -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Tuple, Union import numpy as np @@ -169,10 +169,12 @@ class ArmoryExporter: return shape_keys return None - def find_bone(self, name: str): - for bobject_ref in self.bobject_bone_array.items(): - if bobject_ref[0].name == name: - return bobject_ref + def find_bone(self, name: str) -> Tuple[bpy.types.Bone, Dict] | None: + """Finds the bone reference (a tuple containing the bone object + and its data) by the given name and returns it.""" + for bone_ref in self.bobject_bone_array.items(): + if bone_ref[0].name == name: + return bone_ref return None @staticmethod @@ -392,7 +394,7 @@ class ArmoryExporter: oaction['transform'] = None arm.utils.write_arm(fp, actionf) - def process_bone(self, bone): + def process_bone(self, bone: bpy.types.Bone) -> None: if ArmoryExporter.export_all_flag or bone.select: self.bobject_bone_array[bone] = { "objectType": NodeType.BONE, @@ -402,7 +404,7 @@ class ArmoryExporter: for subbobject in bone.children: self.process_bone(subbobject) - def process_bobject(self, bobject: bpy.types.Object): + def process_bobject(self, bobject: bpy.types.Object) -> None: """Stores some basic information about the given object (its name and type). If the given object is an armature, its bones are also @@ -431,15 +433,18 @@ class ArmoryExporter: self.process_bobject(subbobject) def process_skinned_meshes(self): - for bobjectRef in self.bobject_array.items(): - if bobjectRef[1]["objectType"] is NodeType.MESH: - armature = bobjectRef[0].find_armature() - if armature: + """Iterates through all objects that are exported and ensures + that bones are actually stored as bones.""" + for bobject_ref in self.bobject_array.items(): + if bobject_ref[1]["objectType"] is NodeType.MESH: + armature = bobject_ref[0].find_armature() + if armature is not None: for bone in armature.data.bones: - boneRef = self.find_bone(bone.name) - if boneRef: - # If an object is used as a bone, then we force its type to be a bone - boneRef[1]["objectType"] = NodeType.BONE + bone_ref = self.find_bone(bone.name) + if bone_ref is not None: + # If an object is used as a bone, then we + # force its type to be a bone + bone_ref[1]["objectType"] = NodeType.BONE def export_bone_transform(self, armature, bone, scene, o, action): @@ -1990,7 +1995,7 @@ class ArmoryExporter: current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe - scene_objects = self.scene.collection.all_objects.values() + scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() # bobject = blender object for bobject in scene_objects: # Initialize object export data (map objects to game objects) @@ -1998,10 +2003,12 @@ class ArmoryExporter: self.object_to_arm_object_dict[bobject] = object_export_data # Process - # Skip objects that have a parent because children are processed recursively + # Skip objects that have a parent because children are + # processed recursively if not bobject.parent: self.process_bobject(bobject) - # Softbody needs connected triangles, use optimized geometry export + # Softbody needs connected triangles, use optimized + # geometry export for mod in bobject.modifiers: if mod.type == 'CLOTH' or mod.type == 'SOFT_BODY': ArmoryExporter.optimize_enabled = True diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 318a3717..f5bd8e6d 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -659,9 +659,12 @@ def is_bone_animation_enabled(bobject): return True return False -def export_bone_data(bobject): + +def export_bone_data(bobject: bpy.types.Object) -> bool: + """Returns whether the bone data of the given object should be exported.""" return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().arm_skin == 'On' + def open_editor(hx_path=None): ide_bin = get_ide_bin() From c7ea971ffbdb9c5f46471905ce98950da82aa14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 21:27:01 +0200 Subject: [PATCH 083/230] Fix find_bone annotations --- blender/arm/exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 628db29e..ad78b8d1 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -15,7 +15,7 @@ from enum import Enum, unique import math import os import time -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Tuple, Union, Optional import numpy as np @@ -169,7 +169,7 @@ class ArmoryExporter: return shape_keys return None - def find_bone(self, name: str) -> Tuple[bpy.types.Bone, Dict] | None: + def find_bone(self, name: str) -> Optional[Tuple[bpy.types.Bone, Dict]]: """Finds the bone reference (a tuple containing the bone object and its data) by the given name and returns it.""" for bone_ref in self.bobject_bone_array.items(): From eba49ef2f75312ca88057b2d0ea066519dc128c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 21:36:10 +0200 Subject: [PATCH 084/230] Refactor material variant generation --- blender/arm/exporter.py | 121 ++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index ad78b8d1..684dc1df 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -575,7 +575,71 @@ class ArmoryExporter: return False @staticmethod - def slot_to_material(bobject, slot): + def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Material], List[bpy.types.MaterialSlot]]: + """Creates unique material variants for skinning, tilesheets and + particles.""" + matvars: List[bpy.types.Material] = [] + matslots: List[bpy.types.MaterialSlot] = [] + + bobject: bpy.types.Object + for bobject in scene.collection.all_objects.values(): + variant_suffix = '' + + # Skinning + if arm.utils.export_bone_data(bobject): + variant_suffix = '_armskin' + # Tilesheets + elif bobject.arm_tilesheet != '': + variant_suffix = '_armtile' + + if variant_suffix == '': + continue + + for slot in bobject.material_slots: + if slot.material is None or slot.material.library is not None: + continue + if slot.material.name.endswith(variant_suffix): + continue + + matslots.append(slot) + mat_name = slot.material.name + variant_suffix + mat = bpy.data.materials.get(mat_name) + # Create material variant + if mat is None: + mat = slot.material.copy() + mat.name = mat_name + if variant_suffix == '_armtile': + mat.arm_tilesheet_flag = True + matvars.append(mat) + slot.material = mat + + # Particle and non-particle objects can not share material + particle_sys: bpy.types.ParticleSettings + for particle_sys in bpy.data.particles: + bobject = particle_sys.instance_object + if bobject is None or particle_sys.render_type != 'OBJECT': + continue + + for slot in bobject.material_slots: + if slot.material is None or slot.material.library is not None: + continue + if slot.material.name.endswith('_armpart'): + continue + + matslots.append(slot) + mat_name = slot.material.name + '_armpart' + mat = bpy.data.materials.get(mat_name) + if mat is None: + mat = slot.material.copy() + mat.name = mat_name + mat.arm_particle_flag = True + matvars.append(mat) + slot.material = mat + + return matvars, matslots + + @staticmethod + def slot_to_material(bobject: bpy.types.Object, slot: bpy.types.MaterialSlot): mat = slot.material # Pick up backed material if present if mat is not None: @@ -2021,59 +2085,8 @@ class ArmoryExporter: elif not bpy.data.worlds['Arm'].arm_minimize: self.output['name'] += '.json' - # Create unique material variants for skinning, tilesheets, particles - matvars = [] - matslots = [] - for bobject in scene_objects: - if arm.utils.export_bone_data(bobject): - for slot in bobject.material_slots: - if slot.material is None or slot.material.library is not None: - continue - if slot.material.name.endswith('_armskin'): - continue - matslots.append(slot) - mat_name = slot.material.name + '_armskin' - mat = bpy.data.materials.get(mat_name) - if mat is None: - mat = slot.material.copy() - mat.name = mat_name - matvars.append(mat) - slot.material = mat - elif bobject.arm_tilesheet != '': - for slot in bobject.material_slots: - if slot.material is None or slot.material.library is not None: - continue - if slot.material.name.endswith('_armtile'): - continue - matslots.append(slot) - mat_name = slot.material.name + '_armtile' - mat = bpy.data.materials.get(mat_name) - if mat is None: - mat = slot.material.copy() - mat.name = mat_name - mat.arm_tilesheet_flag = True - matvars.append(mat) - slot.material = mat - - # Particle and non-particle objects can not share material - for psys in bpy.data.particles: - bobject = psys.instance_object - if bobject is None or psys.render_type != 'OBJECT': - continue - for slot in bobject.material_slots: - if slot.material is None or slot.material.library is not None: - continue - if slot.material.name.endswith('_armpart'): - continue - matslots.append(slot) - mat_name = slot.material.name + '_armpart' - mat = bpy.data.materials.get(mat_name) - if mat is None: - mat = slot.material.copy() - mat.name = mat_name - mat.arm_particle_flag = True - matvars.append(mat) - slot.material = mat + # Create unique material variants for skinning, tilesheets and particles + matvars, matslots = self.create_material_variants(self.scene) # Auto-bones wrd = bpy.data.worlds['Arm'] From 42a458bbacbeb62bc2158d911ead667bd9c4f883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Apr 2020 21:46:21 +0200 Subject: [PATCH 085/230] Cleanup terrain export --- blender/arm/exporter.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 684dc1df..a1231898 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2100,28 +2100,34 @@ class ArmoryExporter: # Terrain if self.scene.arm_terrain_object is not None: - # Append trait - if not 'traits' in self.output: - self.output['traits'] = [] - trait = {} - trait['type'] = 'Script' - trait['class_name'] = 'armory.trait.internal.TerrainPhysics' - self.output['traits'].append(trait) - ArmoryExporter.import_traits.append(trait['class_name']) - ArmoryExporter.export_physics = True assets.add_khafile_def('arm_terrain') + + # Append trait + trait_export_data = { + 'type': 'Script', + 'class_name': 'armory.trait.internal.TerrainPhysics' + } + if 'traits' not in self.output: + self.output['traits']: List[Dict[str, str]] = [] + + self.output['traits'].append(trait_export_data) + + ArmoryExporter.import_traits.append(trait_export_data['class_name']) + ArmoryExporter.export_physics = True + # Export material mat = self.scene.arm_terrain_object.children[0].data.materials[0] self.material_array.append(mat) # Terrain data - terrain = {} - terrain['name'] = 'Terrain' - terrain['sectors_x'] = self.scene.arm_terrain_sectors[0] - terrain['sectors_y'] = self.scene.arm_terrain_sectors[1] - terrain['sector_size'] = self.scene.arm_terrain_sector_size - terrain['height_scale'] = self.scene.arm_terrain_height_scale - terrain['material_ref'] = mat.name - self.output['terrain_datas'] = [terrain] + terrain_export_data = { + 'name': 'Terrain', + 'sectors_x': self.scene.arm_terrain_sectors[0], + 'sectors_y': self.scene.arm_terrain_sectors[1], + 'sector_size': self.scene.arm_terrain_sector_size, + 'height_scale': self.scene.arm_terrain_height_scale, + 'material_ref': mat.name + } + self.output['terrain_datas'] = [terrain_export_data] self.output['terrain_ref'] = 'Terrain' self.output['objects'] = [] From d815a5fc98313c8fec3e18f09a602626f54ff99a Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Tue, 14 Apr 2020 20:07:44 +0200 Subject: [PATCH 086/230] OnCanvasElement node now takes into account anchoring point --- .../armory/logicnode/OnCanvasElementNode.hx | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index 313f5860..e589e891 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -45,11 +45,54 @@ class OnCanvasElementNode extends LogicNode { var x1 = canvas.getElement(element).x; var y1 = canvas.getElement(element).y; - var x2 = x1 + canvas.getElement(element).width; - var y2 = y1 + canvas.getElement(element).height; - + var anchor = canvas.getElement(element).anchor; + var cx = canvas.getCanvas().width; + var cy = canvas.getCanvas().height; var mouseX = mouse.x; var mouseY = mouse.y; + var x2 = x1 + canvas.getElement(element).width; + var y2 = y1 + canvas.getElement(element).height; + switch(anchor) + { + case 1: + { + mouseX -= cx/2 - canvas.getElement(element).width/2; + } + case 2: + { + mouseX -= cx - canvas.getElement(element).width; + } + case 3: + { + mouseY -= cy/2 - canvas.getElement(element).height/2; + } + case 4: + { + mouseX -= cx/2 - canvas.getElement(element).width/2; + mouseY -= cy/2 - canvas.getElement(element).height/2; + } + case 5: + { + mouseX -= cx - canvas.getElement(element).width; + mouseY -= cy/2 - canvas.getElement(element).height/2; + } + case 6: + { + mouseY -= cy - canvas.getElement(element).height; + } + case 7: + { + mouseX -= cx/2 - canvas.getElement(element).width/2; + mouseY -= cy - canvas.getElement(element).height; + } + case 8: + { + mouseX -= cx - canvas.getElement(element).width; + mouseY -= cy - canvas.getElement(element).height; + } + } + + if((mouseX >= x1) && (mouseX <= x2)) { From 91988372fde64399cf9c98d4d78f4a7b2495348a Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 15 Apr 2020 20:15:36 +0200 Subject: [PATCH 087/230] OnCanvasElement node now takes into account anchoring points --- .../armory/logicnode/OnCanvasElementNode.hx | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index e589e891..e0778330 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -1,5 +1,6 @@ package armory.logicnode; +import zui.Canvas.Anchor; import iron.Scene; import armory.trait.internal.CanvasScript; @@ -52,48 +53,31 @@ class OnCanvasElementNode extends LogicNode { var mouseY = mouse.y; var x2 = x1 + canvas.getElement(element).width; var y2 = y1 + canvas.getElement(element).height; + switch(anchor) { - case 1: - { + case Top: mouseX -= cx/2 - canvas.getElement(element).width/2; - } - case 2: - { + case TopRight: mouseX -= cx - canvas.getElement(element).width; - } - case 3: - { + case CenterLeft: mouseY -= cy/2 - canvas.getElement(element).height/2; - } - case 4: - { + case Anchor.Center: mouseX -= cx/2 - canvas.getElement(element).width/2; mouseY -= cy/2 - canvas.getElement(element).height/2; - } - case 5: - { + case CenterRight: mouseX -= cx - canvas.getElement(element).width; mouseY -= cy/2 - canvas.getElement(element).height/2; - } - case 6: - { + case BottomLeft: mouseY -= cy - canvas.getElement(element).height; - } - case 7: - { + case Bottom: mouseX -= cx/2 - canvas.getElement(element).width/2; mouseY -= cy - canvas.getElement(element).height; - } - case 8: - { + case BottomRight: mouseX -= cx - canvas.getElement(element).width; mouseY -= cy - canvas.getElement(element).height; - } } - - if((mouseX >= x1) && (mouseX <= x2)) { if((mouseY >= y1) && (mouseY <= y2)) From 2e0af4bd4f670cc9e65decb97bfaa5ee9e385303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 15 Apr 2020 23:18:40 +0200 Subject: [PATCH 088/230] More type annotations --- blender/arm/exporter.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index a1231898..aaa98106 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -520,18 +520,19 @@ class ArmoryExporter: 'seed': psys.seed, 'particle': psys.settings.name } - o['particle_refs'].append(pref) + object_export_data['particle_refs'].append(pref) @staticmethod - def get_view3d_area(): + def get_view3d_area() -> Optional[bpy.types.Area]: screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': return area return None - def get_viewport_view_matrix(self): - play_area = self.get_view3d_area() + @staticmethod + def get_viewport_view_matrix() -> Optional[Matrix]: + play_area = ArmoryExporter.get_view3d_area() if play_area is None: return None for space in play_area.spaces: @@ -539,8 +540,9 @@ class ArmoryExporter: return space.region_3d.view_matrix return None - def get_viewport_projection_matrix(self): - play_area = self.get_view3d_area() + @staticmethod + def get_viewport_projection_matrix() -> Tuple[Optional[Matrix], bool]: + play_area = ArmoryExporter.get_view3d_area() if play_area is None: return None, False for space in play_area.spaces: From 849b29a6443c9d58aaf12f9afc9fde67ee37a344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 15 Apr 2020 23:34:30 +0200 Subject: [PATCH 089/230] Cleanup export_object() --- blender/arm/exporter.py | 68 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index aaa98106..47ec3d83 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -507,7 +507,7 @@ class ArmoryExporter: self.material_array.append(material) o['material_refs'].append(arm.utils.asset_name(material)) - def export_particle_system_ref(self, psys, index, o): + def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, object_export_data): if psys.settings in self.particle_system_array: # or not modifier.show_render: return @@ -731,8 +731,7 @@ class ArmoryExporter: # self.indentLevel -= 1 # self.IndentWrite(B"}\n") - def export_object(self, bobject: bpy.types.Object, scene: bpy.types.Scene, - parent_export_data: Dict = None) -> None: + def export_object(self, bobject: bpy.types.Object, scene: bpy.types.Scene, parent_export_data: Dict = None) -> None: """This function exports a single object in the scene and includes its name, object reference, material references (for meshes), and transform. @@ -783,16 +782,11 @@ class ArmoryExporter: if len(bobject.arm_propertylist) > 0: object_export_data['properties'] = [] - for p in bobject.arm_propertylist: - po = {} - po['name'] = p.name_prop - po['value'] = getattr(p, p.type_prop + '_prop') - object_export_data['properties'].append(po) - - # TODO: - layer_found = True - if not layer_found: - object_export_data['spawn'] = False + for proplist_item in bobject.arm_propertylist: + property_export_data = { + 'name': proplist_item.name_prop, + 'value': getattr(proplist_item, proplist_item.type_prop + '_prop')} + object_export_data['properties'].append(property_export_data) # Export the object reference and material references objref = bobject.data @@ -802,13 +796,14 @@ class ArmoryExporter: # LOD if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0: object_export_data['lods'] = [] - for l in objref.arm_lodlist: - if not l.enabled_prop: + for lodlist_item in objref.arm_lodlist: + if not lodlist_item.enabled_prop: continue - lod = {} - lod['object_ref'] = l.name - lod['screen_size'] = l.screen_size_prop - object_export_data['lods'].append(lod) + lod_export_data = { + 'object_ref': lodlist_item.name, + 'screen_size': lodlist_item.screen_size_prop + } + object_export_data['lods'].append(lod_export_data) if objref.arm_lod_material: object_export_data['lod_material'] = True @@ -845,7 +840,7 @@ class ArmoryExporter: if num_psys > 0: object_export_data['particle_refs'] = [] for i in range(0, num_psys): - self.export_particle_system_ref(bobject.particle_systems[i], i, object_export_data) + self.export_particle_system_ref(bobject.particle_systems[i], object_export_data) aabb = bobject.data.arm_aabb if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: @@ -936,8 +931,10 @@ class ArmoryExporter: object_export_data['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] if bobject.type == 'ARMATURE' and bobject.data is not None: - bdata = bobject.data # Armature data - action = None # Reference start action + # Armature data + bdata = bobject.data + # Reference start action + action = None adata = bobject.animation_data # Active action @@ -953,7 +950,8 @@ class ArmoryExporter: # Export actions export_actions = [action] - # hasattr - armature modifier may reference non-parent armature object to deform with + # hasattr - armature modifier may reference non-parent + # armature object to deform with if hasattr(adata, 'nla_tracks') and adata.nla_tracks is not None: for track in adata.nla_tracks: if track.strips is None: @@ -980,7 +978,8 @@ class ArmoryExporter: orig_action = bobject.animation_data.action if bdata.arm_autobake and bobject.name not in bpy.context.collection.all_objects: clear_op.add('unlink') - # Clone bjobject and put it in the current scene so the bake operator can run + # Clone bobject and put it in the current scene so + # the bake operator can run if bobject.library is not None: skelobj = bobject.copy() clear_op.add('rem') @@ -992,13 +991,13 @@ class ArmoryExporter: fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=ArmoryExporter.compress_enabled) assets.add(fp) if not bdata.arm_cached or not os.path.exists(fp): - #handle autobake + # Handle autobake if bdata.arm_autobake: sel = bpy.context.selected_objects[:] for _o in sel: _o.select_set(False) skelobj.select_set(True) - bpy.ops.nla.bake(frame_start = action.frame_range[0], frame_end=action.frame_range[1], step=1, only_selected=False, visual_keying=True) + bpy.ops.nla.bake(frame_start=action.frame_range[0], frame_end=action.frame_range[1], step=1, only_selected=False, visual_keying=True) action = skelobj.animation_data.action skelobj.select_set(False) for _o in sel: @@ -1015,20 +1014,21 @@ class ArmoryExporter: boneo = {} self.export_bone(skelobj, bone, scene, boneo, action) bones.append(boneo) - self.write_bone_matrices( bpy.context.scene, action) + self.write_bone_matrices(bpy.context.scene, action) if len(bones) > 0 and 'anim' in bones[0]: self.export_pose_markers(bones[0]['anim'], action) # Save action separately - action_obj = {} - action_obj['name'] = aname - action_obj['objects'] = bones + action_obj = {'name': aname, 'objects': bones} arm.utils.write_arm(fp, action_obj) - # restore settings + # Restore settings skelobj.animation_data.action = orig_action - for a in baked_actions: bpy.data.actions.remove(a, do_unlink=True) - if 'unlink' in clear_op: bpy.context.collection.objects.unlink(skelobj) - if 'rem' in clear_op: bpy.data.objects.remove(skelobj, do_unlink=True) + for a in baked_actions: + bpy.data.actions.remove(a, do_unlink=True) + if 'unlink' in clear_op: + bpy.context.collection.objects.unlink(skelobj) + if 'rem' in clear_op: + bpy.data.objects.remove(skelobj, do_unlink=True) # TODO: cache per action bdata.arm_cached = True From 5cf879287165dceccd503996df2f3c4a1e325fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 15 Apr 2020 23:40:05 +0200 Subject: [PATCH 090/230] Cleanup mesh export --- blender/arm/exporter.py | 107 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 47ec3d83..7f5ae23e 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1146,13 +1146,13 @@ class ArmoryExporter: oskin['constraints'] = [] self.add_constraints(bone, oskin, bone=True) - def write_mesh(self, bobject: bpy.types.Object, fp, o): - wrd = bpy.data.worlds['Arm'] - if wrd.arm_single_data_file: - self.output['mesh_datas'].append(o) - else: # One mesh data per file - mesh_obj = {} - mesh_obj['mesh_datas'] = [o] + def write_mesh(self, bobject: bpy.types.Object, fp, mesh_export_data): + if bpy.data.worlds['Arm'].arm_single_data_file: + self.output['mesh_datas'].append(mesh_export_data) + + # One mesh data per file + else: + mesh_obj = {'mesh_datas': [mesh_export_data]} arm.utils.write_arm(fp, mesh_obj) bobject.data.arm_cached = True @@ -1401,12 +1401,12 @@ class ArmoryExporter: def has_tangents(self, exportMesh): return self.get_export_uvs(exportMesh) and self.get_export_tangents(exportMesh) and len(exportMesh.uv_layers) > 0 - def export_mesh(self, objectRef, scene): + def export_mesh(self, object_ref): """Exports a single mesh object.""" # profile_time = time.time() - table = objectRef[1]["objectTable"] + table = object_ref[1]["objectTable"] bobject = table[0] - oid = arm.utils.safestr(objectRef[1]["structName"]) + oid = arm.utils.safestr(object_ref[1]["structName"]) wrd = bpy.data.worlds['Arm'] if wrd.arm_single_data_file: @@ -1427,44 +1427,43 @@ class ArmoryExporter: if wrd.arm_verbose_output: print('Exporting mesh ' + arm.utils.asset_name(bobject.data)) - o = {} - o['name'] = oid - mesh = objectRef[0] - structFlag = False + mesh_export_data = {'name': oid} + mesh = object_ref[0] + struct_flag = False # Save the morph state if necessary - activeShapeKeyIndex = bobject.active_shape_key_index - showOnlyShapeKey = bobject.show_only_shape_key - currentMorphValue = [] + active_shape_key_index = bobject.active_shape_key_index + show_only_shape_key = bobject.show_only_shape_key + current_morph_value = [] - shapeKeys = ArmoryExporter.get_shape_keys(mesh) - if shapeKeys: + shape_keys = ArmoryExporter.get_shape_keys(mesh) + if shape_keys: bobject.active_shape_key_index = 0 bobject.show_only_shape_key = True - baseIndex = 0 - relative = shapeKeys.use_relative + base_index = 0 + relative = shape_keys.use_relative if relative: - morphCount = 0 - baseName = shapeKeys.reference_key.name - for block in shapeKeys.key_blocks: + morph_count = 0 + baseName = shape_keys.reference_key.name + for block in shape_keys.key_blocks: if block.name == baseName: - baseIndex = morphCount + base_index = morph_count break - morphCount += 1 + morph_count += 1 - morphCount = 0 - for block in shapeKeys.key_blocks: - currentMorphValue.append(block.value) + morph_count = 0 + for block in shape_keys.key_blocks: + current_morph_value.append(block.value) block.value = 0.0 if block.name != "": - # self.IndentWrite(B"Morph (index = ", 0, structFlag) - # self.WriteInt(morphCount) + # self.IndentWrite(B"Morph (index = ", 0, struct_flag) + # self.WriteInt(morph_count) - # if (relative) and (morphCount != baseIndex): + # if (relative) and (morph_count != base_index): # self.Write(B", base = ") - # self.WriteInt(baseIndex) + # self.WriteInt(base_index) # self.Write(B")\n") # self.IndentWrite(B"{\n") @@ -1473,24 +1472,24 @@ class ArmoryExporter: # self.Write(B"\"}}\n") # self.IndentWrite(B"}\n") # TODO - structFlag = True + struct_flag = True - morphCount += 1 + morph_count += 1 - shapeKeys.key_blocks[0].value = 1.0 + shape_keys.key_blocks[0].value = 1.0 mesh.update() armature = bobject.find_armature() apply_modifiers = not armature bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject - exportMesh = bobject_eval.to_mesh() + export_mesh = bobject_eval.to_mesh() - if exportMesh is None: + if export_mesh is None: log.warn(oid + ' was not exported') return - if len(exportMesh.uv_layers) > 2: + if len(export_mesh.uv_layers) > 2: log.warn(oid + ' exceeds maximum of 2 UV Maps supported') # Update aabb @@ -1498,37 +1497,37 @@ class ArmoryExporter: # Process meshes if ArmoryExporter.optimize_enabled: - vert_list = exporter_opt.export_mesh_data(self, exportMesh, bobject, o, has_armature=armature is not None) + vert_list = exporter_opt.export_mesh_data(self, export_mesh, bobject, mesh_export_data, has_armature=armature is not None) if armature: - exporter_opt.export_skin(self, bobject, armature, vert_list, o) + exporter_opt.export_skin(self, bobject, armature, vert_list, mesh_export_data) else: - self.export_mesh_data(exportMesh, bobject, o, has_armature=armature is not None) + self.export_mesh_data(export_mesh, bobject, mesh_export_data, has_armature=armature is not None) if armature: - self.export_skin(bobject, armature, exportMesh, o) + self.export_skin(bobject, armature, export_mesh, mesh_export_data) # Restore the morph state - if shapeKeys: - bobject.active_shape_key_index = activeShapeKeyIndex - bobject.show_only_shape_key = showOnlyShapeKey + if shape_keys: + bobject.active_shape_key_index = active_shape_key_index + bobject.show_only_shape_key = show_only_shape_key - for m in range(len(currentMorphValue)): - shapeKeys.key_blocks[m].value = currentMorphValue[m] + for m in range(len(current_morph_value)): + shape_keys.key_blocks[m].value = current_morph_value[m] mesh.update() # Check if mesh is using instanced rendering - instanced_type, instanced_data = self.object_process_instancing(table, o['scale_pos']) + instanced_type, instanced_data = self.object_process_instancing(table, mesh_export_data['scale_pos']) # Save offset data for instanced rendering if instanced_type > 0: - o['instanced_data'] = instanced_data - o['instanced_type'] = instanced_type + mesh_export_data['instanced_data'] = instanced_data + mesh_export_data['instanced_type'] = instanced_type # Export usage if bobject.data.arm_dynamic_usage: - o['dynamic_usage'] = bobject.data.arm_dynamic_usage + mesh_export_data['dynamic_usage'] = bobject.data.arm_dynamic_usage - self.write_mesh(bobject, fp, o) + self.write_mesh(bobject, fp, mesh_export_data) # print('Mesh exported in ' + str(time.time() - profile_time)) if hasattr(bobject, 'evaluated_get'): @@ -2052,7 +2051,7 @@ class ArmoryExporter: self.output['mesh_datas'] = [] for mesh_ref in self.mesh_array.items(): - self.export_mesh(mesh_ref, scene) + self.export_mesh(mesh_ref) def execute(self): """Exports the scene.""" From 182c07600143275ccedb6e95fff47e6deedfb601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 15 Apr 2020 23:46:34 +0200 Subject: [PATCH 091/230] Cleanup export_skin() --- blender/arm/exporter.py | 73 ++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 7f5ae23e..cc1fb516 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1047,16 +1047,15 @@ class ArmoryExporter: for subbobject in bobject.children: self.export_object(subbobject, scene, object_export_data) - def export_skin(self, bobject: bpy.types.Object, armature, exportMesh, o): - # This function exports all skinning data, which includes the skeleton - # and per-vertex bone influence data + def export_skin(self, bobject: bpy.types.Object, armature, export_mesh: bpy.types.Mesh, mesh_export_data): + """This function exports all skinning data, which includes the + skeleton and per-vertex bone influence data""" oskin = {} - o['skin'] = oskin + mesh_export_data['skin'] = oskin # Write the skin bind pose transform - otrans = {} + otrans = {'values': ArmoryExporter.write_matrix(bobject.matrix_world)} oskin['transform'] = otrans - otrans['values'] = ArmoryExporter.write_matrix(bobject.matrix_world) bone_array = armature.data.bones bone_count = len(bone_array) @@ -1070,9 +1069,9 @@ class ArmoryExporter: oskin['bone_len_array'] = np.empty(bone_count, dtype='= 0: #and bone_weight != 0.0: + if bone_index >= 0: #and bone_weight != 0.0: bone_values.append((bone_weight, bone_index)) total_weight += bone_weight bone_count += 1 @@ -2512,29 +2511,27 @@ class ArmoryExporter: @staticmethod def add_constraints(bobject, o, bone=False): - for con in bobject.constraints: - if con.mute: + for constraint in bobject.constraints: + if constraint.mute: continue - co = {} - co['name'] = con.name - co['type'] = con.type + constr_export_data = {'name': constraint.name, 'type': constraint.type} if bone: - co['bone'] = bobject.name - if hasattr(con, 'target') and con.target is not None: - if con.type == 'COPY_LOCATION': - co['target'] = con.target.name - co['use_x'] = con.use_x - co['use_y'] = con.use_y - co['use_z'] = con.use_z - co['invert_x'] = con.invert_x - co['invert_y'] = con.invert_y - co['invert_z'] = con.invert_z - co['use_offset'] = con.use_offset - co['influence'] = con.influence - elif con.type == 'CHILD_OF': - co['target'] = con.target.name - co['influence'] = con.influence - o['constraints'].append(co) + constr_export_data['bone'] = bobject.name + if hasattr(constraint, 'target') and constraint.target is not None: + if constraint.type == 'COPY_LOCATION': + constr_export_data['target'] = constraint.target.name + constr_export_data['use_x'] = constraint.use_x + constr_export_data['use_y'] = constraint.use_y + constr_export_data['use_z'] = constraint.use_z + constr_export_data['invert_x'] = constraint.invert_x + constr_export_data['invert_y'] = constraint.invert_y + constr_export_data['invert_z'] = constraint.invert_z + constr_export_data['use_offset'] = constraint.use_offset + constr_export_data['influence'] = constraint.influence + elif constraint.type == 'CHILD_OF': + constr_export_data['target'] = constraint.target.name + constr_export_data['influence'] = constraint.influence + o['constraints'].append(constr_export_data) def export_traits(self, bobject: bpy.types.Object, o): if hasattr(bobject, 'arm_traitlist'): From 995d1595e52797745b52013dd4e9d676ea6c86a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 15 Apr 2020 23:53:19 +0200 Subject: [PATCH 092/230] Refactor export_light() --- blender/arm/exporter.py | 79 +++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index cc1fb516..c5134339 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1532,67 +1532,70 @@ class ArmoryExporter: if hasattr(bobject, 'evaluated_get'): bobject_eval.to_mesh_clear() - def export_light(self, objectRef): + def export_light(self, object_ref): """Exports a single light object.""" rpdat = arm.utils.get_rp() - objref = objectRef[0] - objtype = objref.type - o = {} - o['name'] = objectRef[1]["structName"] - o['type'] = objtype.lower() - o['cast_shadow'] = objref.use_shadow - o['near_plane'] = objref.arm_clip_start - o['far_plane'] = objref.arm_clip_end - o['fov'] = objref.arm_fov - o['color'] = [objref.color[0], objref.color[1], objref.color[2]] - o['strength'] = objref.energy - o['shadows_bias'] = objref.arm_shadows_bias * 0.0001 + light_ref = object_ref[0] + objtype = light_ref.type + out_light = { + 'name': object_ref[1]["structName"], + 'type': objtype.lower(), + 'cast_shadow': light_ref.use_shadow, + 'near_plane': light_ref.arm_clip_start, + 'far_plane': light_ref.arm_clip_end, + 'fov': light_ref.arm_fov, + 'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]], + 'strength': light_ref.energy, + 'shadows_bias': light_ref.arm_shadows_bias * 0.0001 + } if rpdat.rp_shadows: if objtype == 'POINT': - o['shadowmap_size'] = int(rpdat.rp_shadowmap_cube) + out_light['shadowmap_size'] = int(rpdat.rp_shadowmap_cube) else: - o['shadowmap_size'] = arm.utils.get_cascade_size(rpdat) + out_light['shadowmap_size'] = arm.utils.get_cascade_size(rpdat) else: - o['shadowmap_size'] = 0 + out_light['shadowmap_size'] = 0 if objtype == 'SUN': - o['strength'] *= 0.325 - o['shadows_bias'] *= 20.0 # Scale bias for ortho light matrix - if o['shadowmap_size'] > 1024: - o['shadows_bias'] *= 1 / (o['shadowmap_size'] / 1024) # Less bias for bigger maps + out_light['strength'] *= 0.325 + # Scale bias for ortho light matrix + out_light['shadows_bias'] *= 20.0 + if out_light['shadowmap_size'] > 1024: + # Less bias for bigger maps + out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024) elif objtype == 'POINT': - o['strength'] *= 2.6 + out_light['strength'] *= 2.6 if bpy.app.version >= (2, 80, 72): - o['strength'] *= 0.01 - o['fov'] = 1.5708 # pi/2 - o['shadowmap_cube'] = True - if objref.shadow_soft_size > 0.1: - o['light_size'] = objref.shadow_soft_size * 10 + out_light['strength'] *= 0.01 + out_light['fov'] = 1.5708 # pi/2 + out_light['shadowmap_cube'] = True + if light_ref.shadow_soft_size > 0.1: + out_light['light_size'] = light_ref.shadow_soft_size * 10 elif objtype == 'SPOT': - o['strength'] *= 2.6 + out_light['strength'] *= 2.6 if bpy.app.version >= (2, 80, 72): - o['strength'] *= 0.01 - o['spot_size'] = math.cos(objref.spot_size / 2) - o['spot_blend'] = objref.spot_blend / 10 # Cycles defaults to 0.15 + out_light['strength'] *= 0.01 + out_light['spot_size'] = math.cos(light_ref.spot_size / 2) + # Cycles defaults to 0.15 + out_light['spot_blend'] = light_ref.spot_blend / 10 elif objtype == 'AREA': - o['strength'] *= 80.0 / (objref.size * objref.size_y) + out_light['strength'] *= 80.0 / (light_ref.size * light_ref.size_y) if bpy.app.version >= (2, 80, 72): - o['strength'] *= 0.01 - o['size'] = objref.size - o['size_y'] = objref.size_y + out_light['strength'] *= 0.01 + out_light['size'] = light_ref.size + out_light['size_y'] = light_ref.size_y - self.output['light_datas'].append(o) + self.output['light_datas'].append(out_light) def export_probe(self, objectRef): - o = {} - o['name'] = objectRef[1]["structName"] + o = {'name': objectRef[1]["structName"]} bo = objectRef[0] if bo.type == 'GRID': o['type'] = 'grid' elif bo.type == 'PLANAR': o['type'] = 'planar' - else: # CUBEMAP + else: o['type'] = 'cubemap' self.output['probe_datas'].append(o) From a51c508e84e476fa346a8e5113844b4b23871fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:01:46 +0200 Subject: [PATCH 093/230] Better output data variable names --- blender/arm/exporter.py | 220 +++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 103 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index c5134339..43323caa 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -72,7 +72,14 @@ current_output = None class ArmoryExporter: - """Export to Armory format""" + """Export to Armory format. + + Some common naming patterns: + - out_[]: Variables starting with "out_" represent data that is + exported to Iron + - bobject: A Blender object (bpy.types.Object). Used because + `object` is a reserved Python keyword + """ compress_enabled = False export_all_flag = True @@ -91,6 +98,7 @@ class ArmoryExporter: self.scene = context.scene if scene is None else scene self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph + # The output dict contains all data that is later exported to Iron format self.output: Dict[str, Any] = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} current_output = self.output @@ -507,7 +515,7 @@ class ArmoryExporter: self.material_array.append(material) o['material_refs'].append(arm.utils.asset_name(material)) - def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, object_export_data): + def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): if psys.settings in self.particle_system_array: # or not modifier.show_render: return @@ -520,7 +528,7 @@ class ArmoryExporter: 'seed': psys.seed, 'particle': psys.settings.name } - object_export_data['particle_refs'].append(pref) + out_object['particle_refs'].append(pref) @staticmethod def get_view3d_area() -> Optional[bpy.types.Area]: @@ -731,7 +739,7 @@ class ArmoryExporter: # self.indentLevel -= 1 # self.IndentWrite(B"}\n") - def export_object(self, bobject: bpy.types.Object, scene: bpy.types.Scene, parent_export_data: Dict = None) -> None: + def export_object(self, bobject: bpy.types.Object, scene: bpy.types.Scene, out_parent: Dict = None) -> None: """This function exports a single object in the scene and includes its name, object reference, material references (for meshes), and transform. @@ -746,47 +754,47 @@ class ArmoryExporter: # Linked object, not present in scene if bobject not in self.object_to_arm_object_dict: - object_export_data = { + out_object = { 'traits': [], 'spawn': False } - self.object_to_arm_object_dict[bobject] = object_export_data + self.object_to_arm_object_dict[bobject] = out_object - object_export_data = self.object_to_arm_object_dict[bobject] - object_export_data['type'] = STRUCT_IDENTIFIER[object_type.value] - object_export_data['name'] = bobject_ref["structName"] + out_object = self.object_to_arm_object_dict[bobject] + out_object['type'] = STRUCT_IDENTIFIER[object_type.value] + out_object['name'] = bobject_ref["structName"] if bobject.parent_type == "BONE": - object_export_data['parent_bone'] = bobject.parent_bone + out_object['parent_bone'] = bobject.parent_bone if bobject.hide_render or not bobject.arm_visible: - object_export_data['visible'] = False + out_object['visible'] = False if not bobject.cycles_visibility.camera: - object_export_data['visible_mesh'] = False + out_object['visible_mesh'] = False if not bobject.cycles_visibility.shadow: - object_export_data['visible_shadow'] = False + out_object['visible_shadow'] = False if not bobject.arm_spawn: - object_export_data['spawn'] = False + out_object['spawn'] = False - object_export_data['mobile'] = bobject.arm_mobile + out_object['mobile'] = bobject.arm_mobile if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: - object_export_data['group_ref'] = bobject.instance_collection.name + out_object['group_ref'] = bobject.instance_collection.name if bobject.arm_tilesheet != '': - object_export_data['tilesheet_ref'] = bobject.arm_tilesheet - object_export_data['tilesheet_action_ref'] = bobject.arm_tilesheet_action + out_object['tilesheet_ref'] = bobject.arm_tilesheet + out_object['tilesheet_action_ref'] = bobject.arm_tilesheet_action if len(bobject.arm_propertylist) > 0: - object_export_data['properties'] = [] + out_object['properties'] = [] for proplist_item in bobject.arm_propertylist: - property_export_data = { + out_property = { 'name': proplist_item.name_prop, 'value': getattr(proplist_item, proplist_item.type_prop + '_prop')} - object_export_data['properties'].append(property_export_data) + out_object['properties'].append(out_property) # Export the object reference and material references objref = bobject.data @@ -795,17 +803,17 @@ class ArmoryExporter: # LOD if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0: - object_export_data['lods'] = [] + out_object['lods'] = [] for lodlist_item in objref.arm_lodlist: if not lodlist_item.enabled_prop: continue - lod_export_data = { + out_lod = { 'object_ref': lodlist_item.name, 'screen_size': lodlist_item.screen_size_prop } - object_export_data['lods'].append(lod_export_data) + out_object['lods'].append(out_lod) if objref.arm_lod_material: - object_export_data['lod_material'] = True + out_object['lod_material'] = True if object_type is NodeType.MESH: if objref not in self.mesh_array: @@ -817,46 +825,46 @@ class ArmoryExporter: wrd = bpy.data.worlds['Arm'] if wrd.arm_single_data_file: - object_export_data['data_ref'] = oid + out_object['data_ref'] = oid else: ext = '' if not ArmoryExporter.compress_enabled else '.lz4' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - object_export_data['data_ref'] = 'mesh_' + oid + ext + '/' + oid + out_object['data_ref'] = 'mesh_' + oid + ext + '/' + oid - object_export_data['material_refs'] = [] + out_object['material_refs'] = [] for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) # Export ref - self.export_material_ref(bobject, mat, i, object_export_data) + self.export_material_ref(bobject, mat, i, out_object) # Decal flag if mat is not None and mat.arm_decal: - object_export_data['type'] = 'decal_object' + out_object['type'] = 'decal_object' # No material, mimic cycles and assign default - if len(object_export_data['material_refs']) == 0: - self.use_default_material(bobject, object_export_data) + if len(out_object['material_refs']) == 0: + self.use_default_material(bobject, out_object) num_psys = len(bobject.particle_systems) if num_psys > 0: - object_export_data['particle_refs'] = [] + out_object['particle_refs'] = [] for i in range(0, num_psys): - self.export_particle_system_ref(bobject.particle_systems[i], object_export_data) + self.export_particle_system_ref(bobject.particle_systems[i], out_object) aabb = bobject.data.arm_aabb if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: self.calc_aabb(bobject) - object_export_data['dimensions'] = [aabb[0], aabb[1], aabb[2]] + out_object['dimensions'] = [aabb[0], aabb[1], aabb[2]] # shapeKeys = ArmoryExporter.get_shape_keys(objref) # if shapeKeys: - # self.ExportMorphWeights(bobject, shapeKeys, scene, object_export_data) + # self.ExportMorphWeights(bobject, shapeKeys, scene, out_object) elif object_type is NodeType.LIGHT: if objref not in self.light_array: self.light_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.light_array[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.light_array[objref]["structName"] + out_object['data_ref'] = self.light_array[objref]["structName"] elif object_type is NodeType.PROBE: if objref not in self.probe_array: @@ -867,15 +875,15 @@ class ArmoryExporter: dist = bobject.data.influence_distance if objref.type == "PLANAR": - object_export_data['dimensions'] = [1.0, 1.0, dist] + out_object['dimensions'] = [1.0, 1.0, dist] # GRID, CUBEMAP else: - object_export_data['dimensions'] = [dist, dist, dist] - object_export_data['data_ref'] = self.probe_array[objref]["structName"] + out_object['dimensions'] = [dist, dist, dist] + out_object['data_ref'] = self.probe_array[objref]["structName"] elif object_type is NodeType.CAMERA: - if 'spawn' in object_export_data and not object_export_data['spawn']: + if 'spawn' in out_object and not out_object['spawn']: self.camera_spawned |= False else: self.camera_spawned = True @@ -884,14 +892,14 @@ class ArmoryExporter: self.camera_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.camera_array[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.camera_array[objref]["structName"] + out_object['data_ref'] = self.camera_array[objref]["structName"] elif object_type is NodeType.SPEAKER: if objref not in self.speaker_array: self.speaker_array[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.speaker_array[objref]["objectTable"].append(bobject) - object_export_data['data_ref'] = self.speaker_array[objref]["structName"] + out_object['data_ref'] = self.speaker_array[objref]["structName"] # Export the transform. If object is animated, then animation tracks are exported here if bobject.type != 'ARMATURE' and bobject.animation_data is not None: @@ -907,28 +915,28 @@ class ArmoryExporter: orig_action = action for a in export_actions: bobject.animation_data.action = a - self.export_object_transform(bobject, object_export_data) + self.export_object_transform(bobject, out_object) if len(export_actions) >= 2 and export_actions[0] is None: # No action assigned - object_export_data['object_actions'].insert(0, 'null') + out_object['object_actions'].insert(0, 'null') bobject.animation_data.action = orig_action else: - self.export_object_transform(bobject, object_export_data) + self.export_object_transform(bobject, out_object) # If the object is parented to a bone and is not relative, then undo the bone's transform if bobject.parent_type == "BONE": armature = bobject.parent.data bone = armature.bones[bobject.parent_bone] # if not bone.use_relative_parent: - object_export_data['parent_bone_connected'] = bone.use_connect + out_object['parent_bone_connected'] = bone.use_connect if bone.use_connect: bone_translation = Vector((0, bone.length, 0)) + bone.head - object_export_data['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] else: bone_translation = bone.tail - bone.head - object_export_data['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] pose_bone = bobject.parent.pose.bones[bobject.parent_bone] bone_translation_pose = pose_bone.tail - pose_bone.head - object_export_data['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] + out_object['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] if bobject.type == 'ARMATURE' and bobject.data is not None: # Armature data @@ -967,10 +975,10 @@ class ArmoryExporter: ext = '.lz4' if ArmoryExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - object_export_data['bone_actions'] = [] + out_object['bone_actions'] = [] for action in export_actions: aname = arm.utils.safestr(arm.utils.asset_name(action)) - object_export_data['bone_actions'].append('action_' + armatureid + '_' + aname + ext) + out_object['bone_actions'].append('action_' + armatureid + '_' + aname + ext) clear_op = set() skelobj = bobject @@ -1033,25 +1041,25 @@ class ArmoryExporter: # TODO: cache per action bdata.arm_cached = True - if parent_export_data is None: - self.output['objects'].append(object_export_data) + if out_parent is None: + self.output['objects'].append(out_object) else: - parent_export_data['children'].append(object_export_data) + out_parent['children'].append(out_object) - self.post_export_object(bobject, object_export_data, object_type) + self.post_export_object(bobject, out_object, object_type) - if not hasattr(object_export_data, 'children') and len(bobject.children) > 0: - object_export_data['children'] = [] + if not hasattr(out_object, 'children') and len(bobject.children) > 0: + out_object['children'] = [] if bobject.arm_instanced == 'Off': for subbobject in bobject.children: - self.export_object(subbobject, scene, object_export_data) + self.export_object(subbobject, scene, out_object) - def export_skin(self, bobject: bpy.types.Object, armature, export_mesh: bpy.types.Mesh, mesh_export_data): + def export_skin(self, bobject: bpy.types.Object, armature, export_mesh: bpy.types.Mesh, out_mesh): """This function exports all skinning data, which includes the skeleton and per-vertex bone influence data""" oskin = {} - mesh_export_data['skin'] = oskin + out_mesh['skin'] = oskin # Write the skin bind pose transform otrans = {'values': ArmoryExporter.write_matrix(bobject.matrix_world)} @@ -1145,13 +1153,13 @@ class ArmoryExporter: oskin['constraints'] = [] self.add_constraints(bone, oskin, bone=True) - def write_mesh(self, bobject: bpy.types.Object, fp, mesh_export_data): + def write_mesh(self, bobject: bpy.types.Object, fp, out_mesh): if bpy.data.worlds['Arm'].arm_single_data_file: - self.output['mesh_datas'].append(mesh_export_data) + self.output['mesh_datas'].append(out_mesh) # One mesh data per file else: - mesh_obj = {'mesh_datas': [mesh_export_data]} + mesh_obj = {'mesh_datas': [out_mesh]} arm.utils.write_arm(fp, mesh_obj) bobject.data.arm_cached = True @@ -1426,7 +1434,7 @@ class ArmoryExporter: if wrd.arm_verbose_output: print('Exporting mesh ' + arm.utils.asset_name(bobject.data)) - mesh_export_data = {'name': oid} + out_mesh = {'name': oid} mesh = object_ref[0] struct_flag = False @@ -1444,9 +1452,9 @@ class ArmoryExporter: relative = shape_keys.use_relative if relative: morph_count = 0 - baseName = shape_keys.reference_key.name + base_name = shape_keys.reference_key.name for block in shape_keys.key_blocks: - if block.name == baseName: + if block.name == base_name: base_index = morph_count break morph_count += 1 @@ -1496,13 +1504,13 @@ class ArmoryExporter: # Process meshes if ArmoryExporter.optimize_enabled: - vert_list = exporter_opt.export_mesh_data(self, export_mesh, bobject, mesh_export_data, has_armature=armature is not None) + vert_list = exporter_opt.export_mesh_data(self, export_mesh, bobject, out_mesh, has_armature=armature is not None) if armature: - exporter_opt.export_skin(self, bobject, armature, vert_list, mesh_export_data) + exporter_opt.export_skin(self, bobject, armature, vert_list, out_mesh) else: - self.export_mesh_data(export_mesh, bobject, mesh_export_data, has_armature=armature is not None) + self.export_mesh_data(export_mesh, bobject, out_mesh, has_armature=armature is not None) if armature: - self.export_skin(bobject, armature, export_mesh, mesh_export_data) + self.export_skin(bobject, armature, export_mesh, out_mesh) # Restore the morph state if shape_keys: @@ -1515,18 +1523,18 @@ class ArmoryExporter: mesh.update() # Check if mesh is using instanced rendering - instanced_type, instanced_data = self.object_process_instancing(table, mesh_export_data['scale_pos']) + instanced_type, instanced_data = self.object_process_instancing(table, out_mesh['scale_pos']) # Save offset data for instanced rendering if instanced_type > 0: - mesh_export_data['instanced_data'] = instanced_data - mesh_export_data['instanced_type'] = instanced_type + out_mesh['instanced_data'] = instanced_data + out_mesh['instanced_type'] = instanced_type # Export usage if bobject.data.arm_dynamic_usage: - mesh_export_data['dynamic_usage'] = bobject.data.arm_dynamic_usage + out_mesh['dynamic_usage'] = bobject.data.arm_dynamic_usage - self.write_mesh(bobject, fp, mesh_export_data) + self.write_mesh(bobject, fp, out_mesh) # print('Mesh exported in ' + str(time.time() - profile_time)) if hasattr(bobject, 'evaluated_get'): @@ -1604,10 +1612,11 @@ class ArmoryExporter: """Exports a single collection.""" scene_objects = self.scene.collection.all_objects - out_collection = {} - out_collection['name'] = collection.name - out_collection['instance_offset'] = list(collection.instance_offset) - out_collection['object_refs'] = [] + out_collection = { + 'name': collection.name, + 'instance_offset': list(collection.instance_offset), + 'object_refs': [] + } for bobject in collection.objects: @@ -2063,11 +2072,11 @@ class ArmoryExporter: current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() - # bobject = blender object + # bobject => blender object for bobject in scene_objects: # Initialize object export data (map objects to game objects) - object_export_data: Dict[str, Any] = {'traits': []} - self.object_to_arm_object_dict[bobject] = object_export_data + out_object: Dict[str, Any] = {'traits': []} + self.object_to_arm_object_dict[bobject] = out_object # Process # Skip objects that have a parent because children are @@ -2106,23 +2115,23 @@ class ArmoryExporter: assets.add_khafile_def('arm_terrain') # Append trait - trait_export_data = { + out_trait = { 'type': 'Script', 'class_name': 'armory.trait.internal.TerrainPhysics' } if 'traits' not in self.output: self.output['traits']: List[Dict[str, str]] = [] - self.output['traits'].append(trait_export_data) + self.output['traits'].append(out_trait) - ArmoryExporter.import_traits.append(trait_export_data['class_name']) + ArmoryExporter.import_traits.append(out_trait['class_name']) ArmoryExporter.export_physics = True # Export material mat = self.scene.arm_terrain_object.children[0].data.materials[0] self.material_array.append(mat) # Terrain data - terrain_export_data = { + out_terrain = { 'name': 'Terrain', 'sectors_x': self.scene.arm_terrain_sectors[0], 'sectors_y': self.scene.arm_terrain_sectors[1], @@ -2130,15 +2139,18 @@ class ArmoryExporter: 'height_scale': self.scene.arm_terrain_height_scale, 'material_ref': mat.name } - self.output['terrain_datas'] = [terrain_export_data] + self.output['terrain_datas'] = [out_terrain] self.output['terrain_ref'] = 'Terrain' + # Export objects self.output['objects'] = [] for bobject in scene_objects: - # Skip objects that have a parent because children will be exported recursively + # Skip objects that have a parent because children are + # exported recursively if not bobject.parent: self.export_object(bobject, self.scene) + # Export collections if bpy.data.collections: self.output['groups'] = [] for collection in bpy.data.collections: @@ -2517,24 +2529,26 @@ class ArmoryExporter: for constraint in bobject.constraints: if constraint.mute: continue - constr_export_data = {'name': constraint.name, 'type': constraint.type} + out_constraint = {'name': constraint.name, 'type': constraint.type} + if bone: - constr_export_data['bone'] = bobject.name + out_constraint['bone'] = bobject.name if hasattr(constraint, 'target') and constraint.target is not None: if constraint.type == 'COPY_LOCATION': - constr_export_data['target'] = constraint.target.name - constr_export_data['use_x'] = constraint.use_x - constr_export_data['use_y'] = constraint.use_y - constr_export_data['use_z'] = constraint.use_z - constr_export_data['invert_x'] = constraint.invert_x - constr_export_data['invert_y'] = constraint.invert_y - constr_export_data['invert_z'] = constraint.invert_z - constr_export_data['use_offset'] = constraint.use_offset - constr_export_data['influence'] = constraint.influence + out_constraint['target'] = constraint.target.name + out_constraint['use_x'] = constraint.use_x + out_constraint['use_y'] = constraint.use_y + out_constraint['use_z'] = constraint.use_z + out_constraint['invert_x'] = constraint.invert_x + out_constraint['invert_y'] = constraint.invert_y + out_constraint['invert_z'] = constraint.invert_z + out_constraint['use_offset'] = constraint.use_offset + out_constraint['influence'] = constraint.influence elif constraint.type == 'CHILD_OF': - constr_export_data['target'] = constraint.target.name - constr_export_data['influence'] = constraint.influence - o['constraints'].append(constr_export_data) + out_constraint['target'] = constraint.target.name + out_constraint['influence'] = constraint.influence + + o['constraints'].append(out_constraint) def export_traits(self, bobject: bpy.types.Object, o): if hasattr(bobject, 'arm_traitlist'): From 3d27723ab85cef638c815c20dabab4fcec3e0b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:10:44 +0200 Subject: [PATCH 094/230] Cleanup create_default_camera() --- blender/arm/exporter.py | 57 +++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 43323caa..7a1b4919 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2286,42 +2286,49 @@ class ArmoryExporter: return {'FINISHED'} def create_default_camera(self, is_viewport_camera=False): - o = {} - o['name'] = 'DefaultCamera' - o['near_plane'] = 0.1 - o['far_plane'] = 100.0 - o['fov'] = 0.85 - o['frustum_culling'] = True - o['clear_color'] = self.get_camera_clear_color() + """Creates the default camera and adds a WalkNavigation trait to it.""" + out_camera = { + 'name': 'DefaultCamera', + 'near_plane': 0.1, + 'far_plane': 100.0, + 'fov': 0.85, + 'frustum_culling': True, + 'clear_color': self.get_camera_clear_color() + } + # Set viewport camera projection if is_viewport_camera: proj, is_persp = self.get_viewport_projection_matrix() if proj is not None: if is_persp: - self.extract_projection(o, proj, with_planes=False) + self.extract_projection(out_camera, proj, with_planes=False) else: - self.extract_ortho(o, proj) - self.output['camera_datas'].append(o) + self.extract_ortho(out_camera, proj) + self.output['camera_datas'].append(out_camera) - o = {} - o['name'] = 'DefaultCamera' - o['type'] = 'camera_object' - o['data_ref'] = 'DefaultCamera' - o['material_refs'] = [] - o['transform'] = {} + out_object = { + 'name': 'DefaultCamera', + 'type': 'camera_object', + 'data_ref': 'DefaultCamera', + 'material_refs': [], + 'transform': {} + } viewport_matrix = self.get_viewport_view_matrix() if viewport_matrix is not None: - o['transform']['values'] = ArmoryExporter.write_matrix(viewport_matrix.inverted_safe()) - o['local_only'] = True + out_object['transform']['values'] = ArmoryExporter.write_matrix(viewport_matrix.inverted_safe()) + out_object['local_only'] = True else: - o['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] - o['traits'] = [] - trait = {} - trait['type'] = 'Script' - trait['class_name'] = 'armory.trait.WalkNavigation' - o['traits'].append(trait) + out_object['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] + + # Add WalkNavigation trait + trait = { + 'type': 'Script', + 'class_name': 'armory.trait.WalkNavigation' + } + out_object['traits'] = [trait] ArmoryExporter.import_traits.append(trait['class_name']) - self.output['objects'].append(o) + + self.output['objects'].append(out_object) self.output['camera_ref'] = 'DefaultCamera' @staticmethod From 2e8250263aaec5c3a59682658999c43a477f385b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:11:09 +0200 Subject: [PATCH 095/230] Smaller improvements --- blender/arm/exporter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 7a1b4919..25506c35 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2283,7 +2283,6 @@ class ArmoryExporter: self.scene.frame_set(current_frame, subframe=current_subframe) print('Scene exported in ' + str(time.time() - profile_time)) - return {'FINISHED'} def create_default_camera(self, is_viewport_camera=False): """Creates the default camera and adds a WalkNavigation trait to it.""" @@ -2385,7 +2384,7 @@ class ArmoryExporter: instanced_data.append(rot.x) instanced_data.append(rot.y) instanced_data.append(rot.z) - if 'Scale'in inst: + if 'Scale' in inst: scale = child.matrix_local.to_scale() instanced_data.append(scale.x) instanced_data.append(scale.y) @@ -2417,7 +2416,7 @@ class ArmoryExporter: if bobject.rigid_body is not None and phys_enabled: ArmoryExporter.export_physics = True rb = bobject.rigid_body - shape = 0 # BOX + shape = 0 # BOX if rb.collision_shape == 'SPHERE': shape = 1 elif rb.collision_shape == 'CONVEX_HULL': From c6bfad710c3db15dc620187b12794d8f37016400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:34:03 +0200 Subject: [PATCH 096/230] utils.py: add docstring + annotations to safestr() --- blender/arm/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index f5bd8e6d..d15ce359 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -526,7 +526,9 @@ def safesrc(s): s = '_' + s return s -def safestr(s): +def safestr(s: str) -> str: + """Outputs a string where special characters have been replaced with + '_', which can be safely used in file and path names.""" for c in r'[]/\;,><&*:%=+@!#^()|?^': s = s.replace(c, '_') return ''.join([i if ord(i) < 128 else '_' for i in s]) @@ -534,7 +536,7 @@ def safestr(s): def asset_name(bdata): s = bdata.name # Append library name if linked - if bdata.library != None: + if bdata.library is not None: s += '_' + bdata.library.name return s From f50feb5042585f2a8115e215b31d617f32bf9d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:39:28 +0200 Subject: [PATCH 097/230] Cleanup export_traits() --- blender/arm/exporter.py | 205 ++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 94 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 25506c35..49bc10f4 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2557,109 +2557,126 @@ class ArmoryExporter: o['constraints'].append(out_constraint) def export_traits(self, bobject: bpy.types.Object, o): - if hasattr(bobject, 'arm_traitlist'): - for t in bobject.arm_traitlist: - # Don't export disabled traits but still export those - # with fake user enabled so that nodes like `TraitNode` - # still work - if not t.enabled_prop and not t.fake_user: + if not hasattr(bobject, 'arm_traitlist'): + return + + for traitlistItem in bobject.arm_traitlist: + # Do not export disabled traits but still export those + # with fake user enabled so that nodes like `TraitNode` + # still work + if not traitlistItem.enabled_prop and not traitlistItem.fake_user: + continue + + out_trait = {} + if traitlistItem.type_prop == 'Logic Nodes' and traitlistItem.node_tree_prop is not None and traitlistItem.node_tree_prop.name != '': + group_name = arm.utils.safesrc(traitlistItem.node_tree_prop.name[0].upper() + traitlistItem.node_tree_prop.name[1:]) + + out_trait['type'] = 'Script' + out_trait['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name + + elif traitlistItem.type_prop == 'WebAssembly': + wpath = os.path.join(arm.utils.get_fp(), 'Bundled', traitlistItem.webassembly_prop + '.wasm') + if not os.path.exists(wpath): + log.warn(f'Wasm "{traitlistItem.webassembly_prop}" not found, skipping') continue - x = {} - if t.type_prop == 'Logic Nodes' and t.node_tree_prop is not None and t.node_tree_prop.name != '': - x['type'] = 'Script' - group_name = arm.utils.safesrc(t.node_tree_prop.name[0].upper() + t.node_tree_prop.name[1:]) - x['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name - elif t.type_prop == 'WebAssembly': - wpath = arm.utils.get_fp() + '/Bundled/' + t.webassembly_prop + '.wasm' - if not os.path.exists(wpath): - log.warn('Wasm "' + t.webassembly_prop + '" not found, skipping') - continue - x['type'] = 'Script' - x['class_name'] = 'armory.trait.internal.WasmScript' - x['parameters'] = ["'" + t.webassembly_prop + "'"] - elif t.type_prop == 'UI Canvas': - cpath = arm.utils.get_fp() + '/Bundled/canvas/' + t.canvas_name_prop + '.json' - if not os.path.exists(cpath): - log.warn('Scene "' + self.scene.name + '" - Object "' + bobject.name + '" - Referenced canvas "' + t.canvas_name_prop + '" not found, skipping') - continue - ArmoryExporter.export_ui = True - x['type'] = 'Script' - x['class_name'] = 'armory.trait.internal.CanvasScript' - x['parameters'] = ["'" + t.canvas_name_prop + "'"] - # assets.add(assetpath) # Bundled is auto-added - # Read file list and add canvas assets - assetpath = arm.utils.get_fp() + '/Bundled/canvas/' + t.canvas_name_prop + '.files' - if os.path.exists(assetpath): - with open(assetpath) as f: - fileList = f.read().splitlines() - for asset in fileList: - # Relative to the root/Bundled/canvas path - asset = asset[6:] # Strip ../../ to start in project root - assets.add(asset) - else: # Haxe/Bundled Script - if t.class_name_prop == '': # Empty class name, skip - continue - x['type'] = 'Script' - if t.type_prop == 'Bundled Script': - trait_prefix = 'armory.trait.' - # TODO: temporary, export single mesh navmesh as obj - if t.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Arm'].arm_navigation != 'Disabled': - ArmoryExporter.export_navigation = True - nav_path = arm.utils.get_fp_build() + '/compiled/Assets/navigation' - if not os.path.exists(nav_path): - os.makedirs(nav_path) - nav_filepath = nav_path + '/nav_' + bobject.data.name + '.arm' - assets.add(nav_filepath) - # TODO: Implement cache - #if not os.path.isfile(nav_filepath): - # override = {'selected_objects': [bobject]} - # bobject.scale.y *= -1 - # mesh = obj.data - # for face in mesh.faces: - # face.v.reverse() - # bpy.ops.export_scene.obj(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) - # bobject.scale.y *= -1 - armature = bobject.find_armature() - apply_modifiers = not armature + out_trait['type'] = 'Script' + out_trait['class_name'] = 'armory.trait.internal.WasmScript' + out_trait['parameters'] = ["'" + traitlistItem.webassembly_prop + "'"] - bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject - exportMesh = bobject_eval.to_mesh() + elif traitlistItem.type_prop == 'UI Canvas': + cpath = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.json') + if not os.path.exists(cpath): + log.warn(f'Scene "{self.scene.name}" - Object "{bobject.name}" - Referenced canvas "{traitlistItem.canvas_name_prop}" not found, skipping') + continue - with open(nav_filepath, 'w') as f: - for v in exportMesh.vertices: - f.write("v %.4f " % (v.co[0] * bobject_eval.scale.x)) - f.write("%.4f " % (v.co[2] * bobject_eval.scale.z)) - f.write("%.4f\n" % (v.co[1] * bobject_eval.scale.y)) # Flipped - for p in exportMesh.polygons: - f.write("f") - for i in reversed(p.vertices): # Flipped normals - f.write(" %d" % (i + 1)) - f.write("\n") - else: # Haxe - trait_prefix = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.' - hxfile = '/Sources/' + (trait_prefix + t.class_name_prop).replace('.', '/') + '.hx' - if not os.path.exists(arm.utils.get_fp() + hxfile): - # TODO: Halt build here once this check is tested - print('Armory Error: Scene "' + self.scene.name + '" - Object "' + bobject.name + '" : Referenced trait file "' + hxfile + '" not found') + ArmoryExporter.export_ui = True + out_trait['type'] = 'Script' + out_trait['class_name'] = 'armory.trait.internal.CanvasScript' + out_trait['parameters'] = ["'" + traitlistItem.canvas_name_prop + "'"] - x['class_name'] = trait_prefix + t.class_name_prop + # Read file list and add canvas assets + assetpath = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.files') + if os.path.exists(assetpath): + with open(assetpath) as f: + file_list = f.read().splitlines() + for asset in file_list: + # Relative to the root/Bundled/canvas path + asset = asset[6:] # Strip ../../ to start in project root + assets.add(asset) - # Export trait properties - if t.arm_traitpropslist: - x['props'] = [] - for trait_prop in t.arm_traitpropslist: - x['props'].append(trait_prop.name) - x['props'].append(trait_prop.type) + # Haxe/Bundled Script + else: + # Empty class name, skip + if traitlistItem.class_name_prop == '': + continue - if trait_prop.type.endswith("Object"): - value = arm.utils.asset_name(trait_prop.value_object) - else: - value = trait_prop.get_value() + out_trait['type'] = 'Script' + if traitlistItem.type_prop == 'Bundled Script': + trait_prefix = 'armory.trait.' - x['props'].append(value) + # TODO: temporary, export single mesh navmesh as obj + if traitlistItem.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Arm'].arm_navigation != 'Disabled': + ArmoryExporter.export_navigation = True - o['traits'].append(x) + nav_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'navigation') + if not os.path.exists(nav_path): + os.makedirs(nav_path) + nav_filepath = os.path.join(nav_path, 'nav_' + bobject.data.name + '.arm') + assets.add(nav_filepath) + + # TODO: Implement cache + # if not os.path.isfile(nav_filepath): + # override = {'selected_objects': [bobject]} + # bobject.scale.y *= -1 + # mesh = obj.data + # for face in mesh.faces: + # face.v.reverse() + # bpy.ops.export_scene.obj(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) + # bobject.scale.y *= -1 + armature = bobject.find_armature() + apply_modifiers = not armature + + bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject + export_mesh = bobject_eval.to_mesh() + + with open(nav_filepath, 'w') as f: + for v in export_mesh.vertices: + f.write("v %.4f " % (v.co[0] * bobject_eval.scale.x)) + f.write("%.4f " % (v.co[2] * bobject_eval.scale.z)) + f.write("%.4f\n" % (v.co[1] * bobject_eval.scale.y)) # Flipped + for p in export_mesh.polygons: + f.write("f") + # Flipped normals + for i in reversed(p.vertices): + f.write(" %d" % (i + 1)) + f.write("\n") + + # Haxe + else: + trait_prefix = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.' + hxfile = os.path.join('Sources', (trait_prefix + traitlistItem.class_name_prop).replace('.', '/') + '.hx') + if not os.path.exists(os.path.join(arm.utils.get_fp(), hxfile)): + # TODO: Halt build here once this check is tested + print(f'Armory Error: Scene "{self.scene.name}" - Object "{bobject.name}": Referenced trait file "{hxfile}" not found') + + out_trait['class_name'] = trait_prefix + traitlistItem.class_name_prop + + # Export trait properties + if traitlistItem.arm_traitpropslist: + out_trait['props'] = [] + for trait_prop in traitlistItem.arm_traitpropslist: + out_trait['props'].append(trait_prop.name) + out_trait['props'].append(trait_prop.type) + + if trait_prop.type.endswith("Object"): + value = arm.utils.asset_name(trait_prop.value_object) + else: + value = trait_prop.get_value() + + out_trait['props'].append(value) + + o['traits'].append(out_trait) @staticmethod def export_canvas_themes(): From 70f1d992de0d793cde17a70907748b98b8e3a2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 00:50:03 +0200 Subject: [PATCH 098/230] Performance improvement by direct list initialization --- blender/arm/exporter.py | 170 +++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 80 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 49bc10f4..0310020d 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2731,98 +2731,108 @@ class ArmoryExporter: rb2 = rbc.object2 if rb1 is None or rb2 is None: return + ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0 - trait = {} - trait['type'] = 'Script' - trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsConstraint' - trait['parameters'] = [\ - "'" + rb1.name + "'", \ - "'" + rb2.name + "'", \ - "'" + rbc.type + "'", \ - str(rbc.disable_collisions).lower(), \ - str(breaking_threshold)] + + trait = { + 'type': 'Script', + 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsConstraint', + 'parameters': [ + "'" + rb1.name + "'", + "'" + rb2.name + "'", + "'" + rbc.type + "'", + str(rbc.disable_collisions).lower(), + str(breaking_threshold) + ] + } + if rbc.type == "GENERIC": - limits = [] - limits.append(1 if rbc.use_limit_lin_x else 0) - limits.append(rbc.limit_lin_x_lower) - limits.append(rbc.limit_lin_x_upper) - limits.append(1 if rbc.use_limit_lin_y else 0) - limits.append(rbc.limit_lin_y_lower) - limits.append(rbc.limit_lin_y_upper) - limits.append(1 if rbc.use_limit_lin_z else 0) - limits.append(rbc.limit_lin_z_lower) - limits.append(rbc.limit_lin_z_upper) - limits.append(1 if rbc.use_limit_ang_x else 0) - limits.append(rbc.limit_ang_x_lower) - limits.append(rbc.limit_ang_x_upper) - limits.append(1 if rbc.use_limit_ang_y else 0) - limits.append(rbc.limit_ang_y_lower) - limits.append(rbc.limit_ang_y_upper) - limits.append(1 if rbc.use_limit_ang_z else 0) - limits.append(rbc.limit_ang_z_lower) - limits.append(rbc.limit_ang_z_upper) + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_lin_y else 0, + rbc.limit_lin_y_lower, + rbc.limit_lin_y_upper, + 1 if rbc.use_limit_lin_z else 0, + rbc.limit_lin_z_lower, + rbc.limit_lin_z_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper, + 1 if rbc.use_limit_ang_y else 0, + rbc.limit_ang_y_lower, + rbc.limit_ang_y_upper, + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper + ] trait['parameters'].append(str(limits)) if rbc.type == "GENERIC_SPRING": - limits = [] - limits.append(1 if rbc.use_limit_lin_x else 0) - limits.append(rbc.limit_lin_x_lower) - limits.append(rbc.limit_lin_x_upper) - limits.append(1 if rbc.use_limit_lin_y else 0) - limits.append(rbc.limit_lin_y_lower) - limits.append(rbc.limit_lin_y_upper) - limits.append(1 if rbc.use_limit_lin_z else 0) - limits.append(rbc.limit_lin_z_lower) - limits.append(rbc.limit_lin_z_upper) - limits.append(1 if rbc.use_limit_ang_x else 0) - limits.append(rbc.limit_ang_x_lower) - limits.append(rbc.limit_ang_x_upper) - limits.append(1 if rbc.use_limit_ang_y else 0) - limits.append(rbc.limit_ang_y_lower) - limits.append(rbc.limit_ang_y_upper) - limits.append(1 if rbc.use_limit_ang_z else 0) - limits.append(rbc.limit_ang_z_lower) - limits.append(rbc.limit_ang_z_upper) - limits.append(1 if rbc.use_spring_x else 0) - limits.append(rbc.spring_stiffness_x) - limits.append(rbc.spring_damping_x) - limits.append(1 if rbc.use_spring_y else 0) - limits.append(rbc.spring_stiffness_y) - limits.append(rbc.spring_damping_y) - limits.append(1 if rbc.use_spring_z else 0) - limits.append(rbc.spring_stiffness_z) - limits.append(rbc.spring_damping_z) - limits.append(1 if rbc.use_spring_ang_x else 0) - limits.append(rbc.spring_stiffness_ang_x) - limits.append(rbc.spring_damping_ang_x) - limits.append(1 if rbc.use_spring_ang_y else 0) - limits.append(rbc.spring_stiffness_ang_y) - limits.append(rbc.spring_damping_ang_y) - limits.append(1 if rbc.use_spring_ang_z else 0) - limits.append(rbc.spring_stiffness_ang_z) - limits.append(rbc.spring_damping_ang_z) + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_lin_y else 0, + rbc.limit_lin_y_lower, + rbc.limit_lin_y_upper, + 1 if rbc.use_limit_lin_z else 0, + rbc.limit_lin_z_lower, + rbc.limit_lin_z_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper, + 1 if rbc.use_limit_ang_y else 0, + rbc.limit_ang_y_lower, + rbc.limit_ang_y_upper, + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper, + 1 if rbc.use_spring_x else 0, + rbc.spring_stiffness_x, + rbc.spring_damping_x, + 1 if rbc.use_spring_y else 0, + rbc.spring_stiffness_y, + rbc.spring_damping_y, + 1 if rbc.use_spring_z else 0, + rbc.spring_stiffness_z, + rbc.spring_damping_z, + 1 if rbc.use_spring_ang_x else 0, + rbc.spring_stiffness_ang_x, + rbc.spring_damping_ang_x, + 1 if rbc.use_spring_ang_y else 0, + rbc.spring_stiffness_ang_y, + rbc.spring_damping_ang_y, + 1 if rbc.use_spring_ang_z else 0, + rbc.spring_stiffness_ang_z, + rbc.spring_damping_ang_z + ] trait['parameters'].append(str(limits)) if rbc.type == "HINGE": - limits = [] - limits.append(1 if rbc.use_limit_ang_z else 0) - limits.append(rbc.limit_ang_z_lower) - limits.append(rbc.limit_ang_z_upper) + limits = [ + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper + ] trait['parameters'].append(str(limits)) if rbc.type == "SLIDER": - limits = [] - limits.append(1 if rbc.use_limit_lin_x else 0) - limits.append(rbc.limit_lin_x_lower) - limits.append(rbc.limit_lin_x_upper) + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper + ] trait['parameters'].append(str(limits)) if rbc.type == "PISTON": - limits = [] - limits.append(1 if rbc.use_limit_lin_x else 0) - limits.append(rbc.limit_lin_x_lower) - limits.append(rbc.limit_lin_x_upper) - limits.append(1 if rbc.use_limit_ang_x else 0) - limits.append(rbc.limit_ang_x_lower) - limits.append(rbc.limit_ang_x_upper) + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper + ] trait['parameters'].append(str(limits)) o['traits'].append(trait) From 3d1513d09dd290621541a982dbcf7749e0823707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 22:28:41 +0200 Subject: [PATCH 099/230] Remove unused method --- blender/arm/exporter.py | 61 ----------------------------------------- 1 file changed, 61 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 0310020d..64db5993 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -224,67 +224,6 @@ class ArmoryExporter: oanim['marker_frames'].append(int(pos_marker.frame)) oanim['marker_names'].append(pos_marker.name) - def export_object_sampled_animation(self, bobject: bpy.types.Object,scene: bpy.types.Scene, o: Dict) -> None: - """Exports animation as full 4x4 matrices for each frame""" - animation_flag = False - - animation_flag = bobject.animation_data is not None and bobject.animation_data.action is not None and bobject.type != 'ARMATURE' - - # Font out - if animation_flag: - if 'object_actions' not in o: - o['object_actions'] = [] - - action = bobject.animation_data.action - aname = arm.utils.safestr(arm.utils.asset_name(action)) - fp = self.get_meshes_file_path('action_' + aname, compressed=ArmoryExporter.compress_enabled) - assets.add(fp) - ext = '.lz4' if ArmoryExporter.compress_enabled else '' - if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: - ext = '.json' - o['object_actions'].append('action_' + aname + ext) - - oaction = {} - oaction['sampled'] = True - oaction['name'] = action.name - oanim = {} - oaction['anim'] = oanim - - tracko = {} - tracko['target'] = "transform" - - tracko['frames'] = [] - - begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) - end_frame += 1 - - for i in range(begin_frame, end_frame): - tracko['frames'].append(int(i - begin_frame)) - - tracko['frames'].append(int(end_frame)) - - tracko['values'] = [] - - for i in range(begin_frame, end_frame): - scene.frame_set(i) - tracko['values'] += ArmoryExporter.write_matrix(bobject.matrix_local) # Continuos array of matrix transforms - - oanim['tracks'] = [tracko] - self.export_pose_markers(oanim, action) - - if True: # not action.arm_cached or not os.path.exists(fp): - wrd = bpy.data.worlds['Arm'] - if wrd.arm_verbose_output: - print('Exporting object action ' + aname) - actionf = {} - actionf['objects'] = [] - actionf['objects'].append(oaction) - oaction['type'] = 'object' - oaction['name'] = aname - oaction['data_ref'] = '' - oaction['transform'] = None - arm.utils.write_arm(fp, actionf) - @staticmethod def calculate_animation_length(action): """Calculates the length of the given action.""" From fac393bed83ef0adbfd6ac704eebca0c046d7fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 22:35:29 +0200 Subject: [PATCH 100/230] Refactor/Cleanup animation export --- blender/arm/exporter.py | 147 ++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 81 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 64db5993..6fe81fe8 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -68,6 +68,18 @@ STRUCT_IDENTIFIER = ("object", "bone_object", "mesh_object", "light_object", "camera_object", "speaker_object", "decal_object", "probe_object") +# Internal target names for single FCurve data paths +FCURVE_TARGET_NAMES = { + "location": ("xloc", "yloc", "zloc"), + "rotation_euler": ("xrot", "yrot", "zrot"), + "rotation_quaternion": ("qwrot", "qxrot", "qyrot", "qzrot"), + "scale": ("xscl", "yscl", "zscl"), + "delta_location": ("dxloc", "dyloc", "dzloc"), + "delta_rotation_euler": ("dxrot", "dyrot", "dzrot"), + "delta_rotation_quaternion": ("dqwrot", "dqxrot", "dqyrot", "dqzrot"), + "delta_scale": ("dxscl", "dyscl", "dzscl"), +} + current_output = None @@ -186,30 +198,28 @@ class ArmoryExporter: return None @staticmethod - def collect_bone_animation(armature, name): - path = "pose.bones[\"" + name + "\"]." - curve_array = [] + def collect_bone_animation(armature: bpy.types.Object, name: str) -> List[bpy.types.FCurve]: + path = f"pose.bones[\"{name}\"]." if armature.animation_data: action = armature.animation_data.action if action: - for fcurve in action.fcurves: - if fcurve.data_path.startswith(path): - curve_array.append(fcurve) - return curve_array + return [fcurve for fcurve in action.fcurves if fcurve.data_path.startswith(path)] - def export_bone(self, armature, bone, scene, o, action): + return [] + + def export_bone(self, armature, bone: bpy.types.Bone, o, action: bpy.types.Action): bobject_ref = self.bobject_bone_array.get(bone) if bobject_ref: o['type'] = STRUCT_IDENTIFIER[bobject_ref["objectType"].value] o['name'] = bobject_ref["structName"] - self.export_bone_transform(armature, bone, scene, o, action) + self.export_bone_transform(armature, bone, o, action) o['children'] = [] for sub_bobject in bone.children: so = {} - self.export_bone(armature, sub_bobject, scene, so, action) + self.export_bone(armature, sub_bobject, so, action) o['children'].append(so) @staticmethod @@ -225,8 +235,13 @@ class ArmoryExporter: oanim['marker_names'].append(pos_marker.name) @staticmethod - def calculate_animation_length(action): - """Calculates the length of the given action.""" + def calculate_anim_frame_range(action: bpy.types.Action) -> Tuple[int, int]: + """Calculates the required frame range of the given action by + also taking fcurve modifiers into account. + + Modifiers that are not range-restricted are ignored in this + calculation. + """ start = action.frame_range[0] end = action.frame_range[1] @@ -243,42 +258,25 @@ class ArmoryExporter: if modifier.frame_end > end: end = modifier.frame_end - return (int(start), int(end)) + return int(start), int(end) @staticmethod - def export_animation_track(fcurve, frame_range, target): + def export_animation_track(fcurve: bpy.types.FCurve, frame_range: Tuple[int, int], target: str) -> Dict: """This function exports a single animation track.""" - data_ttrack = {} - - data_ttrack['target'] = target - data_ttrack['frames'] = [] - data_ttrack['values'] = [] + out_track = {'target': target, 'frames': [], 'values': []} start = frame_range[0] end = frame_range[1] for frame in range(start, end + 1): - data_ttrack['frames'].append(frame) - data_ttrack['values'].append(fcurve.evaluate(frame)) + out_track['frames'].append(frame) + out_track['values'].append(fcurve.evaluate(frame)) - return data_ttrack + return out_track def export_object_transform(self, bobject: bpy.types.Object, o): - # Internal target names for single FCurve data paths - target_names = { - "location": ("xloc", "yloc", "zloc"), - "rotation_euler": ("xrot", "yrot", "zrot"), - "rotation_quaternion": ("qwrot", "qxrot", "qyrot", "qzrot"), - "scale": ("xscl", "yscl", "zscl"), - "delta_location": ("dxloc", "dyloc", "dzloc"), - "delta_rotation_euler": ("dxrot", "dyrot", "dzrot"), - "delta_rotation_quaternion": ("dqwrot", "dqxrot", "dqyrot", "dqzrot"), - "delta_scale": ("dxscl", "dyscl", "dzscl"), - } - # Static transform - o['transform'] = {} - o['transform']['values'] = ArmoryExporter.write_matrix(bobject.matrix_local) + o['transform'] = {'values': ArmoryExporter.write_matrix(bobject.matrix_local)} # Animated transform if bobject.animation_data is not None and bobject.type != "ARMATURE": @@ -287,59 +285,52 @@ class ArmoryExporter: if action is not None: action_name = arm.utils.safestr(arm.utils.asset_name(action)) - if 'object_actions' not in o: - o['object_actions'] = [] - fp = self.get_meshes_file_path('action_' + action_name, compressed=ArmoryExporter.compress_enabled) assets.add(fp) ext = '.lz4' if ArmoryExporter.compress_enabled else '' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - o['object_actions'].append('action_' + action_name + ext) - oaction = {} - oaction['name'] = action.name + o.get('object_actions', []).append('action_' + action_name + ext) - # Export the animation tracks - oanim = {} - oaction['anim'] = oanim + frame_range = self.calculate_anim_frame_range(action) + out_anim = { + 'begin': frame_range[0], + 'end': frame_range[1], + 'tracks': [] + } - frame_range = self.calculate_animation_length(action) - oanim['begin'] = frame_range[0] - oanim['end'] = frame_range[1] - - oanim['tracks'] = [] - self.export_pose_markers(oanim, action) + self.export_pose_markers(out_anim, action) for fcurve in action.fcurves: data_path = fcurve.data_path try: - data_ttrack = self.export_animation_track(fcurve, frame_range, target_names[data_path][fcurve.array_index]) - + out_track = self.export_animation_track(fcurve, frame_range, FCURVE_TARGET_NAMES[data_path][fcurve.array_index]) except KeyError: - if data_path not in target_names: + if data_path not in FCURVE_TARGET_NAMES: log.warn(f"Action {action_name}: The data path '{data_path}' is not supported (yet)!") continue - # Missing target entry for array_index or something else else: raise - oanim['tracks'].append(data_ttrack) + out_anim['tracks'].append(out_track) if True: # not action.arm_cached or not os.path.exists(fp): wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting object action ' + action_name) - actionf = {} - actionf['objects'] = [] - actionf['objects'].append(oaction) - oaction['type'] = 'object' - oaction['name'] = action_name - oaction['data_ref'] = '' - oaction['transform'] = None - arm.utils.write_arm(fp, actionf) + + out_object_action = { + 'name': action_name, + 'anim': out_anim, + 'type': 'object', + 'data_ref': '', + 'transform': None + } + action_file = {'objects': [out_object_action]} + arm.utils.write_arm(fp, action_file) def process_bone(self, bone: bpy.types.Bone) -> None: if ArmoryExporter.export_all_flag or bone.select: @@ -393,8 +384,7 @@ class ArmoryExporter: # force its type to be a bone bone_ref[1]["objectType"] = NodeType.BONE - def export_bone_transform(self, armature, bone, scene, o, action): - + def export_bone_transform(self, armature: bpy.types.Object, bone: bpy.types.Bone, o, action: bpy.types.Action): pose_bone = armature.pose.bones.get(bone.name) # if pose_bone is not None: # transform = pose_bone.matrix.copy() @@ -405,25 +395,20 @@ class ArmoryExporter: if bone.parent is not None: transform = (bone.parent.matrix_local.inverted_safe() @ transform) - o['transform'] = {} - o['transform']['values'] = ArmoryExporter.write_matrix(transform) + o['transform'] = {'values': ArmoryExporter.write_matrix(transform)} - curve_array = self.collect_bone_animation(armature, bone.name) - animation = len(curve_array) != 0 + fcurve_list = self.collect_bone_animation(armature, bone.name) - if animation and pose_bone: + if fcurve_list and pose_bone: begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) - o['anim'] = {} - tracko = {} - o['anim']['tracks'] = [tracko] - tracko['target'] = "transform" - tracko['frames'] = [] - for i in range(begin_frame, end_frame + 1): - tracko['frames'].append(i - begin_frame) + out_track = {'target': "transform", 'frames': [], 'values': []} + o['anim'] = {'tracks': [out_track]} - tracko['values'] = [] - self.bone_tracks.append((tracko['values'], pose_bone)) + for i in range(begin_frame, end_frame + 1): + out_track['frames'].append(i - begin_frame) + + self.bone_tracks.append((out_track['values'], pose_bone)) def use_default_material(self, bobject: bpy.types.Object, o): if arm.utils.export_bone_data(bobject): @@ -959,7 +944,7 @@ class ArmoryExporter: for bone in bdata.bones: if not bone.parent: boneo = {} - self.export_bone(skelobj, bone, scene, boneo, action) + self.export_bone(skelobj, bone, boneo, action) bones.append(boneo) self.write_bone_matrices(bpy.context.scene, action) if len(bones) > 0 and 'anim' in bones[0]: From fcf1928002248b17e99b5261647f05267d6bb19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 22:51:22 +0200 Subject: [PATCH 101/230] Cleanup scene trait export --- blender/arm/exporter.py | 89 ++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 6fe81fe8..818bfdb2 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2137,48 +2137,7 @@ class ArmoryExporter: if (len(self.output['camera_datas']) == 0 or len(bpy.data.cameras) == 0) or not self.camera_spawned: self.create_default_camera() - # Scene traits - if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics: - if not 'traits' in self.output: - self.output['traits'] = [] - x = {} - x['type'] = 'Script' - phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' - x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld' - rbw = self.scene.rigidbody_world - if rbw is not None and rbw.enabled: - x['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] - self.output['traits'].append(x) - if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: - if not 'traits' in self.output: - self.output['traits'] = [] - x = {} - x['type'] = 'Script' - x['class_name'] = 'armory.trait.navigation.Navigation' - self.output['traits'].append(x) - if wrd.arm_debug_console: - if not 'traits' in self.output: - self.output['traits'] = [] - ArmoryExporter.export_ui = True - x = {} - x['type'] = 'Script' - x['class_name'] = 'armory.trait.internal.DebugConsole' - x['parameters'] = [str(arm.utils.get_ui_scale())] - self.output['traits'].append(x) - if wrd.arm_live_patch: - if not 'traits' in self.output: - self.output['traits'] = [] - x = {} - x['type'] = 'Script' - x['class_name'] = 'armory.trait.internal.LivePatch' - self.output['traits'].append(x) - if len(self.scene.arm_traitlist) > 0: - if not 'traits' in self.output: - self.output['traits'] = [] - self.export_traits(self.scene, self.output) - if 'traits' in self.output: - for x in self.output['traits']: - ArmoryExporter.import_traits.append(x['class_name']) + self.export_scene_traits() self.export_canvas_themes() @@ -2602,6 +2561,52 @@ class ArmoryExporter: o['traits'].append(out_trait) + def export_scene_traits(self) -> None: + """Exports the traits of the scene and adds some internal traits + to the scene depending on the exporter settings. + """ + wrd = bpy.data.worlds['Arm'] + + if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics: + phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' + + out_trait = { + 'type': 'Script', + 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld' + } + + rbw = self.scene.rigidbody_world + if rbw is not None and rbw.enabled: + out_trait['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] + + self.output.get('traits', None).append(out_trait) + + if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: + out_trait = {'type': 'Script', 'class_name': 'armory.trait.navigation.Navigation'} + self.output.get('traits', None).append(out_trait) + + if wrd.arm_debug_console: + ArmoryExporter.export_ui = True + out_trait = { + 'type': 'Script', + 'class_name': 'armory.trait.internal.DebugConsole', + 'parameters': [str(arm.utils.get_ui_scale())] + } + self.output.get('traits', None).append(out_trait) + + if wrd.arm_live_patch: + out_trait = {'type': 'Script', 'class_name': 'armory.trait.internal.LivePatch'} + self.output.get('traits', None).append(out_trait) + + if len(self.scene.arm_traitlist) > 0: + if 'traits' not in self.output: + self.output['traits'] = [] + self.export_traits(self.scene, self.output) + + if 'traits' in self.output: + for out_trait in self.output['traits']: + ArmoryExporter.import_traits.append(out_trait['class_name']) + @staticmethod def export_canvas_themes(): path_themes = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas') From 94efb17b159064c00d98302a87683fc9bdd19752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 23:09:48 +0200 Subject: [PATCH 102/230] Cleanup softbody/hook export --- blender/arm/exporter.py | 60 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 818bfdb2..7ce2910f 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2374,13 +2374,12 @@ class ArmoryExporter: # Phys traits if phys_enabled: - for m in bobject.modifiers: - if m.type == 'CLOTH': - self.add_softbody_mod(o, bobject, m, 0) # SoftShape.Cloth - elif m.type == 'SOFT_BODY': - self.add_softbody_mod(o, bobject, m, 1) # SoftShape.Volume - elif m.type == 'HOOK': - self.add_hook_mod(o, bobject, m.object.name, m.vertex_group) + for modifier in bobject.modifiers: + if modifier.type == 'CLOTH' or modifier.type == 'SOFT_BODY': + self.add_softbody_mod(o, bobject, modifier) + elif modifier.type == 'HOOK': + self.add_hook_mod(o, bobject, modifier.object.name, modifier.vertex_group) + # Rigid body constraint rbc = bobject.rigid_body_constraint if rbc is not None and rbc.enabled: @@ -2619,29 +2618,41 @@ class ArmoryExporter: pass assets.add(file_theme) - def add_softbody_mod(self, o, bobject: bpy.types.Object, soft_mod, soft_type): + @staticmethod + def add_softbody_mod(o, bobject: bpy.types.Object, modifier: Union[bpy.types.ClothModifier, bpy.types.SoftBodyModifier]): + """Adds a softbody trait to the given object based on the given + softbody/cloth modifier. + """ ArmoryExporter.export_physics = True - phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' assets.add_khafile_def('arm_physics_soft') - trait = {} - trait['type'] = 'Script' - trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.SoftBody' + + phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' + out_trait = {'type': 'Script', 'class_name': 'armory.trait.physics.' + phys_pkg + '.SoftBody'} + # ClothModifier + if modifier.type == 'CLOTH': + bend = modifier.settings.bending_stiffness + soft_type = 0 + # SoftBodyModifier + elif modifier.type == 'SOFT_BODY': + bend = (modifier.settings.bend + 1.0) * 10 + soft_type = 1 + else: + # Wrong modifier type + return + + out_trait['parameters'] = [str(soft_type), str(bend), str(soft_mod.settings.mass), str(bobject.arm_soft_body_margin)] + o['traits'].append(out_trait) + if soft_type == 0: - bend = soft_mod.settings.bending_stiffness - elif soft_type == 1: - bend = (soft_mod.settings.bend + 1.0) * 10 - trait['parameters'] = [str(soft_type), str(bend), str(soft_mod.settings.mass), str(bobject.arm_soft_body_margin)] - o['traits'].append(trait) - if soft_type == 0: - self.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) + ArmoryExporter.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) @staticmethod def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): ArmoryExporter.export_physics = True + phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' - trait = {} - trait['type'] = 'Script' - trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsHook' + out_trait = {'type': 'Script', 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsHook'} + verts = [] if group_name != '': group = bobject.vertex_groups[group_name].index @@ -2651,8 +2662,9 @@ class ArmoryExporter: verts.append(v.co.x) verts.append(v.co.y) verts.append(v.co.z) - trait['parameters'] = ["'" + target_name + "'", str(verts)] - o['traits'].append(trait) + + out_trait['parameters'] = [f"'{target_name}'", str(verts)] + o['traits'].append(out_trait) @staticmethod def add_rigidbody_constraint(o, rbc): From e288f328e77a329f349efaf66fb4cd635d38a301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 16 Apr 2020 23:14:49 +0200 Subject: [PATCH 103/230] More type annotations --- blender/arm/exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 7ce2910f..1ceab6cd 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2833,13 +2833,13 @@ class ArmoryExporter: o['probe'] = po @staticmethod - def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier): + def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier) -> bool: """Compares whether the given modifiers are equal.""" # https://blender.stackexchange.com/questions/70629 return all([getattr(mod1, prop, True) == getattr(mod2, prop, False) for prop in mod1.bl_rna.properties.keys()]) @staticmethod - def mod_equal_stack(obj1, obj2): + def mod_equal_stack(obj1: bpy.types.Object, obj2: bpy.types.Object) -> bool: """Returns `True` if the given objects have the same modifiers.""" if len(obj1.modifiers) == 0 and len(obj2.modifiers) == 0: return True From f848ec4ffc7b7da59cd56fbeaad84b705aa7ebc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 17 Apr 2020 00:32:39 +0200 Subject: [PATCH 104/230] Fix scene trait export --- blender/arm/exporter.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 1ceab6cd..936a080a 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2567,6 +2567,8 @@ class ArmoryExporter: wrd = bpy.data.worlds['Arm'] if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics: + if 'traits' not in self.output: + self.output['traits'] = [] phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' out_trait = { @@ -2578,24 +2580,30 @@ class ArmoryExporter: if rbw is not None and rbw.enabled: out_trait['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] - self.output.get('traits', None).append(out_trait) + self.output['traits'].append(out_trait) if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: + if 'traits' not in self.output: + self.output['traits'] = [] out_trait = {'type': 'Script', 'class_name': 'armory.trait.navigation.Navigation'} - self.output.get('traits', None).append(out_trait) + self.output['traits'].append(out_trait) if wrd.arm_debug_console: + if 'traits' not in self.output: + self.output['traits'] = [] ArmoryExporter.export_ui = True out_trait = { 'type': 'Script', 'class_name': 'armory.trait.internal.DebugConsole', 'parameters': [str(arm.utils.get_ui_scale())] } - self.output.get('traits', None).append(out_trait) + self.output['traits'].append(out_trait) if wrd.arm_live_patch: + if 'traits' not in self.output: + self.output['traits'] = [] out_trait = {'type': 'Script', 'class_name': 'armory.trait.internal.LivePatch'} - self.output.get('traits', None).append(out_trait) + self.output['traits'].append(out_trait) if len(self.scene.arm_traitlist) > 0: if 'traits' not in self.output: From 930e3714192208c7fd8b5c49916c6b8f9d5ce7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 17 Apr 2020 00:36:23 +0200 Subject: [PATCH 105/230] Fix animation export --- blender/arm/exporter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 936a080a..fefbbfac 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -291,7 +291,9 @@ class ArmoryExporter: if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' - o.get('object_actions', []).append('action_' + action_name + ext) + if 'object_actions' not in o: + o['object_actions'] = [] + o['object_actions'].append('action_' + action_name + ext) frame_range = self.calculate_anim_frame_range(action) out_anim = { From be8a1b49b14bbaa0e98d8b59c83933998a9ab56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 17 Apr 2020 00:58:26 +0200 Subject: [PATCH 106/230] Fix missing import --- blender/arm/assets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/assets.py b/blender/arm/assets.py index 7b0d1bdc..b0c1bc97 100755 --- a/blender/arm/assets.py +++ b/blender/arm/assets.py @@ -2,6 +2,8 @@ import shutil import os import stat import bpy + +import arm.log as log import arm.utils assets = [] From 275784fd760bef506c6c4ec3a848855d72002b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 01:02:45 +0200 Subject: [PATCH 107/230] WalkNavigation: set flying speed via mouse wheel --- Sources/armory/trait/WalkNavigation.hx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/armory/trait/WalkNavigation.hx b/Sources/armory/trait/WalkNavigation.hx index 8dd4a2f3..6bfc50d5 100755 --- a/Sources/armory/trait/WalkNavigation.hx +++ b/Sources/armory/trait/WalkNavigation.hx @@ -9,7 +9,7 @@ import iron.math.Vec4; class WalkNavigation extends Trait { public static var enabled = true; - static inline var speed = 5.0; + var speed = 5.0; var dir = new Vec4(); var xvec = new Vec4(); var yvec = new Vec4(); @@ -111,6 +111,13 @@ class WalkNavigation extends Trait { if (ease < 0.0) ease = 0.0; } + if (mouse.wheelDelta < 0) { + speed *= 1.1; + } else if (mouse.wheelDelta > 0) { + speed *= 0.9; + if (speed < 0.5) speed = 0.5; + } + var d = Time.delta * speed * fast * ease; if (d > 0.0) camera.transform.move(dir, d); From 4e8b99637ab3dcc56eae036028de47cc5d6f95f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 01:20:25 +0200 Subject: [PATCH 108/230] Fix exception when the element cannot be found Related to https://github.com/armory3d/armory/issues/1659 --- Sources/armory/logicnode/CanvasSetVisibleNode.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/CanvasSetVisibleNode.hx b/Sources/armory/logicnode/CanvasSetVisibleNode.hx index 7743bb0c..e9a91c58 100644 --- a/Sources/armory/logicnode/CanvasSetVisibleNode.hx +++ b/Sources/armory/logicnode/CanvasSetVisibleNode.hx @@ -18,8 +18,8 @@ class CanvasSetVisibleNode extends LogicNode { if (!canvas.ready) return; tree.removeUpdate(update); - if (visible == true) canvas.getElement(element).visible = true - else canvas.getElement(element).visible = false; + var element = canvas.getElement(element); + if (element != null) element.visible = this.visible; runOutput(0); } override function run(from: Int) { From 146c8903494d2fe372077ab4c25643ff89a1d923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:26:45 +0200 Subject: [PATCH 109/230] New Shader.add_const() method to add global constants --- blender/arm/material/shader.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 60c0817a..73764bcd 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -157,6 +157,7 @@ class Shader: self.ins = [] self.outs = [] self.uniforms = [] + self.constants = [] self.functions = {} self.main = '' self.main_init = '' @@ -209,9 +210,31 @@ class Shader: ar[0] = 'floats' ar[1] = ar[1].split('[', 1)[0] self.context.add_constant(ar[0], ar[1], link=link) - if included == False and s not in self.uniforms: + if not included and s not in self.uniforms: self.uniforms.append(s) + def add_const(self, type_str: str, name: str, value_str: str, array_size: int = 0): + """ + Add a global constant to the shader. + + Parameters + ---------- + type_str: : str + The name of the type, like 'float' or 'vec3'. If the + constant is an array, there is no need to add `[]` to the + type + name: str + The name of the variable + value_str: str + The value of the constant as a string + array_size: int + If not 0 (default value), create an array with the given size + """ + if array_size == 0: + self.constants.append(f'{type_str} {name} = {value_str}') + elif array_size > 0: + self.constants.append(f'{type_str} {name}[{array_size}] = {type_str}[]({value_str})') + def add_function(self, s): fname = s.split('(', 1)[0] if fname in self.functions: @@ -335,6 +358,8 @@ class Shader: s += 'out {0}{1};\n'.format(a, out_ext) for a in self.uniforms: s += 'uniform ' + a + ';\n' + for c in self.constants: + s += 'const ' + c + ';\n' for f in self.functions: s += self.functions[f] s += 'void main() {\n' From ce245d3ec8fc7ef05c44c55612503c91b6e92cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:38:35 +0200 Subject: [PATCH 110/230] PEP8 + type annotations for write_result() and res_var_name() --- blender/arm/material/cycles.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index d7822bab..713cd361 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -1519,28 +1519,28 @@ def is_parsed(s): global parsed return s in parsed -def res_var_name(node, socket): +def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: return node_name(node.name) + '_' + safesrc(socket.name) + '_res' -def write_result(l): +def write_result(link: bpy.types.NodeLink) -> Optional[str]: global parsed - res_var = res_var_name(l.from_node, l.from_socket) + res_var = res_var_name(link.from_node, link.from_socket) # Unparsed node if not is_parsed(res_var): parsed[res_var] = True - st = l.from_socket.type + st = link.from_socket.type if st == 'RGB' or st == 'RGBA' or st == 'VECTOR': - res = parse_vector(l.from_node, l.from_socket) - if res == None: + res = parse_vector(link.from_node, link.from_socket) + if res is None: return None curshader.write('vec3 {0} = {1};'.format(res_var, res)) elif st == 'VALUE': - res = parse_value(l.from_node, l.from_socket) - if res == None: + res = parse_value(link.from_node, link.from_socket) + if res is None: return None curshader.write('float {0} = {1};'.format(res_var, res)) # Normal map already parsed, return - elif l.from_node.type == 'NORMAL_MAP': + elif link.from_node.type == 'NORMAL_MAP': return None return res_var From 34d23dc42e2dcc05fe4cf1a018a6f76b93e1bf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:39:06 +0200 Subject: [PATCH 111/230] Store Value node outputs as const --- blender/arm/material/cycles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 713cd361..c759b9e9 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -1538,7 +1538,10 @@ def write_result(link: bpy.types.NodeLink) -> Optional[str]: res = parse_value(link.from_node, link.from_socket) if res is None: return None - curshader.write('float {0} = {1};'.format(res_var, res)) + if link.from_node.type == "VALUE": + curshader.add_const('float', res_var, res) + else: + curshader.write('float {0} = {1};'.format(res_var, res)) # Normal map already parsed, return elif link.from_node.type == 'NORMAL_MAP': return None From 48590343755f4f936cbbb428ebaaf7da2dc9c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:46:56 +0200 Subject: [PATCH 112/230] Fix uncommited import --- blender/arm/material/cycles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index c759b9e9..e0ad36b0 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -27,6 +27,8 @@ import arm.log import arm.material.mat_state as mat_state import arm.material.cycles_functions as c_functions +from typing import Optional + emission_found = False particle_info = None # Particle info export From 546eea9f279612decb083f95aba0e0aa786fcd5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:50:07 +0200 Subject: [PATCH 113/230] Automatically convert color/vector inputs to BW when connected to a float input --- blender/arm/material/cycles.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index e0ad36b0..79266813 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -1060,18 +1060,20 @@ def parse_normal_map_color_input(inp, strength_input=None): con.add_elem('tang', 'short4norm') frag.write_normal -= 1 -def parse_value_input(inp): +def parse_value_input(inp) -> str: if inp.is_linked: - l = inp.links[0] + link = inp.links[0] - if l.from_node.type == 'REROUTE': - return parse_value_input(l.from_node.inputs[0]) + if link.from_node.type == 'REROUTE': + return parse_value_input(link.from_node.inputs[0]) - res_var = write_result(l) - st = l.from_socket.type - if st == 'RGB' or st == 'RGBA' or st == 'VECTOR': - return '{0}.x'.format(res_var) - else: # VALUE + res_var = write_result(link) + socket_type = link.from_socket.type + if socket_type == 'RGB' or socket_type == 'RGBA' or socket_type == 'VECTOR': + # RGB to BW + return f'((({res_var}.r * 0.3 + {res_var}.g * 0.59 + {res_var}.b * 0.11) / 3.0) * 2.5)' + # VALUE + else: return res_var else: if mat_batch() and inp.is_uniform: From c4b6d835f76903e410bacfa52e0c6eaa026bcf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:52:38 +0200 Subject: [PATCH 114/230] Fix and refactor colorramp node --- blender/arm/material/cycles.py | 55 +++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 79266813..9510b52a 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -755,34 +755,49 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: # Pass constant return to_vec3([rgb[0], rgb[1], rgb[2]]) - elif node.type == 'VALTORGB': # ColorRamp - fac = parse_value_input(node.inputs[0]) + # ColorRamp + elif node.type == 'VALTORGB': + input_fac: bpy.types.NodeSocket = node.inputs[0] + + fac: str = parse_value_input(input_fac) if input_fac.is_linked else to_vec1(input_fac.default_value) interp = node.color_ramp.interpolation elems = node.color_ramp.elements + if len(elems) == 1: return to_vec3(elems[0].color) - # Write cols array - cols_var = node_name(node.name) + '_cols' - curshader.write('vec3 {0}[{1}];'.format(cols_var, len(elems))) # TODO: Make const - for i in range(0, len(elems)): - curshader.write('{0}[{1}] = vec3({2}, {3}, {4});'.format(cols_var, i, elems[i].color[0], elems[i].color[1], elems[i].color[2])) - # Get index + + # Write color array + # The last entry is included twice so that the interpolation + # between indices works (no out of bounds error) + cols_var = node_name(node.name).upper() + '_COLS' + cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems) + cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})' + curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1) + fac_var = node_name(node.name) + '_fac' - curshader.write('float {0} = {1};'.format(fac_var, fac)) - index = '0' - for i in range(1, len(elems)): - index += ' + ({0} > {1} ? 1 : 0)'.format(fac_var, elems[i].position) + curshader.write(f'float {fac_var} = {fac};') + + # Get index of the nearest left element relative to the factor + index = '0 + ' + index += ' + '.join([f'(({fac_var} > {elems[i].position}) ? 1 : 0)' for i in range(1, len(elems))]) + # Write index index_var = node_name(node.name) + '_i' - curshader.write('int {0} = {1};'.format(index_var, index)) + curshader.write(f'int {index_var} = {index};') + if interp == 'CONSTANT': - return '{0}[{1}]'.format(cols_var, index_var) - else: # Linear - # Write facs array - facs_var = node_name(node.name) + '_facs' - curshader.write('float {0}[{1}];'.format(facs_var, len(elems))) # TODO: Make const - for i in range(0, len(elems)): - curshader.write('{0}[{1}] = {2};'.format(facs_var, i, elems[i].position)) + return f'{cols_var}[{index_var}]' + + # Linear interpolation + else: + # Write factor array + facs_var = node_name(node.name).upper() + '_FACS' + facs_entries = ', '.join(str(elem.position) for elem in elems) + # Add one more entry at the rightmost position so that the + # interpolation between indices works (no out of bounds error) + facs_entries += ', 1.0' + curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1) + # Mix color # float f = (pos - start) * (1.0 / (finish - start)) return 'mix({0}[{1}], {0}[{1} + 1], ({2} - {3}[{1}]) * (1.0 / ({3}[{1} + 1] - {3}[{1}]) ))'.format(cols_var, index_var, fac_var, facs_var) From e3d4854546ecc5a7e526279189c3e48db41c2280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 21:52:56 +0200 Subject: [PATCH 115/230] Some code cleanup --- blender/arm/material/cycles.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 9510b52a..74bc4021 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -16,6 +16,7 @@ # import os import shutil +from typing import Optional import bpy from mathutils import Euler, Vector @@ -26,11 +27,11 @@ import arm.make_state import arm.log import arm.material.mat_state as mat_state import arm.material.cycles_functions as c_functions - -from typing import Optional +from arm.material.shader import Shader emission_found = False particle_info = None # Particle info export +curshader: Shader def parse(nodes, con, vert, frag, geom, tesc, tese, parse_surface=True, parse_opacity=True, parse_displacement=True, basecol_only=False): output_node = node_by_type(nodes, 'OUTPUT_MATERIAL') @@ -66,14 +67,15 @@ def parse_output(node, _con, _vert, _frag, _geom, _tesc, _tese, _parse_surface, parse_opacity = _parse_opacity basecol_only = _basecol_only emission_found = False - particle_info = {} - particle_info['index'] = False - particle_info['age'] = False - particle_info['lifetime'] = False - particle_info['location'] = False - particle_info['size'] = False - particle_info['velocity'] = False - particle_info['angular_velocity'] = False + particle_info = { + 'index': False, + 'age': False, + 'lifetime': False, + 'location': False, + 'size': False, + 'velocity': False, + 'angular_velocity': False + } sample_bump = False sample_bump_res = '' procedurals_written = False @@ -357,7 +359,7 @@ def parse_displacement_input(inp): else: return None -def parse_vector_input(inp): +def parse_vector_input(inp) -> str: if inp.is_linked: l = inp.links[0] if l.from_node.type == 'REROUTE': @@ -1053,7 +1055,7 @@ def parse_normal_map_color_input(inp, strength_input=None): global frag if basecol_only: return - if inp.is_linked == False: + if not inp.is_linked: return if normal_parsed: return @@ -1067,7 +1069,7 @@ def parse_normal_map_color_input(inp, strength_input=None): frag.write('n = TBN * normalize(texn);') else: frag.write('vec3 n = ({0}) * 2.0 - 1.0;'.format(parse_vector_input(inp))) - if strength_input != None: + if strength_input is not None: strength = parse_value_input(strength_input) if strength != '1.0': frag.write('n.xy *= {0};'.format(strength)) From 1e4d94a77e6c4da2ad679d6c38434b4b4cdd9a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 18 Apr 2020 22:18:38 +0200 Subject: [PATCH 116/230] Fix docstring --- blender/arm/material/shader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 73764bcd..84799489 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -219,7 +219,7 @@ class Shader: Parameters ---------- - type_str: : str + type_str: str The name of the type, like 'float' or 'vec3'. If the constant is an array, there is no need to add `[]` to the type From 9ebf4577ca1737bc74509d88de7983fb38a4fe05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 19 Apr 2020 17:25:08 +0200 Subject: [PATCH 117/230] Fix SetRotation Node when vector input is null --- Sources/armory/logicnode/SetRotationNode.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/SetRotationNode.hx b/Sources/armory/logicnode/SetRotationNode.hx index 60ea0764..6665fe61 100644 --- a/Sources/armory/logicnode/SetRotationNode.hx +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -15,11 +15,11 @@ class SetRotationNode extends LogicNode { override function run(from: Int) { var object: Object = inputs[1].get(); - if (object == null) { - return; - } + if (object == null) return; var vec: Vec4 = inputs[2].get(); + if (vec == null) return; var w: Float = inputs[3].get(); + switch (property0) { case "Euler Angles": object.transform.rot.fromEuler(vec.x, vec.y, vec.z); From b58fab598c9d8bb75a71b8120367ca9e2083d4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 23 Apr 2020 11:43:01 +0200 Subject: [PATCH 118/230] Fix value node when used with material params --- blender/arm/material/cycles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 74bc4021..a21b1b07 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -1559,7 +1559,7 @@ def write_result(link: bpy.types.NodeLink) -> Optional[str]: res = parse_value(link.from_node, link.from_socket) if res is None: return None - if link.from_node.type == "VALUE": + if link.from_node.type == "VALUE" and not link.from_node.arm_material_param: curshader.add_const('float', res_var, res) else: curshader.write('float {0} = {1};'.format(res_var, res)) From fbf3ce813b68a40d2777fb262aa670467034eda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 27 Apr 2020 09:23:02 +0200 Subject: [PATCH 119/230] Fix compilation for multiple mapping nodes --- blender/arm/material/cycles.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index a21b1b07..661d6861 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -979,18 +979,19 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: out = calc_location(out) if node.vector_type == 'TEXTURE' else calc_scale(out) if input_rotation.is_linked or input_rotation.default_value != Euler((0, 0, 0)): + var_name = node_name(node.name) + "_rotation" if node.vector_type == 'TEXTURE': - curshader.write(f'mat3 rotationX = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));') - curshader.write(f'mat3 rotationY = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));') - curshader.write(f'mat3 rotationZ = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);') + curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));') + curshader.write(f'mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));') + curshader.write(f'mat3 {var_name}Z = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);') else: # A little bit redundant, but faster than 12 more multiplications to make it work dynamically - curshader.write(f'mat3 rotationX = mat3(1.0, 0.0, 0.0, 0.0, cos(-{rotation}.x), sin(-{rotation}.x), 0.0, -sin(-{rotation}.x), cos(-{rotation}.x));') - curshader.write(f'mat3 rotationY = mat3(cos(-{rotation}.y), 0.0, -sin(-{rotation}.y), 0.0, 1.0, 0.0, sin(-{rotation}.y), 0.0, cos(-{rotation}.y));') - curshader.write(f'mat3 rotationZ = mat3(cos(-{rotation}.z), sin(-{rotation}.z), 0.0, -sin(-{rotation}.z), cos(-{rotation}.z), 0.0, 0.0, 0.0, 1.0);') + curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos(-{rotation}.x), sin(-{rotation}.x), 0.0, -sin(-{rotation}.x), cos(-{rotation}.x));') + curshader.write(f'mat3 {var_name}Y = mat3(cos(-{rotation}.y), 0.0, -sin(-{rotation}.y), 0.0, 1.0, 0.0, sin(-{rotation}.y), 0.0, cos(-{rotation}.y));') + curshader.write(f'mat3 {var_name}Z = mat3(cos(-{rotation}.z), sin(-{rotation}.z), 0.0, -sin(-{rotation}.z), cos(-{rotation}.z), 0.0, 0.0, 0.0, 1.0);') # XYZ-order euler rotation - out = f'{out} * rotationX * rotationY * rotationZ' + out = f'{out} * {var_name}X * {var_name}Y * {var_name}Z' out = calc_scale(out) if node.vector_type == 'TEXTURE' else calc_location(out) From 9cb88c111a97f8105886e2966ec5a0d8e67a8c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valent=C3=ADn=20Barros?= Date: Mon, 27 Apr 2020 12:00:59 +0200 Subject: [PATCH 120/230] Added `CanvasScript.setUiScale` method. It sets the scale factor for the UI elements. --- Sources/armory/trait/internal/CanvasScript.hx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/armory/trait/internal/CanvasScript.hx b/Sources/armory/trait/internal/CanvasScript.hx index 1fc25258..b57724a2 100644 --- a/Sources/armory/trait/internal/CanvasScript.hx +++ b/Sources/armory/trait/internal/CanvasScript.hx @@ -104,6 +104,14 @@ class CanvasScript extends Trait { return canvas; } + /** + * Set UI scale factor. + * @param factor Scale factor. + */ + public function setUiScale(factor:Float) { + cui.setScale(factor); + } + /** * Set visibility of canvas * @param visible Whether canvas should be visible or not From 89c2290c151bed19f6153e18789efa45477c670f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 28 Apr 2020 21:44:48 +0200 Subject: [PATCH 121/230] Fix irradiance export when arm_minimize is false --- blender/arm/write_probes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 0ee6cc83..1a3869ed 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -228,8 +228,8 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True return mip_count -# Parse sh coefs produced by cmft into json array def sh_to_json(sh_file): + """Parse sh coefs produced by cmft into json array""" with open(sh_file + '.c') as f: sh_lines = f.read().splitlines() band0_line = sh_lines[5] @@ -240,10 +240,9 @@ def sh_to_json(sh_file): parse_band_floats(irradiance_floats, band0_line) parse_band_floats(irradiance_floats, band1_line) parse_band_floats(irradiance_floats, band2_line) - - sh_json = {} - sh_json['irradiance'] = irradiance_floats - ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else '.json' + + sh_json = {'irradiance': irradiance_floats} + ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else '' arm.utils.write_arm(sh_file + ext, sh_json) # Clean up .c From 3aaf792871b87bdf72c9fc6700792921500f1c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 28 Apr 2020 21:45:02 +0200 Subject: [PATCH 122/230] Whitespace cleanup --- blender/arm/write_probes.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 1a3869ed..94cb904d 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -18,12 +18,12 @@ def add_rad_assets(output_file_rad, rad_format, num_mips): # Generate probes from environment map def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True): envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' - + if not os.path.exists(envpath): os.makedirs(envpath) base_name = arm.utils.extract_filename(image_filepath).rsplit('.', 1)[0] - + # Assets to be generated output_file_irr = envpath + '/' + base_name + '_irradiance' output_file_rad = envpath + '/' + base_name + '_radiance' @@ -37,7 +37,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True if arm_radiance: add_rad_assets(output_file_rad, rad_format, cached_num_mips) return cached_num_mips - + # Get paths sdk_path = arm.utils.get_sdk_path() kha_path = arm.utils.get_kha_path() @@ -51,10 +51,10 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True else: cmft_path = '"' + sdk_path + '/lib/armory_tools/cmft/cmft-linux64"' kraffiti_path = '"' + kha_path + '/Kinc/Tools/kraffiti/kraffiti-linux64"' - + output_gama_numerator = '2.2' if disable_hdr else '1.0' input_file = arm.utils.asset_path(image_filepath) - + # Scale map rpdat = arm.utils.get_rp() target_w = int(rpdat.arm_radiance_size) @@ -77,7 +77,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True ' format=' + rad_format + \ ' width=' + str(target_w) + \ ' height=' + str(target_h)], shell=True) - + # Irradiance spherical harmonics if arm.utils.get_os() == 'win': subprocess.call([ \ @@ -96,7 +96,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True sh_to_json(output_file_irr) add_irr_assets(output_file_irr) - + # Mip-mapped radiance if arm_radiance == False: return cached_num_mips @@ -111,7 +111,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True mip_count = 8 else: mip_count = 7 - + wrd = bpy.data.worlds['Arm'] use_opencl = 'true' @@ -184,7 +184,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True generated_files = [] for i in range(0, mip_count): generated_files.append(output_file_rad + '_' + str(i)) - + # Convert to jpgs if disable_hdr is True: for f in generated_files: @@ -201,7 +201,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True ' to="' + f + '.jpg"' + \ ' format=jpg'], shell=True) os.remove(f + '.hdr') - + # Scale from (4x2 to 1x1> for i in range (0, 2): last = generated_files[-1] @@ -221,7 +221,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True ' scale=0.5' + \ ' format=' + rad_format], shell=True) generated_files.append(out) - + mip_count += 2 add_rad_assets(output_file_rad, rad_format, mip_count) @@ -244,7 +244,7 @@ def sh_to_json(sh_file): sh_json = {'irradiance': irradiance_floats} ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else '' arm.utils.write_arm(sh_file + ext, sh_json) - + # Clean up .c os.remove(sh_file + '.c') @@ -263,9 +263,9 @@ def write_sky_irradiance(base_name): envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' if not os.path.exists(envpath): os.makedirs(envpath) - + output_file = envpath + '/' + base_name + '_irradiance' - + sh_json = {} sh_json['irradiance'] = irradiance_floats arm.utils.write_arm(output_file + '.arm', sh_json) @@ -277,13 +277,13 @@ def write_color_irradiance(base_name, col): irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13] # Adjust to Cycles for i in range(0, 24): irradiance_floats.append(0.0) - + envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' if not os.path.exists(envpath): os.makedirs(envpath) - + output_file = envpath + '/' + base_name + '_irradiance' - + sh_json = {} sh_json['irradiance'] = irradiance_floats arm.utils.write_arm(output_file + '.arm', sh_json) From 8cb87225460e34cfe0ae817e71595e142fdcb03e Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sat, 2 May 2020 14:35:43 +0200 Subject: [PATCH 123/230] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 0cdb249d..7d2e1e1d 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -12,7 +12,7 @@ import arm.proxy import arm.nodes_logic # Armory version -arm_version = '2020.4' +arm_version = '2020.5' arm_commit = '$Id$' def init_properties(): From 0cb0720b2a0510c6bdac71d49675b5cbe82f0431 Mon Sep 17 00:00:00 2001 From: Kenny Lerma Date: Sat, 2 May 2020 08:23:15 -0500 Subject: [PATCH 124/230] Corrected missing 'tex' and 'tex1' for custom materials/shaders --- blender/arm/material/make.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blender/arm/material/make.py b/blender/arm/material/make.py index 63e5be4b..c1ece5e5 100755 --- a/blender/arm/material/make.py +++ b/blender/arm/material/make.py @@ -40,6 +40,14 @@ def parse(material, mat_data, mat_users, mat_armusers): elem['name'] = 'nor' elem['data'] = 'short2norm' con['vertex_elements'].append(elem) + elem = {} + elem['name'] = 'tex' + elem['data'] = 'short2norm' + con['vertex_elements'].append(elem) + elem = {} + elem['name'] = 'tex1' + elem['data'] = 'short2norm' + con['vertex_elements'].append(elem) sd['contexts'].append(con) shader_data_name = material.arm_custom_material bind_constants = {} From 61ff96786fdd27b7af95f1fdc4d938bf5c75c363 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Mon, 4 May 2020 00:19:11 +0200 Subject: [PATCH 125/230] Metal fixes --- Shaders/compositor_pass/compositor_pass.vert.glsl | 4 ++-- Shaders/include/pass.vert.glsl | 2 +- blender/arm/write_data.py | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Shaders/compositor_pass/compositor_pass.vert.glsl b/Shaders/compositor_pass/compositor_pass.vert.glsl index 8e3089f8..4e0a095d 100644 --- a/Shaders/compositor_pass/compositor_pass.vert.glsl +++ b/Shaders/compositor_pass/compositor_pass.vert.glsl @@ -18,7 +18,7 @@ void main() { // Scale vertex attribute to [0-1] range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif @@ -26,7 +26,7 @@ void main() { // #ifdef _CPos // NDC (at the back of cube) - // vec4 v = vec4(pos.xy, 1.0, 1.0); + // vec4 v = vec4(pos.xy, 1.0, 1.0); // v = vec4(invVP * v); // v.xyz /= v.w; // viewRay = v.xyz - eye; diff --git a/Shaders/include/pass.vert.glsl b/Shaders/include/pass.vert.glsl index eececc04..1d924d72 100755 --- a/Shaders/include/pass.vert.glsl +++ b/Shaders/include/pass.vert.glsl @@ -10,7 +10,7 @@ void main() { // Scale vertex attribute to 0-1 range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index c3b5446b..bbdfb28c 100755 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -452,6 +452,12 @@ def write_compiledglsl(defs, make_variants): if make_variants and d.endswith('var'): continue # Write a shader variant instead f.write("#define " + d + "\n") + + f.write("""#if defined(HLSL) || defined(METAL) +#define _InvY +#endif +""") + f.write("""const float PI = 3.1415926535; const float PI2 = PI * 2.0; const vec2 shadowmapSize = vec2(""" + str(shadowmap_size) + """, """ + str(shadowmap_size) + """); From 66e9572554059a975578319268c4f61c00bcc8f3 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Mon, 4 May 2020 23:08:47 +0200 Subject: [PATCH 126/230] Add clear pass for metal --- Shaders/clear_pass/clear_pass.frag.glsl | 9 +++++++++ Shaders/clear_pass/clear_pass.json | 14 ++++++++++++++ Sources/armory/renderpath/RenderPathForward.hx | 7 +++++++ blender/arm/make_renderpath.py | 8 ++++++-- 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Shaders/clear_pass/clear_pass.frag.glsl create mode 100644 Shaders/clear_pass/clear_pass.json diff --git a/Shaders/clear_pass/clear_pass.frag.glsl b/Shaders/clear_pass/clear_pass.frag.glsl new file mode 100644 index 00000000..e32026b8 --- /dev/null +++ b/Shaders/clear_pass/clear_pass.frag.glsl @@ -0,0 +1,9 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + gl_FragDepth = 1.0; +} diff --git a/Shaders/clear_pass/clear_pass.json b/Shaders/clear_pass/clear_pass.json new file mode 100644 index 00000000..409bef4a --- /dev/null +++ b/Shaders/clear_pass/clear_pass.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "clear_pass", + "depth_write": true, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_pass.frag.glsl" + } + ] +} diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index 7c69f134..e989636f 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -62,6 +62,13 @@ class RenderPathForward { path = _path; + #if kha_metal + { + path.loadShader("shader_datas/clear_pass/clear_pass"); + path.clearShader = "shader_datas/clear_pass/clear_pass"; + } + #end + #if (rp_background == "World") { path.loadShader("shader_datas/world_pass/world_pass"); diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index ca0e829b..9e640131 100755 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -127,6 +127,10 @@ def build(): assets.add_khafile_def('rp_shadowmap_cascade={0}'.format(arm.utils.get_cascade_size(rpdat))) assets.add_khafile_def('rp_shadowmap_cube={0}'.format(rpdat.rp_shadowmap_cube)) + gapi = state.export_gapi + if gapi == 'metal': + assets.add_shader_pass('clear_pass') + assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) if rpdat.rp_background == 'World': assets.add_shader_pass('world_pass') @@ -221,7 +225,7 @@ def build(): if rpdat.rp_antialiasing == 'TAA': assets.add_khafile_def('arm_taa') - assets.add_khafile_def('rp_supersampling={0}'.format(rpdat.rp_supersampling)) + assets.add_khafile_def('rp_supersampling={0}'.format(rpdat.rp_supersampling)) if rpdat.rp_supersampling == '4': assets.add_shader_pass('supersample_resolve') @@ -276,7 +280,7 @@ def build(): else: # mobile, solid assets.add_shader_pass('deferred_light_' + rpdat.arm_material_model.lower()) assets.add_khafile_def('rp_material_' + rpdat.arm_material_model.lower()) - + if len(bpy.data.lightprobes) > 0: wrd.world_defs += '_Probes' assets.add_khafile_def('rp_probes') From ede8a0f2d330c4b6ec6082d6f3829a58673da2a1 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Tue, 5 May 2020 09:54:24 +0200 Subject: [PATCH 127/230] Improve metal gapi detect --- blender/arm/make_renderpath.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index 9e640131..8da7d32c 100755 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -127,8 +127,7 @@ def build(): assets.add_khafile_def('rp_shadowmap_cascade={0}'.format(arm.utils.get_cascade_size(rpdat))) assets.add_khafile_def('rp_shadowmap_cube={0}'.format(rpdat.rp_shadowmap_cube)) - gapi = state.export_gapi - if gapi == 'metal': + if arm.utils.get_gapi() == 'metal': assets.add_shader_pass('clear_pass') assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) From 9a71b5b66499e444951aa091f2100dd590851d58 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Tue, 5 May 2020 21:33:15 +0200 Subject: [PATCH 128/230] World pass cleanup --- Shaders/world_pass/world_pass.frag.glsl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Shaders/world_pass/world_pass.frag.glsl b/Shaders/world_pass/world_pass.frag.glsl index dca8371e..ac290f25 100644 --- a/Shaders/world_pass/world_pass.frag.glsl +++ b/Shaders/world_pass/world_pass.frag.glsl @@ -150,13 +150,9 @@ void main() { #ifdef _EnvSky vec3 n = normalize(normal); - float phi = acos(n.z); - float theta = atan(-n.y, n.x) + PI; - float cos_theta = clamp(n.z, 0.0, 1.0); float cos_gamma = dot(n, hosekSunDirection); float gamma_val = acos(cos_gamma); - fragColor.rgb = Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength; #endif From 967f69b24a149784206089468c6ea537f6fe00f9 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Wed, 6 May 2020 18:11:02 +0200 Subject: [PATCH 129/230] Color attachment format --- Shaders/clear_pass/clear_pass.json | 3 ++- Shaders/world_pass/world_pass.json | 3 ++- blender/arm/lib/make_datas.py | 12 +++++++++--- blender/arm/material/make_mesh.py | 12 ++++++++---- blender/arm/material/shader.py | 4 ++++ 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Shaders/clear_pass/clear_pass.json b/Shaders/clear_pass/clear_pass.json index 409bef4a..1abd20d6 100644 --- a/Shaders/clear_pass/clear_pass.json +++ b/Shaders/clear_pass/clear_pass.json @@ -8,7 +8,8 @@ "links": [], "texture_params": [], "vertex_shader": "../include/pass.vert.glsl", - "fragment_shader": "clear_pass.frag.glsl" + "fragment_shader": "clear_pass.frag.glsl", + "color_attachment": "_HDR" } ] } diff --git a/Shaders/world_pass/world_pass.json b/Shaders/world_pass/world_pass.json index 810f8bb2..2515f6b2 100644 --- a/Shaders/world_pass/world_pass.json +++ b/Shaders/world_pass/world_pass.json @@ -108,7 +108,8 @@ ], "texture_params": [], "vertex_shader": "world_pass.vert.glsl", - "fragment_shader": "world_pass.frag.glsl" + "fragment_shader": "world_pass.frag.glsl", + "color_attachment": "_HDR" } ] } diff --git a/blender/arm/lib/make_datas.py b/blender/arm/lib/make_datas.py index b57dbfe3..44d8fbbb 100644 --- a/blender/arm/lib/make_datas.py +++ b/blender/arm/lib/make_datas.py @@ -34,11 +34,17 @@ def parse_context(c, sres, asset, defs, vert=None, frag=None): if con['tesseval_shader'] not in asset: asset.append(con['tesseval_shader']) + if 'color_attachment' in c: + con['color_attachment'] = c['color_attachment'] + if con['color_attachment'] == '_HDR': + con['color_attachment'] = 'RGBA32' if '_LDR' in defs else 'RGBA64' + # Params params = ['depth_write', 'compare_mode', 'cull_mode', \ 'blend_source', 'blend_destination', 'blend_operation', \ 'alpha_blend_source', 'alpha_blend_destination', 'alpha_blend_operation' \ 'color_writes_red', 'color_writes_green', 'color_writes_blue', 'color_writes_alpha', \ + 'color_attachment_count', \ 'conservative_raster'] for p in params: @@ -65,7 +71,7 @@ def parse_context(c, sres, asset, defs, vert=None, frag=None): with open(c['tesscontrol_shader']) as f: tesc = f.read().splitlines() parse_shader(sres, c, con, defs, tesc, False) - + if 'tesseval_shader' in c: with open(c['tesseval_shader']) as f: tese = f.read().splitlines() @@ -76,12 +82,12 @@ def parse_shader(sres, c, con, defs, lines, parse_attributes): skip_else = False vertex_elements_parsed = False vertex_elements_parsing = False - + stack = [] if parse_attributes == False: vertex_elements_parsed = True - + for line in lines: line = line.lstrip() diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 8b211fbf..cc6801f6 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -16,11 +16,12 @@ write_material_attribs_post = None write_vertex_attribs = None def make(context_id, rpasses): + wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() rid = rpdat.rp_renderer con = { 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' } - + # Blend context mat = mat_state.material blend = mat.arm_blending @@ -42,6 +43,9 @@ def make(context_id, rpasses): con['depth_write'] = False con['compare_mode'] = 'equal' + if '_LDR' not in wrd.world_defs: + con['color_attachment'] = 'RGBA64' + con_mesh = mat_state.data.add_context(con) mat_state.con_mesh = con_mesh @@ -243,7 +247,7 @@ def make_deferred(con_mesh, rpasses): frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') - + if '_Emission' in wrd.world_defs or '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs: frag.write('uint matid = 0;') if '_Emission' in wrd.world_defs: @@ -393,7 +397,7 @@ def make_forward_mobile(con_mesh): if '_Spot' in wrd.world_defs: vert.add_out('vec4 spotPosition') vert.add_uniform('mat4 LWVPSpot0', link='_biasLightWorldViewProjectionMatrixSpot0') - vert.write('spotPosition = LWVPSpot0 * spos;') + vert.write('spotPosition = LWVPSpot0 * spos;') frag.add_uniform('sampler2DShadow shadowMapSpot[1]') frag.write('if (spotPosition.w > 0.0) {') frag.write(' vec3 lPos = spotPosition.xyz / spotPosition.w;') @@ -469,7 +473,7 @@ def make_forward_solid(con_mesh): parse_opacity = (blend and is_transluc) or arm_discard if parse_opacity: frag.write('float opacity;') - + cycles.parse(mat_state.nodes, con_mesh, vert, frag, geom, tesc, tese, parse_opacity=parse_opacity, parse_displacement=False, basecol_only=True) if arm_discard: diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 84799489..5d77dcc0 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -65,6 +65,10 @@ class ShaderContext: self.data['color_writes_blue'] = props['color_writes_blue'] if 'color_writes_alpha' in props: self.data['color_writes_alpha'] = props['color_writes_alpha'] + if 'color_attachment_count' in props: + self.data['color_attachment_count'] = props['color_attachment_count'] + if 'color_attachment' in props: + self.data['color_attachment'] = props['color_attachment'] self.data['texture_units'] = [] self.tunits = self.data['texture_units'] From df6346c1d13f0778d7917ab37b481ea391b6e7f2 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Wed, 6 May 2020 21:30:59 +0200 Subject: [PATCH 130/230] Color attachment format --- Shaders/deferred_light/deferred_light.json | 3 ++- Shaders/deferred_light_mobile/deferred_light_mobile.json | 3 ++- Shaders/deferred_light_solid/deferred_light_solid.json | 3 ++- blender/arm/material/make_mesh.py | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Shaders/deferred_light/deferred_light.json b/Shaders/deferred_light/deferred_light.json index e1152218..197aaf04 100755 --- a/Shaders/deferred_light/deferred_light.json +++ b/Shaders/deferred_light/deferred_light.json @@ -240,7 +240,8 @@ } ], "vertex_shader": "../include/pass_viewray.vert.glsl", - "fragment_shader": "deferred_light.frag.glsl" + "fragment_shader": "deferred_light.frag.glsl", + "color_attachment": "RGBA64" } ] } diff --git a/Shaders/deferred_light_mobile/deferred_light_mobile.json b/Shaders/deferred_light_mobile/deferred_light_mobile.json index 8a90e82e..2641b43d 100644 --- a/Shaders/deferred_light_mobile/deferred_light_mobile.json +++ b/Shaders/deferred_light_mobile/deferred_light_mobile.json @@ -159,7 +159,8 @@ } ], "vertex_shader": "../include/pass_viewray.vert.glsl", - "fragment_shader": "deferred_light.frag.glsl" + "fragment_shader": "deferred_light.frag.glsl", + "color_attachment": "RGBA64" } ] } diff --git a/Shaders/deferred_light_solid/deferred_light_solid.json b/Shaders/deferred_light_solid/deferred_light_solid.json index 822c2de7..e718b313 100644 --- a/Shaders/deferred_light_solid/deferred_light_solid.json +++ b/Shaders/deferred_light_solid/deferred_light_solid.json @@ -7,7 +7,8 @@ "cull_mode": "none", "links": [], "vertex_shader": "../include/pass.vert.glsl", - "fragment_shader": "deferred_light.frag.glsl" + "fragment_shader": "deferred_light.frag.glsl", + "color_attachment": "RGBA64" } ] } diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index cc6801f6..8ac5f1be 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -46,6 +46,9 @@ def make(context_id, rpasses): if '_LDR' not in wrd.world_defs: con['color_attachment'] = 'RGBA64' + if rid == 'Deferred': + con['color_attachment_count'] = 3 if '_gbuffer2' in wrd.world_defs else 2 + con_mesh = mat_state.data.add_context(con) mat_state.con_mesh = con_mesh From b0cd02d68eadabcc194c79845a1914ddbfbce88c Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sun, 10 May 2020 10:46:12 +0200 Subject: [PATCH 131/230] Allow multiple color attachment formats --- Shaders/clear_pass/clear_pass.json | 2 +- Shaders/deferred_light/deferred_light.json | 2 +- .../deferred_light_mobile/deferred_light_mobile.json | 2 +- Shaders/deferred_light_solid/deferred_light_solid.json | 2 +- Shaders/world_pass/world_pass.json | 2 +- blender/arm/lib/make_datas.py | 10 +++++----- blender/arm/material/make_mesh.py | 9 ++++----- blender/arm/material/shader.py | 6 ++---- 8 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Shaders/clear_pass/clear_pass.json b/Shaders/clear_pass/clear_pass.json index 1abd20d6..c2165846 100644 --- a/Shaders/clear_pass/clear_pass.json +++ b/Shaders/clear_pass/clear_pass.json @@ -9,7 +9,7 @@ "texture_params": [], "vertex_shader": "../include/pass.vert.glsl", "fragment_shader": "clear_pass.frag.glsl", - "color_attachment": "_HDR" + "color_attachments": ["_HDR"] } ] } diff --git a/Shaders/deferred_light/deferred_light.json b/Shaders/deferred_light/deferred_light.json index 197aaf04..06d5a80f 100755 --- a/Shaders/deferred_light/deferred_light.json +++ b/Shaders/deferred_light/deferred_light.json @@ -241,7 +241,7 @@ ], "vertex_shader": "../include/pass_viewray.vert.glsl", "fragment_shader": "deferred_light.frag.glsl", - "color_attachment": "RGBA64" + "color_attachments": ["RGBA64"] } ] } diff --git a/Shaders/deferred_light_mobile/deferred_light_mobile.json b/Shaders/deferred_light_mobile/deferred_light_mobile.json index 2641b43d..118bcdf3 100644 --- a/Shaders/deferred_light_mobile/deferred_light_mobile.json +++ b/Shaders/deferred_light_mobile/deferred_light_mobile.json @@ -160,7 +160,7 @@ ], "vertex_shader": "../include/pass_viewray.vert.glsl", "fragment_shader": "deferred_light.frag.glsl", - "color_attachment": "RGBA64" + "color_attachments": ["RGBA64"] } ] } diff --git a/Shaders/deferred_light_solid/deferred_light_solid.json b/Shaders/deferred_light_solid/deferred_light_solid.json index e718b313..04997ba0 100644 --- a/Shaders/deferred_light_solid/deferred_light_solid.json +++ b/Shaders/deferred_light_solid/deferred_light_solid.json @@ -8,7 +8,7 @@ "links": [], "vertex_shader": "../include/pass.vert.glsl", "fragment_shader": "deferred_light.frag.glsl", - "color_attachment": "RGBA64" + "color_attachments": ["RGBA64"] } ] } diff --git a/Shaders/world_pass/world_pass.json b/Shaders/world_pass/world_pass.json index 2515f6b2..4be4597a 100644 --- a/Shaders/world_pass/world_pass.json +++ b/Shaders/world_pass/world_pass.json @@ -109,7 +109,7 @@ "texture_params": [], "vertex_shader": "world_pass.vert.glsl", "fragment_shader": "world_pass.frag.glsl", - "color_attachment": "_HDR" + "color_attachments": ["_HDR"] } ] } diff --git a/blender/arm/lib/make_datas.py b/blender/arm/lib/make_datas.py index 44d8fbbb..7a4618ad 100644 --- a/blender/arm/lib/make_datas.py +++ b/blender/arm/lib/make_datas.py @@ -34,17 +34,17 @@ def parse_context(c, sres, asset, defs, vert=None, frag=None): if con['tesseval_shader'] not in asset: asset.append(con['tesseval_shader']) - if 'color_attachment' in c: - con['color_attachment'] = c['color_attachment'] - if con['color_attachment'] == '_HDR': - con['color_attachment'] = 'RGBA32' if '_LDR' in defs else 'RGBA64' + if 'color_attachments' in c: + con['color_attachments'] = c['color_attachments'] + for i in range(len(con['color_attachments'])): + if con['color_attachments'][i] == '_HDR': + con['color_attachments'][i] = 'RGBA32' if '_LDR' in defs else 'RGBA64' # Params params = ['depth_write', 'compare_mode', 'cull_mode', \ 'blend_source', 'blend_destination', 'blend_operation', \ 'alpha_blend_source', 'alpha_blend_destination', 'alpha_blend_operation' \ 'color_writes_red', 'color_writes_green', 'color_writes_blue', 'color_writes_alpha', \ - 'color_attachment_count', \ 'conservative_raster'] for p in params: diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 8ac5f1be..bd7899c4 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -43,11 +43,10 @@ def make(context_id, rpasses): con['depth_write'] = False con['compare_mode'] = 'equal' - if '_LDR' not in wrd.world_defs: - con['color_attachment'] = 'RGBA64' - - if rid == 'Deferred': - con['color_attachment_count'] = 3 if '_gbuffer2' in wrd.world_defs else 2 + attachment_format = 'RGBA32' if '_LDR' in wrd.world_defs else 'RGBA64' + con['color_attachments'] = [attachment_format, attachment_format] + if '_gbuffer2' in wrd.world_defs: + con['color_attachments'].append(attachment_format) con_mesh = mat_state.data.add_context(con) mat_state.con_mesh = con_mesh diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 5d77dcc0..aa0a2f07 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -65,10 +65,8 @@ class ShaderContext: self.data['color_writes_blue'] = props['color_writes_blue'] if 'color_writes_alpha' in props: self.data['color_writes_alpha'] = props['color_writes_alpha'] - if 'color_attachment_count' in props: - self.data['color_attachment_count'] = props['color_attachment_count'] - if 'color_attachment' in props: - self.data['color_attachment'] = props['color_attachment'] + if 'color_attachments' in props: + self.data['color_attachments'] = props['color_attachments'] self.data['texture_units'] = [] self.tunits = self.data['texture_units'] From a68a050ed60f4a0a699dad2fceeb6a2e5995bdb5 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sun, 10 May 2020 10:52:44 +0200 Subject: [PATCH 132/230] Set shader version --- .github/workflows/krom.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/krom.yml b/.github/workflows/krom.yml index 832c8877..31b9b219 100644 --- a/.github/workflows/krom.yml +++ b/.github/workflows/krom.yml @@ -19,4 +19,4 @@ jobs: - name: Compile run: | cd armory_ci - nodejs_bin/node-linux64 Kha/make.js krom + nodejs_bin/node-linux64 Kha/make.js krom --shaderversion 330 From c970c5db6c2a8726f6ca20c470d495181e9851a0 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Sun, 10 May 2020 19:43:02 +0200 Subject: [PATCH 133/230] Pass irradiance uniform as argument --- .../deferred_light/deferred_light.frag.glsl | 18 +++++++++--------- .../deferred_light.frag.glsl | 8 ++++---- Shaders/std/shirr.glsl | 3 +-- blender/arm/material/make_mesh.py | 8 ++++---- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index 4e80e3c8..1f9a024f 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -36,7 +36,7 @@ uniform vec3 eyeSnap; uniform float envmapStrength; #ifdef _Irr -//!uniform vec4 shirr[7]; +uniform vec4 shirr[7]; #endif #ifdef _Brdf uniform sampler2D senvmapBrdf; @@ -165,7 +165,7 @@ out vec4 fragColor; void main() { vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, metallic/roughness, matid - + vec3 n; n.z = 1.0 - abs(g0.x) - abs(g0.y); n.xy = n.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy); @@ -196,7 +196,7 @@ void main() { // Envmap #ifdef _Irr - vec3 envl = shIrradiance(n); + vec3 envl = shIrradiance(n, shirr); #ifdef _EnvTex envl /= PI; #endif @@ -218,7 +218,7 @@ void main() { #endif envl.rgb *= albedo; - + #ifdef _Rad // Indirect specular envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y; #else @@ -236,7 +236,7 @@ void main() { #else vec3 voxpos = p / voxelgiHalfExtents; #endif - + #ifndef _VoxelAONoTrace #ifdef _VoxelGITemporal envl.rgb *= 1.0 - (traceAO(voxpos, n, voxels) * voxelBlend + @@ -245,7 +245,7 @@ void main() { envl.rgb *= 1.0 - traceAO(voxpos, n, voxels); #endif #endif - + #endif fragColor.rgb = envl; @@ -272,7 +272,7 @@ void main() { // for(uint step = 0; step < 400 && color.a < 0.99f; ++step) { // vec3 point = origin + 0.005 * step * direction; // color += (1.0f - color.a) * textureLod(voxels, point * 0.5 + 0.5, 0); - // } + // } // fragColor.rgb += color.rgb; // Show SSAO @@ -363,12 +363,12 @@ void main() { , gbufferD, invVP, eye #endif ); - + #ifdef _Spot #ifdef _SSS if (g0.a == 2.0) fragColor.rgb += fragColor.rgb * SSSSTransmittance(LWVPSpot0, p, n, normalize(pointPos - p), lightPlane.y, shadowMapSpot[0]); #endif - #endif + #endif #endif diff --git a/Shaders/deferred_light_mobile/deferred_light.frag.glsl b/Shaders/deferred_light_mobile/deferred_light.frag.glsl index 3c82a4ce..034f2bec 100644 --- a/Shaders/deferred_light_mobile/deferred_light.frag.glsl +++ b/Shaders/deferred_light_mobile/deferred_light.frag.glsl @@ -17,7 +17,7 @@ uniform sampler2D gbuffer1; uniform float envmapStrength; #ifdef _Irr -//!uniform vec4 shirr[7]; +uniform vec4 shirr[7]; #endif #ifdef _Brdf uniform sampler2D senvmapBrdf; @@ -96,7 +96,7 @@ out vec4 fragColor; void main() { vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, metallic/roughness, depth - + vec3 n; n.z = 1.0 - abs(g0.x) - abs(g0.y); n.xy = n.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy); @@ -123,7 +123,7 @@ void main() { // Envmap #ifdef _Irr - vec3 envl = shIrradiance(n); + vec3 envl = shIrradiance(n, shirr); #ifdef _EnvTex envl /= PI; #endif @@ -145,7 +145,7 @@ void main() { #endif envl.rgb *= albedo; - + #ifdef _Rad // Indirect specular envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y; #else diff --git a/Shaders/std/shirr.glsl b/Shaders/std/shirr.glsl index 39515beb..e56dc978 100755 --- a/Shaders/std/shirr.glsl +++ b/Shaders/std/shirr.glsl @@ -1,6 +1,5 @@ -uniform vec4 shirr[7]; -vec3 shIrradiance(const vec3 nor) { +vec3 shIrradiance(const vec3 nor, const vec4 shirr[7]) { const float c1 = 0.429043; const float c2 = 0.511664; const float c3 = 0.743125; diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index bd7899c4..517a1a59 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -431,8 +431,8 @@ def make_forward_mobile(con_mesh): if '_Irr' in wrd.world_defs: frag.add_include('std/shirr.glsl') - frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance', included=True) - env_str = 'shIrradiance(n)' + frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance') + env_str = 'shIrradiance(n, shirr)' else: env_str = '0.5' @@ -610,8 +610,8 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): if '_Irr' in wrd.world_defs: frag.add_include('std/shirr.glsl') - frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance', included=True) - frag.write('vec3 indirect = shIrradiance(n);') + frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance') + frag.write('vec3 indirect = shIrradiance(n, shirr);') if '_EnvTex' in wrd.world_defs: frag.write('indirect /= PI;') frag.write('indirect *= albedo;') From 413406f3d3a1700bdc23ec66243ca92841d5527a Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 10 May 2020 21:25:01 +0200 Subject: [PATCH 134/230] Generate Main.projectVersion --- blender/arm/write_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index bbdfb28c..da5e6da5 100755 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -351,6 +351,7 @@ def write_mainhx(scene_name, resx, resy, is_play, is_publish): package ; class Main { public static inline var projectName = '""" + arm.utils.safestr(wrd.arm_project_name) + """'; + public static inline var projectVersion = '""" + arm.utils.safestr(wrd.arm_project_version) + """'; public static inline var projectPackage = '""" + arm.utils.safestr(wrd.arm_project_package) + """';""") if rpdat.rp_voxelao: From e30a8c7f4679a415029282080af9e80d46e63941 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Mon, 11 May 2020 09:03:13 +0200 Subject: [PATCH 135/230] Metal fixes --- .../clear_color_depth_pass.frag.glsl} | 0 .../clear_color_depth_pass.json} | 4 +-- .../clear_color_pass.frag.glsl | 8 +++++ .../clear_color_pass/clear_color_pass.json | 15 ++++++++++ .../clear_depth_pass.frag.glsl | 8 +++++ .../clear_depth_pass/clear_depth_pass.json | 19 ++++++++++++ Shaders/include/pass_viewray.vert.glsl | 2 +- Shaders/include/pass_viewray2.vert.glsl | 4 +-- .../motion_blur_pass.frag.glsl | 8 ++--- .../motion_blur_veloc_pass.frag.glsl | 6 ++-- Shaders/probe_cubemap/probe_cubemap.frag.glsl | 4 +-- Shaders/probe_planar/probe_planar.frag.glsl | 4 +-- .../smaa_blend_weight.frag.glsl | 29 ++++++++++--------- .../smaa_edge_detect.vert.glsl | 4 +-- .../smaa_neighborhood_blend.frag.glsl | 8 ++--- .../smaa_neighborhood_blend.vert.glsl | 4 +-- Shaders/ssgi_pass/ssgi_pass.frag.glsl | 2 +- Shaders/ssr_pass/ssr_pass.frag.glsl | 12 ++++---- Shaders/std/gbuffer.glsl | 10 +++---- Shaders/std/shadows.glsl | 4 +-- Shaders/std/ssrs.glsl | 2 +- Shaders/taa_pass/taa_pass.frag.glsl | 4 +-- .../armory/renderpath/RenderPathDeferred.hx | 9 ++++++ .../armory/renderpath/RenderPathForward.hx | 6 ++-- blender/arm/make_renderpath.py | 4 ++- blender/arm/material/make_decal.py | 6 ++-- blender/arm/material/make_mesh.py | 2 +- 27 files changed, 126 insertions(+), 62 deletions(-) rename Shaders/{clear_pass/clear_pass.frag.glsl => clear_color_depth_pass/clear_color_depth_pass.frag.glsl} (100%) rename Shaders/{clear_pass/clear_pass.json => clear_color_depth_pass/clear_color_depth_pass.json} (70%) create mode 100644 Shaders/clear_color_pass/clear_color_pass.frag.glsl create mode 100644 Shaders/clear_color_pass/clear_color_pass.json create mode 100644 Shaders/clear_depth_pass/clear_depth_pass.frag.glsl create mode 100644 Shaders/clear_depth_pass/clear_depth_pass.json diff --git a/Shaders/clear_pass/clear_pass.frag.glsl b/Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl similarity index 100% rename from Shaders/clear_pass/clear_pass.frag.glsl rename to Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl diff --git a/Shaders/clear_pass/clear_pass.json b/Shaders/clear_color_depth_pass/clear_color_depth_pass.json similarity index 70% rename from Shaders/clear_pass/clear_pass.json rename to Shaders/clear_color_depth_pass/clear_color_depth_pass.json index c2165846..5a7b5b93 100644 --- a/Shaders/clear_pass/clear_pass.json +++ b/Shaders/clear_color_depth_pass/clear_color_depth_pass.json @@ -1,14 +1,14 @@ { "contexts": [ { - "name": "clear_pass", + "name": "clear_color_depth_pass", "depth_write": true, "compare_mode": "always", "cull_mode": "none", "links": [], "texture_params": [], "vertex_shader": "../include/pass.vert.glsl", - "fragment_shader": "clear_pass.frag.glsl", + "fragment_shader": "clear_color_depth_pass.frag.glsl", "color_attachments": ["_HDR"] } ] diff --git a/Shaders/clear_color_pass/clear_color_pass.frag.glsl b/Shaders/clear_color_pass/clear_color_pass.frag.glsl new file mode 100644 index 00000000..c8cf9ad8 --- /dev/null +++ b/Shaders/clear_color_pass/clear_color_pass.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); +} diff --git a/Shaders/clear_color_pass/clear_color_pass.json b/Shaders/clear_color_pass/clear_color_pass.json new file mode 100644 index 00000000..3c2c5582 --- /dev/null +++ b/Shaders/clear_color_pass/clear_color_pass.json @@ -0,0 +1,15 @@ +{ + "contexts": [ + { + "name": "clear_color_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_color_pass.frag.glsl", + "color_attachments": ["_HDR"] + } + ] +} diff --git a/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl b/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl new file mode 100644 index 00000000..78cb125e --- /dev/null +++ b/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + gl_FragDepth = 1.0; +} diff --git a/Shaders/clear_depth_pass/clear_depth_pass.json b/Shaders/clear_depth_pass/clear_depth_pass.json new file mode 100644 index 00000000..ca318696 --- /dev/null +++ b/Shaders/clear_depth_pass/clear_depth_pass.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "clear_depth_pass", + "depth_write": true, + "color_write_red": false, + "color_write_green": false, + "color_write_blue": false, + "color_write_alpha": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_depth_pass.frag.glsl", + "color_attachments": ["_HDR"] + } + ] +} diff --git a/Shaders/include/pass_viewray.vert.glsl b/Shaders/include/pass_viewray.vert.glsl index 5c916a4a..0b77b14d 100644 --- a/Shaders/include/pass_viewray.vert.glsl +++ b/Shaders/include/pass_viewray.vert.glsl @@ -14,7 +14,7 @@ void main() { // Scale vertex attribute to [0-1] range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif diff --git a/Shaders/include/pass_viewray2.vert.glsl b/Shaders/include/pass_viewray2.vert.glsl index 727a42a6..8370ab38 100644 --- a/Shaders/include/pass_viewray2.vert.glsl +++ b/Shaders/include/pass_viewray2.vert.glsl @@ -13,14 +13,14 @@ void main() { // Scale vertex attribute to [0-1] range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif gl_Position = vec4(pos.xy, 0.0, 1.0); // NDC (at the back of cube) - vec4 v = vec4(pos.x, pos.y, 1.0, 1.0); + vec4 v = vec4(pos.x, pos.y, 1.0, 1.0); v = vec4(invP * v); viewRay = vec3(v.xy / v.z, 1.0); } diff --git a/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl b/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl index 5161e0bd..f58a2e80 100644 --- a/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl +++ b/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl @@ -18,7 +18,7 @@ in vec3 viewRay; out vec4 fragColor; vec2 getVelocity(vec2 coord, float depth) { - #ifdef HLSL + #ifdef _InvY coord.y = 1.0 - coord.y; #endif vec4 currentPos = vec4(coord.xy * 2.0 - 1.0, depth, 1.0); @@ -26,7 +26,7 @@ vec2 getVelocity(vec2 coord, float depth) { vec4 previousPos = prevVP * worldPos; previousPos /= previousPos.w; vec2 velocity = (currentPos - previousPos).xy / 40.0; - #ifdef HLSL + #ifdef _InvY velocity.y = -velocity.y; #endif return velocity; @@ -34,7 +34,7 @@ vec2 getVelocity(vec2 coord, float depth) { void main() { fragColor.rgb = textureLod(tex, texCoord, 0.0).rgb; - + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; if (depth == 1.0) { return; @@ -42,7 +42,7 @@ void main() { float blurScale = motionBlurIntensity * frameScale; vec2 velocity = getVelocity(texCoord, depth) * blurScale; - + vec2 offset = texCoord; int processed = 1; for(int i = 0; i < 8; ++i) { diff --git a/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl index a361e3a9..e521f7b1 100755 --- a/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl +++ b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl @@ -14,11 +14,11 @@ out vec4 fragColor; void main() { vec2 velocity = textureLod(sveloc, texCoord, 0.0).rg * motionBlurIntensity * frameScale; - - #ifdef HLSL + + #ifdef _InvY velocity.y = -velocity.y; #endif - + fragColor.rgb = textureLod(tex, texCoord, 0.0).rgb; // float speed = length(velocity / texStep); diff --git a/Shaders/probe_cubemap/probe_cubemap.frag.glsl b/Shaders/probe_cubemap/probe_cubemap.frag.glsl index ee33182c..648bec1e 100644 --- a/Shaders/probe_cubemap/probe_cubemap.frag.glsl +++ b/Shaders/probe_cubemap/probe_cubemap.frag.glsl @@ -17,7 +17,7 @@ out vec4 fragColor; void main() { vec2 texCoord = wvpposition.xy / wvpposition.w; texCoord = texCoord * 0.5 + 0.5; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif @@ -46,7 +46,7 @@ void main() { vec3 v = wp - eye; vec3 r = reflect(v, n); - #ifdef HLSL + #ifdef _InvY r.y = -r.y; #endif float intensity = clamp((1.0 - roughness) * dot(wp - probep, n), 0.0, 1.0); diff --git a/Shaders/probe_planar/probe_planar.frag.glsl b/Shaders/probe_planar/probe_planar.frag.glsl index c2de3f0b..81b1782a 100644 --- a/Shaders/probe_planar/probe_planar.frag.glsl +++ b/Shaders/probe_planar/probe_planar.frag.glsl @@ -17,7 +17,7 @@ out vec4 fragColor; void main() { vec2 texCoord = wvpposition.xy / wvpposition.w; texCoord = texCoord * 0.5 + 0.5; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif @@ -39,7 +39,7 @@ void main() { vec3 wp = getPos2(invVP, depth, texCoord); vec4 pp = probeVP * vec4(wp.xyz, 1.0); vec2 tc = (pp.xy / pp.w) * 0.5 + 0.5; - #ifdef HLSL + #ifdef _InvY tc.y = 1.0 - tc.y; #endif diff --git a/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl b/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl index db2f6593..b1239d53 100644 --- a/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl +++ b/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl @@ -1,5 +1,6 @@ #version 450 +#include "compiled.inc" #define SMAA_MAX_SEARCH_STEPS_DIAG 8 #define SMAA_AREATEX_MAX_DISTANCE 16 #define SMAA_AREATEX_MAX_DISTANCE_DIAG 20 @@ -33,7 +34,7 @@ out vec4 fragColor; vec2 cdw_end; vec4 textureLodA(sampler2D tex, vec2 coord, float lod) { - #ifdef HLSL + #ifdef _InvY coord.y = 1.0 - coord.y; #endif return textureLod(tex, coord, lod); @@ -104,7 +105,7 @@ vec2 SMAASearchDiag2(vec2 texcoord, vec2 dir) { return coord.zw; } -/** +/** * Similar to SMAAArea, this calculates the area corresponding to a certain * diagonal distance and crossing edges 'e'. */ @@ -147,7 +148,7 @@ vec2 SMAACalculateDiagWeights(vec2 texcoord, vec2 e, vec4 subsampleIndices) { // Fetch the crossing edges: vec4 coords = mad(vec4(-d.x + 0.25, d.x, d.y, -d.y - 0.25), screenSizeInv.xyxy, texcoord.xyxy); vec4 c; - + c.xy = SMAASampleLevelZeroOffset(edgesTex, coords.xy, ivec2(-1, 0)).rg; c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, ivec2( 1, 0)).rg; c.yxwz = SMAADecodeDiagBilinearAccess(c.xyzw); @@ -172,7 +173,7 @@ vec2 SMAACalculateDiagWeights(vec2 texcoord, vec2 e, vec4 subsampleIndices) { d.yw = SMAASearchDiag2(texcoord, vec2(1.0, 1.0)/*, cdw_end*/); float dadd = cdw_end.y > 0.9 ? 1.0 : 0.0; d.y += dadd; - } + } else { d.yw = vec2(0.0, 0.0); } @@ -207,7 +208,7 @@ vec2 SMAACalculateDiagWeights(vec2 texcoord, vec2 e, vec4 subsampleIndices) { /** * This allows to determine how much length should we add in the last step - * of the searches. It takes the bilinearly interpolated edge (see + * of the searches. It takes the bilinearly interpolated edge (see * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and * crossing edges are active. */ @@ -244,7 +245,7 @@ float SMAASearchXLeft(vec2 texcoord, float end) { * which edges are active from the four fetched ones. */ vec2 e = vec2(0.0, 1.0); - while (texcoord.x > end && + while (texcoord.x > end && e.g > 0.8281 && // Is there some edge not activated? e.r == 0.0) { // Or is there a crossing edge that breaks the line? e = textureLodA(edgesTex, texcoord, 0.0).rg; @@ -257,20 +258,20 @@ float SMAASearchXLeft(vec2 texcoord, float end) { float SMAASearchXRight(vec2 texcoord, float end) { vec2 e = vec2(0.0, 1.0); - while (texcoord.x < end && + while (texcoord.x < end && e.g > 0.8281 && // Is there some edge not activated? e.r == 0.0) { // Or is there a crossing edge that breaks the line? e = textureLodA(edgesTex, texcoord, 0.0).rg; texcoord = mad(vec2(2.0, 0.0), screenSizeInv.xy, texcoord); } - + float offset = mad(-(255.0 / 127.0), SMAASearchLength(e, 0.5), 3.25); return mad(-screenSizeInv.x, offset, texcoord.x); } float SMAASearchYUp(vec2 texcoord, float end) { vec2 e = vec2(1.0, 0.0); - while (texcoord.y > end && + while (texcoord.y > end && e.r > 0.8281 && // Is there some edge not activated? e.g == 0.0) { // Or is there a crossing edge that breaks the line? e = textureLodA(edgesTex, texcoord, 0.0).rg; @@ -282,7 +283,7 @@ float SMAASearchYUp(vec2 texcoord, float end) { float SMAASearchYDown(vec2 texcoord, float end) { vec2 e = vec2(1.0, 0.0); - while (texcoord.y < end && + while (texcoord.y < end && e.r > 0.8281 && // Is there some edge not activated? e.g == 0.0) { // Or is there a crossing edge that breaks the line? e = textureLodA(edgesTex, texcoord, 0.0).rg; @@ -292,14 +293,14 @@ float SMAASearchYDown(vec2 texcoord, float end) { return mad(-screenSizeInv.y, offset, texcoord.y); } -/** +/** * Ok, we have the distance and both crossing edges. So, what are the areas * at each side of current edge? */ vec2 SMAAArea(vec2 dist, float e1, float e2, float offset) { // Rounding prevents precision errors of bilinear filtering: vec2 texcoord = mad(vec2(SMAA_AREATEX_MAX_DISTANCE, SMAA_AREATEX_MAX_DISTANCE), round(4.0 * vec2(e1, e2)), dist); - + // We do a scale and bias for mapping to texel space: texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); @@ -363,7 +364,7 @@ vec4 SMAABlendingWeightCalculationPS(vec2 texcoord, vec2 pixcoord, // one of the boundaries is enough. weights.rg = SMAACalculateDiagWeights(texcoord, e, subsampleIndices); - // We give priority to diagonals, so if we find a diagonal we skip + // We give priority to diagonals, so if we find a diagonal we skip // horizontal/vertical processing. //SMAA_BRANCH if (weights.r == -weights.g) { // weights.r + weights.g == 0.0 @@ -433,7 +434,7 @@ vec4 SMAABlendingWeightCalculationPS(vec2 texcoord, vec2 pixcoord, // We want the distances to be in pixel units: d = abs(round(mad(screenSize.yy, d, -pixcoord.yy))); - // SMAAArea below needs a sqrt, as the areas texture is compressed + // SMAAArea below needs a sqrt, as the areas texture is compressed // quadratically: vec2 sqrt_d = sqrt(d); diff --git a/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl b/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl index 61b3961c..7ce815b3 100644 --- a/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl +++ b/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl @@ -11,7 +11,7 @@ out vec4 offset0; out vec4 offset1; out vec4 offset2; -#ifdef HLSL +#ifdef _InvY #define V_DIR(v) -(v) #else #define V_DIR(v) v @@ -21,7 +21,7 @@ void main() { // Scale vertex attribute to [0-1] range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif diff --git a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl index d307e79d..99dca290 100755 --- a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl +++ b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl @@ -18,7 +18,7 @@ out vec4 fragColor; // Neighborhood Blending Pixel Shader (Third Pass) vec4 textureLodA(sampler2D tex, vec2 coords, float lod) { - #ifdef HLSL + #ifdef _InvY coords.y = 1.0 - coords.y; #endif return textureLod(tex, coords, lod); @@ -49,7 +49,7 @@ vec4 SMAANeighborhoodBlendingPS(vec2 texcoord, vec4 offset) { // Calculate the blending offsets: vec4 blendingOffset = vec4(0.0, a.y, 0.0, a.w); vec2 blendingWeight = a.yw; - + if (h) { blendingOffset.x = a.x; blendingOffset.y = 0.0; @@ -58,11 +58,11 @@ vec4 SMAANeighborhoodBlendingPS(vec2 texcoord, vec4 offset) { blendingWeight.x = a.x; blendingWeight.y = a.z; } - + blendingWeight /= dot(blendingWeight, vec2(1.0, 1.0)); // Calculate the texture coordinates: - #ifdef HLSL + #ifdef _InvY vec2 tc = vec2(texcoord.x, 1.0 - texcoord.y); #else vec2 tc = texcoord; diff --git a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl index 8d433797..6568ab64 100644 --- a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl +++ b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl @@ -9,7 +9,7 @@ uniform vec2 screenSizeInv; out vec2 texCoord; out vec4 offset; -#ifdef HLSL +#ifdef _InvY #define V_DIR(v) -(v) #else #define V_DIR(v) v @@ -19,7 +19,7 @@ void main() { // Scale vertex attribute to [0-1] range const vec2 madd = vec2(0.5, 0.5); texCoord = pos.xy * madd + madd; - #ifdef HLSL + #ifdef _InvY texCoord.y = 1.0 - texCoord.y; #endif diff --git a/Shaders/ssgi_pass/ssgi_pass.frag.glsl b/Shaders/ssgi_pass/ssgi_pass.frag.glsl index acc6d341..694e0f21 100755 --- a/Shaders/ssgi_pass/ssgi_pass.frag.glsl +++ b/Shaders/ssgi_pass/ssgi_pass.frag.glsl @@ -37,7 +37,7 @@ vec2 getProjectedCoord(vec3 hitCoord) { vec4 projectedCoord = P * vec4(hitCoord, 1.0); projectedCoord.xy /= projectedCoord.w; projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; - #ifdef HLSL + #ifdef _InvY projectedCoord.y = 1.0 - projectedCoord.y; #endif return projectedCoord.xy; diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 0fc1ea0a..8ec84256 100755 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -29,13 +29,13 @@ vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); projectedCoord.xy /= projectedCoord.w; projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; - #ifdef HLSL + #ifdef _InvY projectedCoord.y = 1.0 - projectedCoord.y; #endif return projectedCoord.xy; } -float getDeltaDepth(const vec3 hit) { +float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; @@ -79,7 +79,7 @@ void main() { float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } - + float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; if (d == 1.0) { fragColor.rgb = vec3(0.0); return; } @@ -88,18 +88,18 @@ void main() { n.z = 1.0 - abs(enc.x) - abs(enc.y); n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); n = normalize(n); - + vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); vec3 reflected = normalize(reflect(viewPos, viewNormal)); hitCoord = viewPos; - + #ifdef _CPostprocess vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; #else vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; #endif - + // * max(ssrMinRayStep, -viewPos.z) vec4 coords = rayCast(dir); diff --git a/Shaders/std/gbuffer.glsl b/Shaders/std/gbuffer.glsl index d72a8783..233213c0 100755 --- a/Shaders/std/gbuffer.glsl +++ b/Shaders/std/gbuffer.glsl @@ -19,7 +19,7 @@ vec3 getPosView(const vec3 viewRay, const float depth, const vec2 cameraProj) { return viewRay * linearDepth; } -vec3 getPos(const vec3 eye, const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { +vec3 getPos(const vec3 eye, const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { // eyeLook, viewRay should be normalized float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); float viewZDist = dot(eyeLook, viewRay); @@ -27,7 +27,7 @@ vec3 getPos(const vec3 eye, const vec3 eyeLook, const vec3 viewRay, const float return wposition; } -vec3 getPosNoEye(const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { +vec3 getPosNoEye(const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { // eyeLook, viewRay should be normalized float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); float viewZDist = dot(eyeLook, viewRay); @@ -35,7 +35,7 @@ vec3 getPosNoEye(const vec3 eyeLook, const vec3 viewRay, const float depth, cons return wposition; } -#ifdef HLSL +#if defined(HLSL) || defined(METAL) vec3 getPos2(const mat4 invVP, const float depth, vec2 coord) { coord.y = 1.0 - coord.y; #else @@ -47,7 +47,7 @@ vec3 getPos2(const mat4 invVP, const float depth, const vec2 coord) { return pos.xyz; } -#ifdef HLSL +#if defined(HLSL) || defined(METAL) vec3 getPosView2(const mat4 invP, const float depth, vec2 coord) { coord.y = 1.0 - coord.y; #else @@ -59,7 +59,7 @@ vec3 getPosView2(const mat4 invP, const float depth, const vec2 coord) { return pos.xyz; } -#ifdef HLSL +#if defined(HLSL) || defined(METAL) vec3 getPos2NoEye(const vec3 eye, const mat4 invVP, const float depth, vec2 coord) { coord.y = 1.0 - coord.y; #else diff --git a/Shaders/std/shadows.glsl b/Shaders/std/shadows.glsl index 5a7769fb..d610676a 100755 --- a/Shaders/std/shadows.glsl +++ b/Shaders/std/shadows.glsl @@ -35,7 +35,7 @@ float PCFCube(samplerCubeShadow shadowMapCube, const vec3 lp, vec3 ml, const flo const float s = shadowmapCubePcfSize; // TODO: incorrect... float compare = lpToDepth(lp, lightProj) - bias * 1.5; ml = ml + n * bias * 20; - #ifdef HLSL + #ifdef _InvY ml.y = -ml.y; #endif float result = texture(shadowMapCube, vec4(ml, compare)); @@ -105,7 +105,7 @@ float shadowTestCascade(sampler2DShadow shadowMap, const vec3 eye, const vec3 p, int casi; int casIndex; mat4 LWVP = getCascadeMat(d, casi, casIndex); - + vec4 lPos = LWVP * vec4(p, 1.0); lPos.xyz /= lPos.w; diff --git a/Shaders/std/ssrs.glsl b/Shaders/std/ssrs.glsl index a04374f8..c9586610 100644 --- a/Shaders/std/ssrs.glsl +++ b/Shaders/std/ssrs.glsl @@ -9,7 +9,7 @@ vec2 getProjectedCoord(vec3 hitCoord) { vec4 projectedCoord = VP * vec4(hitCoord, 1.0); projectedCoord.xy /= projectedCoord.w; projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; - #ifdef HLSL + #if defined(HLSL) || defined(METAL) projectedCoord.y = 1.0 - projectedCoord.y; #endif return projectedCoord.xy; diff --git a/Shaders/taa_pass/taa_pass.frag.glsl b/Shaders/taa_pass/taa_pass.frag.glsl index 57ec11af..cdaf2bbb 100755 --- a/Shaders/taa_pass/taa_pass.frag.glsl +++ b/Shaders/taa_pass/taa_pass.frag.glsl @@ -15,12 +15,12 @@ const float SMAA_REPROJECTION_WEIGHT_SCALE = 30.0; void main() { vec4 current = textureLod(tex, texCoord, 0.0); - + #ifdef _Veloc // Velocity is assumed to be calculated for motion blur, so we need to inverse it for reprojection vec2 velocity = -textureLod(sveloc, texCoord, 0.0).rg; - #ifdef HLSL + #ifdef _InvY velocity.y = -velocity.y; #endif diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 87665920..90e243f6 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -37,6 +37,15 @@ class RenderPathDeferred { path = _path; + #if kha_metal + { + path.loadShader("shader_datas/clear_color_depth_pass/clear_color_depth_pass"); + path.loadShader("shader_datas/clear_color_pass/clear_color_pass"); + path.loadShader("shader_datas/clear_depth_pass/clear_depth_pass"); + path.clearShader = "shader_datas/clear_color_depth_pass/clear_color_depth_pass"; + } + #end + #if (rp_background == "World") { path.loadShader("shader_datas/world_pass/world_pass"); diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index e989636f..fc74be45 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -64,8 +64,10 @@ class RenderPathForward { #if kha_metal { - path.loadShader("shader_datas/clear_pass/clear_pass"); - path.clearShader = "shader_datas/clear_pass/clear_pass"; + path.loadShader("shader_datas/clear_color_depth_pass/clear_color_depth_pass"); + path.loadShader("shader_datas/clear_color_pass/clear_color_pass"); + path.loadShader("shader_datas/clear_depth_pass/clear_depth_pass"); + path.clearShader = "shader_datas/clear_color_depth_pass/clear_color_depth_pass"; } #end diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index 8da7d32c..f0e76103 100755 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -128,7 +128,9 @@ def build(): assets.add_khafile_def('rp_shadowmap_cube={0}'.format(rpdat.rp_shadowmap_cube)) if arm.utils.get_gapi() == 'metal': - assets.add_shader_pass('clear_pass') + assets.add_shader_pass('clear_color_depth_pass') + assets.add_shader_pass('clear_color_pass') + assets.add_shader_pass('clear_depth_pass') assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) if rpdat.rp_background == 'World': diff --git a/blender/arm/material/make_decal.py b/blender/arm/material/make_decal.py index 90a026a1..32e896e9 100644 --- a/blender/arm/material/make_decal.py +++ b/blender/arm/material/make_decal.py @@ -30,7 +30,7 @@ def make(context_id): vert.write('wnormal = N * vec3(0.0, 0.0, 1.0);') vert.write('wvpposition = WVP * vec4(pos.xyz, 1.0);') vert.write('gl_Position = wvpposition;') - + frag.add_include('compiled.inc') frag.add_include('std/gbuffer.glsl') frag.ins = vert.outs @@ -43,11 +43,11 @@ def make(context_id): frag.write_attrib(' vec2 screenPosition = wvpposition.xy / wvpposition.w;') frag.write_attrib(' vec2 depthCoord = screenPosition * 0.5 + 0.5;') - frag.write_attrib('#ifdef HLSL') + frag.write_attrib('#ifdef _InvY') frag.write_attrib(' depthCoord.y = 1.0 - depthCoord.y;') frag.write_attrib('#endif') frag.write_attrib(' float depth = texture(gbufferD, depthCoord).r * 2.0 - 1.0;') - + frag.write_attrib(' vec3 wpos = getPos2(invVP, depth, depthCoord);') frag.write_attrib(' vec4 mpos = invW * vec4(wpos, 1.0);') frag.write_attrib(' if (abs(mpos.x) > 1.0) discard;') diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 517a1a59..5965e850 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -413,7 +413,7 @@ def make_forward_mobile(con_mesh): frag.add_uniform('samplerCubeShadow shadowMapPoint[1]') frag.write('const float s = shadowmapCubePcfSize;') # TODO: incorrect... frag.write('float compare = lpToDepth(ld, lightProj) - pointBias * 1.5;') - frag.write('#ifdef HLSL') + frag.write('#ifdef _InvY') frag.write('l.y = -l.y;') frag.write('#endif') if '_Legacy' in wrd.world_defs: From 5aede77e2366ca6b623e93b94114810abb1b3ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 May 2020 17:29:46 +0200 Subject: [PATCH 136/230] Implement render emitter option --- blender/arm/exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index fefbbfac..fa28ec89 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -773,7 +773,8 @@ class ArmoryExporter: num_psys = len(bobject.particle_systems) if num_psys > 0: out_object['particle_refs'] = [] - for i in range(0, num_psys): + out_object['render_emitter'] = bobject.show_instancer_for_render + for i in range(num_psys): self.export_particle_system_ref(bobject.particle_systems[i], out_object) aabb = bobject.data.arm_aabb @@ -1893,7 +1894,6 @@ class ArmoryExporter: o['name'] = particleRef[1]["structName"] o['type'] = 0 if psettings.type == 'EMITTER' else 1 # HAIR o['loop'] = psettings.arm_loop - o['render_emitter'] = False # TODO # Emission o['count'] = int(psettings.count * psettings.arm_count_mult) o['frame_start'] = int(psettings.frame_start) From 0d55749f1f61207cf48acff12b070830dcf5e75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 May 2020 17:30:00 +0200 Subject: [PATCH 137/230] Fix linked particle instances --- blender/arm/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index fa28ec89..0b65a0d5 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1914,7 +1914,7 @@ class ArmoryExporter: o['size_random'] = psettings.size_random o['mass'] = psettings.mass # Render - o['instance_object'] = psettings.instance_object.name + o['instance_object'] = arm.utils.asset_name(psettings.instance_object) self.object_to_arm_object_dict[psettings.instance_object]['is_particle'] = True # Field weights o['weight_gravity'] = psettings.effector_weights.gravity From 133dfe15105f6768f715ec9db942ba403f46d0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 May 2020 17:35:21 +0200 Subject: [PATCH 138/230] Cleanup particle system export --- blender/arm/exporter.py | 62 ++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 0b65a0d5..f15b5fff 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1882,7 +1882,6 @@ class ArmoryExporter: if len(self.particle_system_array) > 0: self.output['particle_datas'] = [] for particleRef in self.particle_system_array.items(): - o = {} psettings = particleRef[0] if psettings is None: @@ -1891,34 +1890,41 @@ class ArmoryExporter: if psettings.instance_object is None or psettings.render_type != 'OBJECT': continue - o['name'] = particleRef[1]["structName"] - o['type'] = 0 if psettings.type == 'EMITTER' else 1 # HAIR - o['loop'] = psettings.arm_loop - # Emission - o['count'] = int(psettings.count * psettings.arm_count_mult) - o['frame_start'] = int(psettings.frame_start) - o['frame_end'] = int(psettings.frame_end) - o['lifetime'] = psettings.lifetime - o['lifetime_random'] = psettings.lifetime_random - o['emit_from'] = 1 if psettings.emit_from == 'VOLUME' else 0 # VERT, FACE - # Velocity - # o['normal_factor'] = psettings.normal_factor - # o['tangent_factor'] = psettings.tangent_factor - # o['tangent_phase'] = psettings.tangent_phase - o['object_align_factor'] = [psettings.object_align_factor[0], psettings.object_align_factor[1], psettings.object_align_factor[2]] - # o['object_factor'] = psettings.object_factor - o['factor_random'] = psettings.factor_random - # Physics - o['physics_type'] = 1 if psettings.physics_type == 'NEWTON' else 0 - o['particle_size'] = psettings.particle_size - o['size_random'] = psettings.size_random - o['mass'] = psettings.mass - # Render - o['instance_object'] = arm.utils.asset_name(psettings.instance_object) + out_particlesys = { + 'name': particleRef[1]["structName"], + 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR + 'loop': psettings.arm_loop, + # Emission + 'count': int(psettings.count * psettings.arm_count_mult), + 'frame_start': int(psettings.frame_start), + 'frame_end': int(psettings.frame_end), + 'lifetime': psettings.lifetime, + 'lifetime_random': psettings.lifetime_random, + 'emit_from': 1 if psettings.emit_from == 'VOLUME' else 0, # VERT, FACE + # Velocity + # 'normal_factor': psettings.normal_factor, + # 'tangent_factor': psettings.tangent_factor, + # 'tangent_phase': psettings.tangent_phase, + 'object_align_factor': ( + psettings.object_align_factor[0], + psettings.object_align_factor[1], + psettings.object_align_factor[2] + ), + # 'object_factor': psettings.object_factor, + 'factor_random': psettings.factor_random, + # Physics + 'physics_type': 1 if psettings.physics_type == 'NEWTON' else 0, + 'particle_size': psettings.particle_size, + 'size_random': psettings.size_random, + 'mass': psettings.mass, + # Render + 'instance_object': arm.utils.asset_name(psettings.instance_object), + # Field weights + 'weight_gravity': psettings.effector_weights.gravity + } + self.object_to_arm_object_dict[psettings.instance_object]['is_particle'] = True - # Field weights - o['weight_gravity'] = psettings.effector_weights.gravity - self.output['particle_datas'].append(o) + self.output['particle_datas'].append(out_particlesys) def export_tilesheets(self): wrd = bpy.data.worlds['Arm'] From c056e996f1fd2304fab255e8541ba578525f75fd Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 13 May 2020 02:31:41 +0200 Subject: [PATCH 139/230] Fixed issue where element is not visible --- Sources/armory/logicnode/OnCanvasElementNode.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index e0778330..78a90603 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -30,7 +30,7 @@ class OnCanvasElementNode extends LogicNode { if(canvas == null) return; if (!canvas.ready) return; if(canvas.getElement(element) == null) return; - + if(canvas.getElement(element).visible == false) return; var mouse = iron.system.Input.getMouse(); var b = false; switch (property0) { From 8cc5d07ad6d270016f98e76fb13db7ce48f4d045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 May 2020 17:23:36 +0200 Subject: [PATCH 140/230] Fix onCanvasElement node when Zui is disabled --- Sources/armory/logicnode/OnCanvasElementNode.hx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index 78a90603..9ded15d6 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -1,8 +1,11 @@ package armory.logicnode; -import zui.Canvas.Anchor; -import iron.Scene; import armory.trait.internal.CanvasScript; +import iron.Scene; + +#if arm_ui +import zui.Canvas.Anchor; +#end class OnCanvasElementNode extends LogicNode { @@ -19,7 +22,7 @@ class OnCanvasElementNode extends LogicNode { tree.notifyOnUpdate(update); } -#if arm_ui + #if arm_ui function update() { element = inputs[0].get(); @@ -87,5 +90,7 @@ class OnCanvasElementNode extends LogicNode { } } } -#end + #else + function update() {} + #end } From c820b4ddc20c28c2228c0fc77bc019918ebd22e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 May 2020 17:29:02 +0200 Subject: [PATCH 141/230] onCanvasElementNode: massive performance improvement + better variable names --- .../armory/logicnode/OnCanvasElementNode.hx | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index 9ded15d6..4a171a3b 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -46,44 +46,45 @@ class OnCanvasElementNode extends LogicNode { } if (b) { - - var x1 = canvas.getElement(element).x; - var y1 = canvas.getElement(element).y; - var anchor = canvas.getElement(element).anchor; + var canvasElem = canvas.getElement(element); + var left = canvasElem.x; + var top = canvasElem.y; + var right = left + canvasElem.width; + var bottom = top + canvasElem.height; + + var anchor = canvasElem.anchor; var cx = canvas.getCanvas().width; var cy = canvas.getCanvas().height; var mouseX = mouse.x; var mouseY = mouse.y; - var x2 = x1 + canvas.getElement(element).width; - var y2 = y1 + canvas.getElement(element).height; switch(anchor) { case Top: - mouseX -= cx/2 - canvas.getElement(element).width/2; + mouseX -= cx/2 - canvasElem.width/2; case TopRight: - mouseX -= cx - canvas.getElement(element).width; + mouseX -= cx - canvasElem.width; case CenterLeft: - mouseY -= cy/2 - canvas.getElement(element).height/2; + mouseY -= cy/2 - canvasElem.height/2; case Anchor.Center: - mouseX -= cx/2 - canvas.getElement(element).width/2; - mouseY -= cy/2 - canvas.getElement(element).height/2; + mouseX -= cx/2 - canvasElem.width/2; + mouseY -= cy/2 - canvasElem.height/2; case CenterRight: - mouseX -= cx - canvas.getElement(element).width; - mouseY -= cy/2 - canvas.getElement(element).height/2; + mouseX -= cx - canvasElem.width; + mouseY -= cy/2 - canvasElem.height/2; case BottomLeft: - mouseY -= cy - canvas.getElement(element).height; + mouseY -= cy - canvasElem.height; case Bottom: - mouseX -= cx/2 - canvas.getElement(element).width/2; - mouseY -= cy - canvas.getElement(element).height; + mouseX -= cx/2 - canvasElem.width/2; + mouseY -= cy - canvasElem.height; case BottomRight: - mouseX -= cx - canvas.getElement(element).width; - mouseY -= cy - canvas.getElement(element).height; + mouseX -= cx - canvasElem.width; + mouseY -= cy - canvasElem.height; } - - if((mouseX >= x1) && (mouseX <= x2)) + + if((mouseX >= left) && (mouseX <= right)) { - if((mouseY >= y1) && (mouseY <= y2)) + if((mouseY >= top) && (mouseY <= bottom)) { runOutput(0); } From 1d82747fbc37bc80dff0dbedac3ce50afcf3f785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 May 2020 17:34:39 +0200 Subject: [PATCH 142/230] onCanvasElementNodes can now listen to hover events --- .../armory/logicnode/OnCanvasElementNode.hx | 45 +++++++++++++------ .../arm/logicnode/input_on_canvas_element.py | 34 ++++++++------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index 4a171a3b..b38b4ea1 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -11,22 +11,33 @@ class OnCanvasElementNode extends LogicNode { var canvas: CanvasScript; var element: String; - + + /** + * The event type this node should react to, can be "click" or "hover". + */ public var property0: String; + /** + * If the event type is click, this property states whether to check for + * "down", "started" or "released" events. + */ public var property1: String; + /** + * The mouse button that this node should react to. Only used when listening + * for mouse clicks. + */ + public var property2: String; public function new(tree: LogicTree) { super(tree); - // Ensure canvas is ready tree.notifyOnUpdate(update); } #if arm_ui function update() { + element = inputs[0].get(); - element = inputs[0].get(); - + // Ensure canvas is ready if(!Scene.active.ready) return; canvas = Scene.active.getTrait(CanvasScript); if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); @@ -35,16 +46,24 @@ class OnCanvasElementNode extends LogicNode { if(canvas.getElement(element) == null) return; if(canvas.getElement(element).visible == false) return; var mouse = iron.system.Input.getMouse(); - var b = false; - switch (property0) { - case "Down": - b = mouse.down(property1); - case "Started": - b = mouse.started(property1); - case "Released": - b = mouse.released(property1); + var isEvent = false; + + if (property0 == "click") { + switch (property1) { + case "down": + isEvent = mouse.down(property2); + case "started": + isEvent = mouse.started(property2); + case "released": + isEvent = mouse.released(property2); + } } - if (b) + // Hovered + else { + isEvent = true; + } + + if (isEvent) { var canvasElem = canvas.getElement(element); var left = canvasElem.x; diff --git a/blender/arm/logicnode/input_on_canvas_element.py b/blender/arm/logicnode/input_on_canvas_element.py index 9076b72f..2d90da54 100644 --- a/blender/arm/logicnode/input_on_canvas_element.py +++ b/blender/arm/logicnode/input_on_canvas_element.py @@ -1,30 +1,38 @@ import bpy from bpy.props import * -from bpy.types import Node, NodeSocket +from bpy.types import Node from arm.logicnode.arm_nodes import * class OnCanvasElementNode(Node, ArmLogicTreeNode): - '''On canvas element node''' + """On canvas element node""" bl_idname = 'LNOnCanvasElementNode' bl_label = 'On Canvas Element' bl_icon = 'CURVE_PATH' + property0: EnumProperty( - items = [('Down', 'Down', 'Down'), - ('Started', 'Started', 'Started'), - ('Released', 'Released', 'Released')], - name='', default='Down') + items=[('click', 'Click', 'Listen to mouse clicks'), + ('hover', 'Hover', 'Listen to mouse hover')], + name='Listen to', default='click') property1: EnumProperty( - items = [('left', 'left', 'left'), - ('right', 'right', 'right'), - ('middle', 'middle', 'middle')], - name='Mouse button', default='left') - + items=[('down', 'Down', 'Down'), + ('started', 'Started', 'Started'), + ('released', 'Released', 'Released')], + name='Status', default='down') + property2: EnumProperty( + items=[('left', 'Left', 'Left Button'), + ('right', 'Right', 'Right Button'), + ('middle', 'Middle', 'Middle Button')], + name='Mouse Button', default='left') + def init(self, context): - self.inputs.new('NodeSocketString','Element') + self.inputs.new('NodeSocketString', 'Element') self.outputs.new('ArmNodeSocketAction', 'Out') def draw_buttons(self, context, layout): layout.prop(self, 'property0') - layout.prop(self, 'property1') + + if self.property0 == "click": + layout.prop(self, 'property1') + layout.prop(self, 'property2') add_node(OnCanvasElementNode, category='Input') From 9f4a0344fe91a998e34120b9698e34c3a2d21f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 15 May 2020 23:22:04 +0200 Subject: [PATCH 143/230] Fix boolean type node properties --- blender/arm/make_logic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index 35751f7e..592510ba 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -154,6 +154,8 @@ def build_node(node, f): prop = getattr(node, prop_name) if isinstance(prop, str): prop = '"' + str(prop) + '"' + elif isinstance(prop, bool): + prop = str(prop).lower() elif hasattr(prop, 'name'): # PointerProperty prop = '"' + str(prop.name) + '"' else: From 4fa9d8b98038584866b51eb50263cc2a7d95b3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 15 May 2020 23:22:41 +0200 Subject: [PATCH 144/230] Add optional sample rate setting to PlaySoundRawNode --- Sources/armory/logicnode/PlaySoundRawNode.hx | 9 +++++++++ blender/arm/logicnode/sound_play_sound.py | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index da877eef..2f394f6b 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -3,6 +3,14 @@ package armory.logicnode; class PlaySoundRawNode extends LogicNode { public var property0: String; + /** + * Override sample rate + */ + public var property1: Bool; + /** + * Playback sample rate + */ + public var property2: Int; public function new(tree: LogicTree) { super(tree); @@ -10,6 +18,7 @@ class PlaySoundRawNode extends LogicNode { override function run(from: Int) { iron.data.Data.getSound(property0, function(sound: kha.Sound) { + if (property1) sound.sampleRate = property2; iron.system.Audio.play(sound, false); }); runOutput(0); diff --git a/blender/arm/logicnode/sound_play_sound.py b/blender/arm/logicnode/sound_play_sound.py index 1dfb3bdf..b8ad271c 100644 --- a/blender/arm/logicnode/sound_play_sound.py +++ b/blender/arm/logicnode/sound_play_sound.py @@ -4,12 +4,21 @@ from bpy.types import Node, NodeSocket from arm.logicnode.arm_nodes import * class PlaySoundNode(Node, ArmLogicTreeNode): - '''Play sound node''' + """Play sound node""" bl_idname = 'LNPlaySoundRawNode' bl_label = 'Play Sound' bl_icon = 'QUESTION' property0: PointerProperty(name='', type=bpy.types.Sound) + property1: BoolProperty( + name='Use Custom Sample Rate', + description='If enabled, override the default sample rate', + default=False) + property2: IntProperty( + name='Sample Rate', + description='Set the sample rate used to play this sound', + default=44100, + min=0) def init(self, context): self.inputs.new('ArmNodeSocketAction', 'In') @@ -18,4 +27,13 @@ class PlaySoundNode(Node, ArmLogicTreeNode): def draw_buttons(self, context, layout): layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') + layout.label(text="Overrides:") + # Sample rate + split = layout.split(factor=0.15, align=False) + split.prop(self, 'property1', text="") + row = split.row() + if not self.property1: + row.enabled = False + row.prop(self, 'property2') + add_node(PlaySoundNode, category='Sound') From 55ce26c02f6fed03e6544aff143da099db3c7c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 15 May 2020 23:32:48 +0200 Subject: [PATCH 145/230] PlaySoundRawNode: add loop option --- Sources/armory/logicnode/PlaySoundRawNode.hx | 17 ++++++++--------- blender/arm/logicnode/sound_play_sound.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index 2f394f6b..69a51bdb 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -2,15 +2,14 @@ package armory.logicnode; class PlaySoundRawNode extends LogicNode { + /** The name of the sound */ public var property0: String; - /** - * Override sample rate - */ + /** Whether to loop the playback */ public var property1: Bool; - /** - * Playback sample rate - */ - public var property2: Int; + /** Override sample rate */ + public var property2: Bool; + /** Playback sample rate */ + public var property3: Int; public function new(tree: LogicTree) { super(tree); @@ -18,8 +17,8 @@ class PlaySoundRawNode extends LogicNode { override function run(from: Int) { iron.data.Data.getSound(property0, function(sound: kha.Sound) { - if (property1) sound.sampleRate = property2; - iron.system.Audio.play(sound, false); + if (property2) sound.sampleRate = property3; + iron.system.Audio.play(sound, property1); }); runOutput(0); } diff --git a/blender/arm/logicnode/sound_play_sound.py b/blender/arm/logicnode/sound_play_sound.py index b8ad271c..d9214133 100644 --- a/blender/arm/logicnode/sound_play_sound.py +++ b/blender/arm/logicnode/sound_play_sound.py @@ -11,10 +11,14 @@ class PlaySoundNode(Node, ArmLogicTreeNode): property0: PointerProperty(name='', type=bpy.types.Sound) property1: BoolProperty( + name='Loop', + description='Play the sound in a loop', + default=False) + property2: BoolProperty( name='Use Custom Sample Rate', description='If enabled, override the default sample rate', default=False) - property2: IntProperty( + property3: IntProperty( name='Sample Rate', description='Set the sample rate used to play this sound', default=44100, @@ -27,13 +31,15 @@ class PlaySoundNode(Node, ArmLogicTreeNode): def draw_buttons(self, context, layout): layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') + layout.prop(self, 'property1') + layout.label(text="Overrides:") # Sample rate split = layout.split(factor=0.15, align=False) - split.prop(self, 'property1', text="") + split.prop(self, 'property2', text="") row = split.row() - if not self.property1: + if not self.property2: row.enabled = False - row.prop(self, 'property2') + row.prop(self, 'property3') add_node(PlaySoundNode, category='Sound') From 49c5b1b12941d6524741c6dcd7d065ac8a5d599c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 15 May 2020 23:37:18 +0200 Subject: [PATCH 146/230] Change PlaySoundRawNode icon Does this even show up in the UI? --- blender/arm/logicnode/sound_play_sound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/sound_play_sound.py b/blender/arm/logicnode/sound_play_sound.py index d9214133..7431aaee 100644 --- a/blender/arm/logicnode/sound_play_sound.py +++ b/blender/arm/logicnode/sound_play_sound.py @@ -7,7 +7,7 @@ class PlaySoundNode(Node, ArmLogicTreeNode): """Play sound node""" bl_idname = 'LNPlaySoundRawNode' bl_label = 'Play Sound' - bl_icon = 'QUESTION' + bl_icon = 'PLAY_SOUND' property0: PointerProperty(name='', type=bpy.types.Sound) property1: BoolProperty( From 18ebd3444f274b91cdc5b1413f1581209c92d7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 16 May 2020 00:21:10 +0200 Subject: [PATCH 147/230] PlaySoundRawNode: add pause/stop functionality --- Sources/armory/logicnode/PlaySoundRawNode.hx | 63 ++++++++++++++++++-- blender/arm/logicnode/sound_play_sound.py | 6 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index 69a51bdb..679f4d12 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -11,15 +11,68 @@ class PlaySoundRawNode extends LogicNode { /** Playback sample rate */ public var property3: Int; + var sound: kha.Sound = null; + var channel: kha.audio1.AudioChannel = null; + public function new(tree: LogicTree) { super(tree); } override function run(from: Int) { - iron.data.Data.getSound(property0, function(sound: kha.Sound) { - if (property2) sound.sampleRate = property3; - iron.system.Audio.play(sound, property1); - }); - runOutput(0); + switch (from) { + case Play: + if (sound == null) { + iron.data.Data.getSound(property0, function(s: kha.Sound) { + this.sound = s; + }); + } + + // Resume + if (channel != null) { + channel.play(); + } + // Start + else if (sound != null) { + if (property2) sound.sampleRate = property3; + channel = iron.system.Audio.play(sound, property1); + } + + tree.notifyOnUpdate(this.onUpdate); + runOutput(0); + + case Pause: + if (channel != null) { + channel.pause(); + } + + tree.removeUpdate(this.onUpdate); + + case Stop: + if (channel != null) { + channel.stop(); + } + + tree.removeUpdate(this.onUpdate); + } + } + + function onUpdate() { + if (channel != null) { + // Done + if (channel.finished) { + channel = null; + runOutput(2); + } + // Running + else { + runOutput(1); + } + } } } + +private enum abstract PlayState(Int) from Int to Int { + var Play = 0; + var Pause = 1; + var Stop = 2; +} diff --git a/blender/arm/logicnode/sound_play_sound.py b/blender/arm/logicnode/sound_play_sound.py index 7431aaee..cfae3f9c 100644 --- a/blender/arm/logicnode/sound_play_sound.py +++ b/blender/arm/logicnode/sound_play_sound.py @@ -25,8 +25,12 @@ class PlaySoundNode(Node, ArmLogicTreeNode): min=0) def init(self, context): - self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('ArmNodeSocketAction', 'Play') + self.inputs.new('ArmNodeSocketAction', 'Pause') + self.inputs.new('ArmNodeSocketAction', 'Stop') self.outputs.new('ArmNodeSocketAction', 'Out') + self.outputs.new('ArmNodeSocketAction', 'Running') + self.outputs.new('ArmNodeSocketAction', 'Done') def draw_buttons(self, context, layout): layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') From 5382399b70bc3d27dd048ae132e2b7052ae296c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 16 May 2020 00:28:52 +0200 Subject: [PATCH 148/230] Add retrigger option to Play Sound node --- Sources/armory/logicnode/PlaySoundRawNode.hx | 23 ++++++++------------ blender/arm/logicnode/sound_play_sound.py | 16 +++++++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index 679f4d12..c1d402e1 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -6,10 +6,12 @@ class PlaySoundRawNode extends LogicNode { public var property0: String; /** Whether to loop the playback */ public var property1: Bool; - /** Override sample rate */ + /** Retrigger */ public var property2: Bool; + /** Override sample rate */ + public var property3: Bool; /** Playback sample rate */ - public var property3: Int; + public var property4: Int; var sound: kha.Sound = null; var channel: kha.audio1.AudioChannel = null; @@ -29,11 +31,12 @@ class PlaySoundRawNode extends LogicNode { // Resume if (channel != null) { + if (property2) channel.stop(); channel.play(); } // Start else if (sound != null) { - if (property2) sound.sampleRate = property3; + if (property3) sound.sampleRate = property4; channel = iron.system.Audio.play(sound, property1); } @@ -41,17 +44,11 @@ class PlaySoundRawNode extends LogicNode { runOutput(0); case Pause: - if (channel != null) { - channel.pause(); - } - + if (channel != null) channel.pause(); tree.removeUpdate(this.onUpdate); case Stop: - if (channel != null) { - channel.stop(); - } - + if (channel != null) channel.stop(); tree.removeUpdate(this.onUpdate); } } @@ -64,9 +61,7 @@ class PlaySoundRawNode extends LogicNode { runOutput(2); } // Running - else { - runOutput(1); - } + else runOutput(1); } } } diff --git a/blender/arm/logicnode/sound_play_sound.py b/blender/arm/logicnode/sound_play_sound.py index cfae3f9c..17cebdd6 100644 --- a/blender/arm/logicnode/sound_play_sound.py +++ b/blender/arm/logicnode/sound_play_sound.py @@ -15,10 +15,14 @@ class PlaySoundNode(Node, ArmLogicTreeNode): description='Play the sound in a loop', default=False) property2: BoolProperty( + name='Retrigger', + description='Play the sound from the beginning everytime', + default=False) + property3: BoolProperty( name='Use Custom Sample Rate', description='If enabled, override the default sample rate', default=False) - property3: IntProperty( + property4: IntProperty( name='Sample Rate', description='Set the sample rate used to play this sound', default=44100, @@ -35,15 +39,17 @@ class PlaySoundNode(Node, ArmLogicTreeNode): def draw_buttons(self, context, layout): layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') - layout.prop(self, 'property1') + col = layout.column(align=True) + col.prop(self, 'property1') + col.prop(self, 'property2') layout.label(text="Overrides:") # Sample rate split = layout.split(factor=0.15, align=False) - split.prop(self, 'property2', text="") + split.prop(self, 'property3', text="") row = split.row() - if not self.property2: + if not self.property3: row.enabled = False - row.prop(self, 'property3') + row.prop(self, 'property4') add_node(PlaySoundNode, category='Sound') From 03490e44e3e33ac80cf43b3b20a8e02c74b56b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 16 May 2020 00:42:12 +0200 Subject: [PATCH 149/230] Play Sound node: run "done" on stop --- Sources/armory/logicnode/PlaySoundRawNode.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index c1d402e1..c957c6a9 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -50,6 +50,7 @@ class PlaySoundRawNode extends LogicNode { case Stop: if (channel != null) channel.stop(); tree.removeUpdate(this.onUpdate); + runOutput(2); } } From 2b5ca912fba0626f6ec2da974d6a59e5138f4876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 21 May 2020 12:01:21 +0200 Subject: [PATCH 150/230] Fix for running blender in background mode --- blender/arm/keymap.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/blender/arm/keymap.py b/blender/arm/keymap.py index 12c0bad2..b4a019ea 100644 --- a/blender/arm/keymap.py +++ b/blender/arm/keymap.py @@ -1,12 +1,21 @@ import bpy import arm.props_ui as props_ui -import arm.utils arm_keymaps = [] def register(): wm = bpy.context.window_manager - km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW") + addon_keyconfig = wm.keyconfigs.addon + + # Keyconfigs are not available in background mode. If the keyconfig + # was not found despite running _not_ in background mode, a warning + # is printed + if addon_keyconfig is None: + if not bpy.app.background: + print("Armory warning: no keyconfig path found") + return + + km = addon_keyconfig.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW") km.keymap_items.new(props_ui.ArmoryPlayButton.bl_idname, type='F5', value='PRESS') arm_keymaps.append(km) From bba3b924c0f3a30588111fad775e007cebbb28f9 Mon Sep 17 00:00:00 2001 From: tong Date: Sat, 23 May 2020 12:18:07 +0200 Subject: [PATCH 151/230] Allow to set custom play scene --- blender/arm/props.py | 1 + blender/arm/props_ui.py | 1 + blender/arm/utils.py | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 7d2e1e1d..e3cd303c 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -71,6 +71,7 @@ def init_properties(): items=[('Scene', 'Scene', 'Scene'), ('Viewport', 'Viewport', 'Viewport')], name="Camera", description="Viewport camera", default='Scene', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_play_scene = PointerProperty(name="Scene", description="Scene to launch", update=assets.invalidate_compiler_cache, type=bpy.types.Scene) bpy.types.World.arm_debug_console = BoolProperty(name="Debug Console", description="Show inspector in player and enable debug draw.\nRequires that Zui is not disabled", default=False, update=assets.invalidate_shader_cache) bpy.types.World.arm_verbose_output = BoolProperty(name="Verbose Output", description="Print additional information to the console during compilation", default=False) bpy.types.World.arm_runtime = EnumProperty( diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index eb7d7088..4bbe9b45 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -272,6 +272,7 @@ class ARM_PT_ArmoryPlayerPanel(bpy.types.Panel): row.operator("arm.clean_menu") layout.prop(wrd, 'arm_runtime') layout.prop(wrd, 'arm_play_camera') + layout.prop(wrd, 'arm_play_scene') if log.num_warnings > 0: box = layout.box() diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 3aec6fc1..78bf1472 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -559,10 +559,12 @@ def get_project_scene_name(): return get_active_scene().name def get_active_scene(): + wrd = bpy.data.worlds['Arm'] if not state.is_export: - return bpy.context.scene + if wrd.arm_play_scene == None: + return bpy.context.scene + return wrd.arm_play_scene else: - wrd = bpy.data.worlds['Arm'] item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] return item.arm_project_scene From 6c94354da6e5f052fd1ebf5bceea272434387759 Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 24 May 2020 18:28:53 +0200 Subject: [PATCH 152/230] Proper debug console log output formatting --- Sources/armory/trait/internal/DebugConsole.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 4062cd17..e7f2dff8 100755 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -118,7 +118,7 @@ class DebugConsole extends Trait { static var haxeTrace: Dynamic->haxe.PosInfos->Void = null; static var lastTraces: Array = [""]; static function consoleTrace(v: Dynamic, ?inf: haxe.PosInfos) { - lastTraces.unshift(Std.string(v)); + lastTraces.unshift(haxe.Log.formatOutput(v,inf)); if (lastTraces.length > 10) lastTraces.pop(); haxeTrace(v, inf); } From 12cdd57748a5beefebe0d5b6e079e80bbb2e155b Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sat, 30 May 2020 22:50:41 +0200 Subject: [PATCH 153/230] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index e3cd303c..b9854531 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -12,7 +12,7 @@ import arm.proxy import arm.nodes_logic # Armory version -arm_version = '2020.5' +arm_version = '2020.6' arm_commit = '$Id$' def init_properties(): From dd72b2dd1848e57ee7ac9b19e5ae612495ee43a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 4 Jun 2020 22:15:41 +0200 Subject: [PATCH 154/230] Fix multi-usage of particle systems --- blender/arm/exporter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index f15b5fff..69360baf 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -442,9 +442,6 @@ class ArmoryExporter: o['material_refs'].append(arm.utils.asset_name(material)) def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): - if psys.settings in self.particle_system_array: # or not modifier.show_render: - return - if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT': return From 2e398c96ba344783254d1785ad8d8c61206ac3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 4 Jun 2020 22:19:02 +0200 Subject: [PATCH 155/230] Implement "reflect" operation for VectorMathNode --- Sources/armory/logicnode/VectorMathNode.hx | 2 ++ blender/arm/logicnode/value_vector_math.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/VectorMathNode.hx b/Sources/armory/logicnode/VectorMathNode.hx index c19880a7..88455735 100644 --- a/Sources/armory/logicnode/VectorMathNode.hx +++ b/Sources/armory/logicnode/VectorMathNode.hx @@ -42,6 +42,8 @@ class VectorMathNode extends LogicNode { f = v.length(); case "Distance": f = v.distanceTo(v2); + case "Reflect": + v.reflect(v2); } if (from == 0) return v; diff --git a/blender/arm/logicnode/value_vector_math.py b/blender/arm/logicnode/value_vector_math.py index 9091981b..25d2ca66 100644 --- a/blender/arm/logicnode/value_vector_math.py +++ b/blender/arm/logicnode/value_vector_math.py @@ -18,9 +18,10 @@ class VectorMathNode(Node, ArmLogicTreeNode): ('Cross Product', 'Cross Product', 'Cross Product'), ('Length', 'Length', 'Length'), ('Distance', 'Distance', 'Distance'), + ('Reflect', 'Reflect', 'Reflect'), ], name='', default='Add') - + def init(self, context): self.inputs.new('NodeSocketVector', 'Vector') self.inputs[-1].default_value = [0.5, 0.5, 0.5] From 807497daf3398a13498141b5411f6aff177644d6 Mon Sep 17 00:00:00 2001 From: tong Date: Fri, 5 Jun 2020 10:53:57 +0200 Subject: [PATCH 156/230] Replace __js__ with Syntax.code --- Sources/armory/system/Starter.hx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx index 5661615f..31ef84e5 100644 --- a/Sources/armory/system/Starter.hx +++ b/Sources/armory/system/Starter.hx @@ -78,7 +78,7 @@ class Starter { kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { var print = function(s:String) { trace(s); }; var loaded = function() { tasks--; start(); }; - untyped __js__("(1, eval)({0})", b.toString()); + js.Syntax.code("(1, eval)({0})", b.toString()); #if kha_krom var instantiateWasm = function(imports, successCallback) { var wasmbin = Krom.loadBlob("ammo.wasm.wasm"); @@ -87,9 +87,9 @@ class Starter { successCallback(inst); return inst.exports; }; - untyped __js__("Ammo({print:print, instantiateWasm:instantiateWasm}).then(loaded)"); + js.Syntax.code("Ammo({print:print, instantiateWasm:instantiateWasm}).then(loaded)"); #else - untyped __js__("Ammo({print:print}).then(loaded)"); + js.Syntax.code("Ammo({print:print}).then(loaded)"); #end }); } @@ -98,7 +98,7 @@ class Starter { #if (js && arm_navigation) function loadLib(name: String) { kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { - untyped __js__("(1, eval)({0})", b.toString()); + js.Syntax.code("(1, eval)({0})", b.toString()); tasks--; start(); }); From 3cd6ab133134010eeb95676dcf004109bdb40742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 7 Jun 2020 20:02:07 +0200 Subject: [PATCH 157/230] Escape '"' in logic node strings --- blender/arm/make_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index 592510ba..00d1e437 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -249,7 +249,7 @@ def build_default_node(inp): inp_name = 'new armory.logicnode.ObjectNode(this, "' + str(inp.get_default_value()) + '")' return inp_name if inp.bl_idname == 'ArmNodeSocketAnimAction': - inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.get_default_value()) + '")' + inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.get_default_value()).replace("\"", "\\\"") + '")' return inp_name if inp.type == 'VECTOR': inp_name = 'new armory.logicnode.VectorNode(this, ' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ', ' + str(inp.default_value[2]) + ')' @@ -264,5 +264,5 @@ def build_default_node(inp): elif inp.type == 'BOOLEAN': inp_name = 'new armory.logicnode.BooleanNode(this, ' + str(inp.default_value).lower() + ')' elif inp.type == 'STRING': - inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.default_value) + '")' + inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.default_value).replace("\"", "\\\"") + '")' return inp_name From 1dfc777f2bcbf8afcda8369e3cd0e7ff9fe68402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 8 Jun 2020 08:49:21 +0200 Subject: [PATCH 158/230] Add CustomSocket node socket type --- blender/arm/logicnode/arm_nodes.py | 9 +++++++++ blender/arm/make_logic.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index c94b93ae..e613c6f5 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -24,6 +24,15 @@ class ArmActionSocket(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.8, 0.3, 0.3, 1) +class ArmCustomSocket(bpy.types.NodeSocket): + """ + A custom socket that can be used to define more socket types for + logic node packs. Do not use this type directly (it is not + registered)! + """ + bl_idname = 'ArmCustomSocket' + bl_label = 'Custom Socket' + class ArmArraySocket(bpy.types.NodeSocket): bl_idname = 'ArmNodeSocketArray' bl_label = 'Array Socket' diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index 00d1e437..c54e0bb5 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -100,7 +100,7 @@ def build_node_tree(node_group): f.write('}') node_group.arm_cached = True -def build_node(node, f): +def build_node(node: bpy.types.Node, f): global parsed_nodes global parsed_ids @@ -243,6 +243,8 @@ def get_root_nodes(node_group): def build_default_node(inp): inp_name = 'new armory.logicnode.NullNode(this)' + if isinstance(inp, arm.logicnode.arm_nodes.ArmCustomSocket): + return inp_name if inp.bl_idname == 'ArmNodeSocketAction' or inp.bl_idname == 'ArmNodeSocketArray': return inp_name if inp.bl_idname == 'ArmNodeSocketObject': From 107c61ad3e7fa4666815e864dadb3d5dc07c3989 Mon Sep 17 00:00:00 2001 From: tong Date: Mon, 8 Jun 2020 12:26:17 +0200 Subject: [PATCH 159/230] Colored terminal output --- blender/arm/log.py | 27 ++++++++++++++++++++++++--- blender/arm/make.py | 8 ++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/blender/arm/log.py b/blender/arm/log.py index 3ad44e6a..a0d3b49c 100644 --- a/blender/arm/log.py +++ b/blender/arm/log.py @@ -1,3 +1,9 @@ +DEBUG = 36 +INFO = 37 +WARN = 35 +ERROR = 31 + +no_colors = False info_text = '' num_warnings = 0 @@ -10,12 +16,27 @@ def clear(clear_warnings=False): def format_text(text): return (text[:80] + '..') if len(text) > 80 else text # Limit str size -def print_info(text): - global info_text +def log(text,color=None): + if not no_colors and color is not None: + csi = '\033[' + text = csi + str(color) + 'm' + text + csi + '0m'; print(text) + +def debug(text): + log(text,DEBUG) + +def info(text): + global info_text + log(text,INFO) info_text = format_text(text) +def print_warn(text): + log('Warning: ' + text,WARN) + def warn(text): global num_warnings num_warnings += 1 - print('Armory Warning: ' + text) + print_warn(text) + +def error(text): + log('ERROR: ' + text,ERROR) diff --git a/blender/arm/make.py b/blender/arm/make.py index daf6ef34..e77953fb 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -388,7 +388,7 @@ def assets_done(): else: state.proc_build = None state.redraw_ui = True - log.print_info('Build failed, check console') + log.error('Build failed, check console') def compilation_server_done(): if state.proc_build == None: @@ -403,12 +403,12 @@ def compilation_server_done(): else: state.proc_build = None state.redraw_ui = True - log.print_info('Build failed, check console') + log.error('Build failed, check console') def build_done(): print('Finished in ' + str(time.time() - profile_time)) if log.num_warnings > 0: - print(f'{log.num_warnings} warnings occurred during compilation!') + log.print_warn(f'{log.num_warnings} warnings occurred during compilation') if state.proc_build is None: return result = state.proc_build.poll() @@ -418,7 +418,7 @@ def build_done(): bpy.data.worlds['Arm'].arm_recompile = False build_success() else: - log.print_info('Build failed, check console') + log.error('Build failed, check console') def patch(): if state.proc_build != None: From b75e19e88aa48deb54da6ce2c94384ec21c791b1 Mon Sep 17 00:00:00 2001 From: tong Date: Mon, 8 Jun 2020 13:32:06 +0200 Subject: [PATCH 160/230] Disable terminal colors on windows < 10 --- blender/arm/log.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/blender/arm/log.py b/blender/arm/log.py index a0d3b49c..ea2fdf8f 100644 --- a/blender/arm/log.py +++ b/blender/arm/log.py @@ -1,9 +1,15 @@ +import platform + DEBUG = 36 INFO = 37 WARN = 35 ERROR = 31 -no_colors = False +if platform.system() == "Windows": + HAS_COLOR_SUPPORT = platform.release() == "10" +else: + HAS_COLOR_SUPPORT = True + info_text = '' num_warnings = 0 @@ -17,7 +23,7 @@ def format_text(text): return (text[:80] + '..') if len(text) > 80 else text # Limit str size def log(text,color=None): - if not no_colors and color is not None: + if HAS_COLOR_SUPPORT and color is not None: csi = '\033[' text = csi + str(color) + 'm' + text + csi + '0m'; print(text) From 85411312d73c044d24846418d0fd05509f5e051b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 10 Jun 2020 14:25:01 +0200 Subject: [PATCH 161/230] Implement custom default values for custom node sockets --- blender/arm/logicnode/arm_nodes.py | 4 ++++ blender/arm/make_logic.py | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index e613c6f5..af0636aa 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -33,6 +33,10 @@ class ArmCustomSocket(bpy.types.NodeSocket): bl_idname = 'ArmCustomSocket' bl_label = 'Custom Socket' + def get_default_value(self): + """Override this for values of unconnected input sockets.""" + return None + class ArmArraySocket(bpy.types.NodeSocket): bl_idname = 'ArmNodeSocketArray' bl_label = 'Array Socket' diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index c54e0bb5..0ac32173 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -241,10 +241,21 @@ def get_root_nodes(node_group): roots.append(node) return roots -def build_default_node(inp): +def build_default_node(inp: bpy.types.NodeSocket): + """Creates a new node to give a not connected input socket a value""" inp_name = 'new armory.logicnode.NullNode(this)' + if isinstance(inp, arm.logicnode.arm_nodes.ArmCustomSocket): - return inp_name + # ArmCustomSockets need to implement get_default_value() + default_value = inp.get_default_value() + + if default_value is None: + return inp_name + if isinstance(default_value, str): + default_value = f'"{default_value}"' + + return f'new armory.logicnode.DynamicNode(this, {default_value})' + if inp.bl_idname == 'ArmNodeSocketAction' or inp.bl_idname == 'ArmNodeSocketArray': return inp_name if inp.bl_idname == 'ArmNodeSocketObject': From 740b84db41ddea23e19635cfb63f2182cddd630d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 10 Jun 2020 14:39:21 +0200 Subject: [PATCH 162/230] Cleanup build_default_node() --- blender/arm/make_logic.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index 0ac32173..e38f2c3f 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -243,39 +243,40 @@ def get_root_nodes(node_group): def build_default_node(inp: bpy.types.NodeSocket): """Creates a new node to give a not connected input socket a value""" - inp_name = 'new armory.logicnode.NullNode(this)' + null_node = 'new armory.logicnode.NullNode(this)' if isinstance(inp, arm.logicnode.arm_nodes.ArmCustomSocket): # ArmCustomSockets need to implement get_default_value() default_value = inp.get_default_value() - if default_value is None: - return inp_name + return null_node if isinstance(default_value, str): default_value = f'"{default_value}"' return f'new armory.logicnode.DynamicNode(this, {default_value})' if inp.bl_idname == 'ArmNodeSocketAction' or inp.bl_idname == 'ArmNodeSocketArray': - return inp_name + return null_node if inp.bl_idname == 'ArmNodeSocketObject': - inp_name = 'new armory.logicnode.ObjectNode(this, "' + str(inp.get_default_value()) + '")' - return inp_name + return f'new armory.logicnode.ObjectNode(this, "{inp.get_default_value()}")' if inp.bl_idname == 'ArmNodeSocketAnimAction': - inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.get_default_value()).replace("\"", "\\\"") + '")' - return inp_name + # Backslashes are not allowed in f-strings so we need this variable + default_value = inp.get_default_value().replace("\"", "\\\"") + return f'new armory.logicnode.StringNode(this, "{default_value}")' if inp.type == 'VECTOR': - inp_name = 'new armory.logicnode.VectorNode(this, ' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ', ' + str(inp.default_value[2]) + ')' + return f'new armory.logicnode.VectorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]})' elif inp.type == 'RGBA': - inp_name = 'new armory.logicnode.ColorNode(this, ' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ', ' + str(inp.default_value[2]) + ', ' + str(inp.default_value[3]) + ')' + return f'new armory.logicnode.ColorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]}, {inp.default_value[3]})' elif inp.type == 'RGB': - inp_name = 'new armory.logicnode.ColorNode(this, ' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ', ' + str(inp.default_value[2]) + ')' + return f'new armory.logicnode.ColorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]})' elif inp.type == 'VALUE': - inp_name = 'new armory.logicnode.FloatNode(this, ' + str(inp.default_value) + ')' + return f'new armory.logicnode.FloatNode(this, {inp.default_value})' elif inp.type == 'INT': - inp_name = 'new armory.logicnode.IntegerNode(this, ' + str(inp.default_value) + ')' + return f'new armory.logicnode.IntegerNode(this, {inp.default_value})' elif inp.type == 'BOOLEAN': - inp_name = 'new armory.logicnode.BooleanNode(this, ' + str(inp.default_value).lower() + ')' + return f'new armory.logicnode.BooleanNode(this, {inp.default_value.lower()})' elif inp.type == 'STRING': - inp_name = 'new armory.logicnode.StringNode(this, "' + str(inp.default_value).replace("\"", "\\\"") + '")' - return inp_name + default_value = inp.default_value.replace("\"", "\\\"") + return f'new armory.logicnode.StringNode(this, "{default_value}")' + + return null_node From 4f5d8a83d3a09e516ce14eb7ae99df66e94f76ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 11 Jun 2020 22:51:38 +0200 Subject: [PATCH 163/230] Fix boolean node sockets --- blender/arm/make_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py index e38f2c3f..a2392530 100755 --- a/blender/arm/make_logic.py +++ b/blender/arm/make_logic.py @@ -274,7 +274,7 @@ def build_default_node(inp: bpy.types.NodeSocket): elif inp.type == 'INT': return f'new armory.logicnode.IntegerNode(this, {inp.default_value})' elif inp.type == 'BOOLEAN': - return f'new armory.logicnode.BooleanNode(this, {inp.default_value.lower()})' + return f'new armory.logicnode.BooleanNode(this, {str(inp.default_value).lower()})' elif inp.type == 'STRING': default_value = inp.default_value.replace("\"", "\\\"") return f'new armory.logicnode.StringNode(this, "{default_value}")' From 5ff56e64645c3a3b0f74b3a513efdc564d99cc6f Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 14 Jun 2020 21:08:06 +0200 Subject: [PATCH 164/230] Filter haxe modules with invalid names --- blender/arm/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 78bf1472..2b4710f9 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -401,12 +401,12 @@ def fetch_script_names(): if os.path.isdir(sources_path): os.chdir(sources_path) # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python - for file in glob.glob('**/*.hx', recursive=True): - name = file.rsplit('.')[0] - # Replace the path syntax for package syntax so that it can be searchable in blender traits "Class" dropdown - wrd.arm_scripts_list.add().name = name.replace(os.sep, '.') - fetch_script_props(file) - + for file in glob.glob('**/[A-Z][A-Za-z0-9_]*.hx', recursive=True): + parts = file.rsplit('.') + if parts[1] == "hx": + # Replace the path syntax for package syntax so that it can be searchable in blender traits "Class" dropdown + wrd.arm_scripts_list.add().name = parts[0].replace(os.sep, '.') + fetch_script_props(file) # Canvas wrd.arm_canvas_list.clear() canvas_path = get_fp() + '/Bundled/canvas' From a7f9acd606326164e135823b8ad6ce8dc3244940 Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 14 Jun 2020 23:04:49 +0200 Subject: [PATCH 165/230] Use regexp to validate haxe module name --- blender/arm/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 2b4710f9..58a5808b 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -2,6 +2,7 @@ import glob import json import os import platform +import re import subprocess import webbrowser @@ -401,12 +402,13 @@ def fetch_script_names(): if os.path.isdir(sources_path): os.chdir(sources_path) # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python - for file in glob.glob('**/[A-Z][A-Za-z0-9_]*.hx', recursive=True): - parts = file.rsplit('.') - if parts[1] == "hx": - # Replace the path syntax for package syntax so that it can be searchable in blender traits "Class" dropdown - wrd.arm_scripts_list.add().name = parts[0].replace(os.sep, '.') + for file in glob.glob('**/*.hx', recursive=True): + mod = file.rsplit('.')[0] + mod_parts = mod.rsplit('/') + if re.match('^[A-Z][A-Za-z0-9_]*$',mod_parts[-1]): + wrd.arm_scripts_list.add().name = mod.replace(os.sep, '.') fetch_script_props(file) + # Canvas wrd.arm_canvas_list.clear() canvas_path = get_fp() + '/Bundled/canvas' From 04010333621b95699fcf5917691e0508f9f7bd09 Mon Sep 17 00:00:00 2001 From: tong Date: Wed, 17 Jun 2020 19:13:43 +0200 Subject: [PATCH 166/230] Fix ammo lib loading --- Sources/armory/system/Starter.hx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx index 31ef84e5..39b85c33 100644 --- a/Sources/armory/system/Starter.hx +++ b/Sources/armory/system/Starter.hx @@ -4,8 +4,6 @@ import kha.WindowOptions; class Starter { - static var tasks: Int; - #if arm_loadscreen public static var drawLoading: kha.graphics2.Graphics->Int->Int->Void = null; public static var numAssets: Int; @@ -13,6 +11,8 @@ class Starter { public static function main(scene: String, mode: Int, resize: Bool, min: Bool, max: Bool, w: Int, h: Int, msaa: Int, vsync: Bool, getRenderPath: Void->iron.RenderPath) { + var tasks : Int; + function start() { if (tasks > 0) return; @@ -76,20 +76,17 @@ class Starter { #if (js && arm_bullet) function loadLibAmmo(name: String) { kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { - var print = function(s:String) { trace(s); }; - var loaded = function() { tasks--; start(); }; - js.Syntax.code("(1, eval)({0})", b.toString()); + js.Syntax.code("(1,eval)({0})", b.toString()); #if kha_krom - var instantiateWasm = function(imports, successCallback) { - var wasmbin = Krom.loadBlob("ammo.wasm.wasm"); - var module = new js.lib.webassembly.Module(wasmbin); - var inst = new js.lib.webassembly.Instance(module, imports); + js.Syntax.code("Ammo({print:function(s){haxe.Log.trace(s);},instantiateWasm:function(imports,successCallback) { + var wasmbin = Krom.loadBlob('ammo.wasm.wasm'); + var module = new WebAssembly.Module(wasmbin); + var inst = new WebAssembly.Instance(module,imports); successCallback(inst); return inst.exports; - }; - js.Syntax.code("Ammo({print:print, instantiateWasm:instantiateWasm}).then(loaded)"); + }}).then(function(){ tasks--; start();})"); #else - js.Syntax.code("Ammo({print:print}).then(loaded)"); + js.Syntax.code("Ammo({print:function(s){haxe.Log.trace(s);}}).then(function(){ tasks--; start();})"); #end }); } From 85a934076452fe7ca872d0155bba939c2b486a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 18 Jun 2020 14:15:34 +0200 Subject: [PATCH 167/230] Add reroutes and frames to add node menu --- blender/arm/nodes_logic.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 791e9b47..082f283d 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -9,7 +9,7 @@ import webbrowser registered_nodes = [] class ArmLogicTree(NodeTree): - '''Logic nodes''' + """Logic nodes""" bl_idname = 'ArmLogicTreeType' bl_label = 'Logic Node Editor' bl_icon = 'DECORATE' @@ -33,11 +33,29 @@ def register_nodes(): node_categories = [] for category in sorted(arm_nodes.category_items): - sorted_items=sorted(arm_nodes.category_items[category], key=lambda item: item.nodetype) + if category == 'Layout': + # Handled separately + continue + + sorted_items = sorted(arm_nodes.category_items[category], key=lambda item: item.nodetype) node_categories.append( LogicNodeCategory('Logic' + category + 'Nodes', category, items=sorted_items) ) + # Add special layout nodes known from Blender's node editors + if 'Layout' in arm_nodes.category_items: + # Clone with [:] to prevent double entries + layout_items = arm_nodes.category_items['Layout'][:] + else: + layout_items = [] + + layout_items += [NodeItem('NodeReroute'), NodeItem('NodeFrame')] + layout_items = sorted(layout_items, key=lambda item: item.nodetype) + + node_categories.append( + LogicNodeCategory('LogicLayoutNodes', 'Layout', description='Layout Nodes', items=layout_items) + ) + nodeitems_utils.register_node_categories('ArmLogicNodes', node_categories) def unregister_nodes(): From 42393e34a85edf0d23a177b3a8080de8c880f5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 18 Jun 2020 15:36:34 +0200 Subject: [PATCH 168/230] Cleanup world export --- blender/arm/exporter.py | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 69360baf..9332402a 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1943,14 +1943,14 @@ class ArmoryExporter: o['actions'].append(ao) self.output['tilesheet_datas'].append(o) - def export_worlds(self): - worldRef = self.scene.world - if worldRef is not None: - o = {} - w = worldRef - o['name'] = w.name - self.post_export_world(w, o) - self.output['world_datas'].append(o) + def export_world(self): + """Exports the world of the scene.""" + world = self.scene.world + if world is not None: + out_world = {'name': world.name} + + self.post_export_world(world, out_world) + self.output['world_datas'].append(out_world) def export_objects(self, scene): """Exports all supported blender objects. @@ -2110,7 +2110,7 @@ class ArmoryExporter: self.export_materials() self.export_particle_systems() self.output['world_datas'] = [] - self.export_worlds() + self.export_world() self.export_tilesheets() if self.scene.world is not None: @@ -2791,27 +2791,29 @@ class ArmoryExporter: o['traits'].append(trait) @staticmethod - def post_export_world(world, o): + def post_export_world(world: bpy.types.World, out_world: Dict): wrd = bpy.data.worlds['Arm'] + bgcol = world.arm_envtex_color - if '_LDR' in wrd.world_defs: # No compositor used + # No compositor used + if '_LDR' in wrd.world_defs: for i in range(0, 3): bgcol[i] = pow(bgcol[i], 1.0 / 2.2) - o['background_color'] = arm.utils.color_to_int(bgcol) + out_world['background_color'] = arm.utils.color_to_int(bgcol) if '_EnvSky' in wrd.world_defs: # Sky data for probe - o['sun_direction'] = list(world.arm_envtex_sun_direction) - o['turbidity'] = world.arm_envtex_turbidity - o['ground_albedo'] = world.arm_envtex_ground_albedo + out_world['sun_direction'] = list(world.arm_envtex_sun_direction) + out_world['turbidity'] = world.arm_envtex_turbidity + out_world['ground_albedo'] = world.arm_envtex_ground_albedo disable_hdr = world.arm_envtex_name.endswith('.jpg') if '_EnvTex' in wrd.world_defs or '_EnvImg' in wrd.world_defs: o['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] if disable_hdr: - o['envmap'] += '.jpg' + out_world['envmap'] += '.jpg' else: - o['envmap'] += '.hdr' + out_world['envmap'] += '.hdr' # Main probe rpdat = arm.utils.get_rp() @@ -2833,17 +2835,16 @@ class ArmoryExporter: if mobile_mat: arm_radiance = False - po = {} - po['name'] = world.name + out_probe = {'name': world.name} if arm_irradiance: ext = '' if wrd.arm_minimize else '.json' - po['irradiance'] = irrsharmonics + '_irradiance' + ext + out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext if arm_radiance: - po['radiance'] = radtex + '_radiance' - po['radiance'] += '.jpg' if disable_hdr else '.hdr' - po['radiance_mipmaps'] = num_mips - po['strength'] = strength - o['probe'] = po + out_probe['radiance'] = radtex + '_radiance' + out_probe['radiance'] += '.jpg' if disable_hdr else '.hdr' + out_probe['radiance_mipmaps'] = num_mips + out_probe['strength'] = strength + out_world['probe'] = out_probe @staticmethod def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier) -> bool: From e691e8b5f2033eb10cc1c247c20d5cb0c684ea73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 18 Jun 2020 15:38:14 +0200 Subject: [PATCH 169/230] Cleanup make_world.py --- blender/arm/make_world.py | 57 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index f2646d4a..d14060f5 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -23,25 +23,26 @@ def build_node_tree(world): wrd = bpy.data.worlds['Arm'] wrd.world_defs = '' rpdat = arm.utils.get_rp() - - if callback != None: + + if callback is not None: callback() - + # Traverse world node tree - parsed = False - if world.node_tree != None: + is_parsed = False + if world.node_tree is not None: output_node = node_utils.get_node_by_type(world.node_tree, 'OUTPUT_WORLD') - if output_node != None: + if output_node is not None: parse_world_output(world, output_node) - parsed = True - if parsed == False: + is_parsed = True + + if not is_parsed: solid_mat = rpdat.arm_material_model == 'Solid' if rpdat.arm_irradiance and not solid_mat: wrd.world_defs += '_Irr' c = world.color world.arm_envtex_color = [c[0], c[1], c[2], 1.0] world.arm_envtex_strength = 1.0 - + # Clear to color if no texture or sky is provided if '_EnvSky' not in wrd.world_defs and '_EnvTex' not in wrd.world_defs: if '_EnvImg' not in wrd.world_defs: @@ -52,7 +53,7 @@ def build_node_tree(world): write_probes.write_color_irradiance(wname, world.arm_envtex_color) # film_transparent - if bpy.context.scene != None and hasattr(bpy.context.scene.render, 'film_transparent') and bpy.context.scene.render.film_transparent: + if bpy.context.scene is not None and hasattr(bpy.context.scene.render, 'film_transparent') and bpy.context.scene.render.film_transparent: wrd.world_defs += '_EnvTransp' wrd.world_defs += '_EnvCol' @@ -67,15 +68,15 @@ def parse_world_output(world, node): if node.inputs[0].is_linked: surface_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) parse_surface(world, surface_node) - + def parse_surface(world, node): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' - + # Extract environment strength if node.type == 'BACKGROUND': - + # Append irradiance define if rpdat.arm_irradiance and not solid_mat: wrd.world_defs += '_Irr' @@ -88,18 +89,18 @@ def parse_surface(world, node): color_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) parse_color(world, color_node) -def parse_color(world, node): +def parse_color(world, node): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' # Env map included - if node.type == 'TEX_ENVIRONMENT' and node.image != None: + if node.type == 'TEX_ENVIRONMENT' and node.image is not None: image = node.image filepath = image.filepath - - if image.packed_file == None and not os.path.isfile(arm.utils.asset_path(filepath)): + + if image.packed_file is None and not os.path.isfile(arm.utils.asset_path(filepath)): log.warn(world.name + ' - unable to open ' + image.filepath) return @@ -121,7 +122,7 @@ def parse_color(world, node): tex_file = base[0] + '.jpg' target_format = 'JPEG' - if image.packed_file != None: + if image.packed_file is not None: # Extract packed data unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): @@ -133,10 +134,10 @@ def parse_color(world, node): if not os.path.isfile(unpack_filepath): arm.utils.unpack_image(image, unpack_filepath, file_format=target_format) - elif os.path.isfile(unpack_filepath) == False or os.path.getsize(unpack_filepath) != image.packed_file.size: + elif not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(image.packed_file.data) - + assets.add(unpack_filepath) else: if do_convert: @@ -157,12 +158,12 @@ def parse_color(world, node): world.arm_envtex_name = tex_file world.arm_envtex_irr_name = tex_file.rsplit('.', 1)[0] disable_hdr = target_format == 'JPEG' - + mip_count = world.arm_envtex_num_mips mip_count = write_probes.write_probes(filepath, disable_hdr, mip_count, arm_radiance=rpdat.arm_radiance) - + world.arm_envtex_num_mips = mip_count - + # Append envtex define wrd.world_defs += '_EnvTex' # Append LDR define @@ -173,7 +174,7 @@ def parse_color(world, node): wrd.world_defs += '_Rad' # Static image background - elif node.type == 'TEX_IMAGE': + elif node.type == 'TEX_IMAGE': image = node.image filepath = image.filepath @@ -200,14 +201,14 @@ def parse_color(world, node): elif node.type == 'TEX_SKY': # Match to cycles world.arm_envtex_strength *= 0.1 - wrd.world_defs += '_EnvSky' + assets.add_khafile_def('arm_hosek') - + world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] world.arm_envtex_turbidity = node.turbidity world.arm_envtex_ground_albedo = node.ground_albedo - + # Irradiance json file name wname = arm.utils.safestr(world.name) world.arm_envtex_irr_name = wname @@ -222,6 +223,6 @@ def parse_color(world, node): assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance.hdr') for i in range(0, 8): assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance_' + str(i) + '.hdr') - + world.arm_envtex_name = 'hosek' world.arm_envtex_num_mips = 8 From 1152b99e1087e9a15921f13b50e3f719ffb6c8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 22 Jun 2020 21:56:21 +0200 Subject: [PATCH 170/230] Fix non-material shader export by swapping evaluation order --- blender/arm/material/make_shader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/make_shader.py b/blender/arm/material/make_shader.py index 24ea7755..91adbf53 100644 --- a/blender/arm/material/make_shader.py +++ b/blender/arm/material/make_shader.py @@ -130,7 +130,7 @@ def write_shader(rel_path, shader, ext, rpass, matname, keep_cache=True): return # TODO: blend context - if mat_state.material.arm_blending and rpass == 'mesh': + if rpass == 'mesh' and mat_state.material.arm_blending: rpass = 'blend' file_ext = '.glsl' From 569d139e4ec4a720b27bdc76d9f047fa59fdf6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 22 Jun 2020 22:03:02 +0200 Subject: [PATCH 171/230] Replace some wrd defs by individual word defs --- blender/arm/exporter.py | 14 ++++--- blender/arm/make_world.py | 39 +++++++++--------- blender/arm/props.py | 24 +++++++++++ blender/arm/props_ui.py | 86 ++++++++++++++++++++++++--------------- 4 files changed, 106 insertions(+), 57 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 9332402a..fd196814 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2796,20 +2796,21 @@ class ArmoryExporter: bgcol = world.arm_envtex_color # No compositor used - if '_LDR' in wrd.world_defs: + if '_LDR' in world.world_defs: for i in range(0, 3): bgcol[i] = pow(bgcol[i], 1.0 / 2.2) out_world['background_color'] = arm.utils.color_to_int(bgcol) - if '_EnvSky' in wrd.world_defs: + if '_EnvSky' in world.world_defs: # Sky data for probe out_world['sun_direction'] = list(world.arm_envtex_sun_direction) out_world['turbidity'] = world.arm_envtex_turbidity out_world['ground_albedo'] = world.arm_envtex_ground_albedo disable_hdr = world.arm_envtex_name.endswith('.jpg') - if '_EnvTex' in wrd.world_defs or '_EnvImg' in wrd.world_defs: - o['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] + print('_EnvTex' in world.world_defs) + if '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs: + out_world['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] if disable_hdr: out_world['envmap'] += '.jpg' else: @@ -2822,10 +2823,11 @@ class ArmoryExporter: arm_radiance = False radtex = world.arm_envtex_name.rsplit('.', 1)[0] irrsharmonics = world.arm_envtex_irr_name + # Radiance - if '_EnvTex' in wrd.world_defs: + if '_EnvTex' in world.world_defs: arm_radiance = rpdat.arm_radiance - elif '_EnvSky' in wrd.world_defs: + elif '_EnvSky' in world.world_defs: arm_radiance = rpdat.arm_radiance radtex = 'hosek' num_mips = world.arm_envtex_num_mips diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index d14060f5..055fe874 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -18,10 +18,11 @@ def build(): worlds.append(scene.world) build_node_tree(scene.world) -def build_node_tree(world): - wname = arm.utils.safestr(world.name) - wrd = bpy.data.worlds['Arm'] - wrd.world_defs = '' + +def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): + """Generates the shader code for the given world.""" + world_name = arm.utils.safestr(world.name) + world.world_defs = '' rpdat = arm.utils.get_rp() if callback is not None: @@ -38,31 +39,31 @@ def build_node_tree(world): if not is_parsed: solid_mat = rpdat.arm_material_model == 'Solid' if rpdat.arm_irradiance and not solid_mat: - wrd.world_defs += '_Irr' + world.world_defs += '_Irr' c = world.color world.arm_envtex_color = [c[0], c[1], c[2], 1.0] world.arm_envtex_strength = 1.0 # Clear to color if no texture or sky is provided - if '_EnvSky' not in wrd.world_defs and '_EnvTex' not in wrd.world_defs: - if '_EnvImg' not in wrd.world_defs: - wrd.world_defs += '_EnvCol' + if '_EnvSky' not in world.world_defs and '_EnvTex' not in world.world_defs: + if '_EnvImg' not in world.world_defs: + world.world_defs += '_EnvCol' # Irradiance json file name - world.arm_envtex_name = wname - world.arm_envtex_irr_name = wname - write_probes.write_color_irradiance(wname, world.arm_envtex_color) + world.arm_envtex_name = world_name + world.arm_envtex_irr_name = world_name + write_probes.write_color_irradiance(world_name, world.arm_envtex_color) # film_transparent if bpy.context.scene is not None and hasattr(bpy.context.scene.render, 'film_transparent') and bpy.context.scene.render.film_transparent: - wrd.world_defs += '_EnvTransp' - wrd.world_defs += '_EnvCol' + world.world_defs += '_EnvTransp' + world.world_defs += '_EnvCol' # Clouds enabled if rpdat.arm_clouds: - wrd.world_defs += '_EnvClouds' + world.world_defs += '_EnvClouds' - if '_EnvSky' in wrd.world_defs or '_EnvTex' in wrd.world_defs or '_EnvImg' in wrd.world_defs or '_EnvClouds' in wrd.world_defs: - wrd.world_defs += '_EnvStr' + if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: + world.world_defs += '_EnvStr' def parse_world_output(world, node): if node.inputs[0].is_linked: @@ -165,10 +166,10 @@ def parse_color(world, node): world.arm_envtex_num_mips = mip_count # Append envtex define - wrd.world_defs += '_EnvTex' + world.world_defs += '_EnvTex' # Append LDR define if disable_hdr: - wrd.world_defs += '_EnvLDR' + world.world_defs += '_EnvLDR' # Append radiance define if rpdat.arm_irradiance and rpdat.arm_radiance and not mobile_mat: wrd.world_defs += '_Rad' @@ -201,8 +202,8 @@ def parse_color(world, node): elif node.type == 'TEX_SKY': # Match to cycles world.arm_envtex_strength *= 0.1 - wrd.world_defs += '_EnvSky' + world.world_defs += '_EnvSky' assets.add_khafile_def('arm_hosek') world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] diff --git a/blender/arm/props.py b/blender/arm/props.py index b9854531..e0a5b598 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -269,6 +269,30 @@ def init_properties(): bpy.types.World.arm_wasm_list = CollectionProperty(type=bpy.types.PropertyGroup) bpy.types.World.world_defs = StringProperty(name="World Shader Defs", default='') bpy.types.World.compo_defs = StringProperty(name="Compositor Shader Defs", default='') + + bpy.types.World.arm_use_fog = BoolProperty(name="Volumetric Fog", default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_fog_color = FloatVectorProperty(name="Color", size=3, subtype='COLOR', default=[0.5, 0.6, 0.7], min=0, max=1, update=assets.invalidate_shader_cache) + bpy.types.World.arm_fog_amounta = FloatProperty(name="Amount A", default=0.25, update=assets.invalidate_shader_cache) + bpy.types.World.arm_fog_amountb = FloatProperty(name="Amount B", default=0.5, update=assets.invalidate_shader_cache) + + bpy.types.World.arm_use_clouds = BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_lower = FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_upper = FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_wind = FloatVectorProperty(name="Wind", default=[1.0, 0.0], size=2, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_secondary = FloatProperty(name="Secondary", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_precipitation = FloatProperty(name="Precipitation", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_steps = IntProperty(name="Steps", default=24, min=1, max=240, update=assets.invalidate_shader_cache) + + bpy.types.World.arm_use_water = BoolProperty(name="Water", description="Water surface pass", default=False, update=props_renderpath.update_renderpath) + bpy.types.World.arm_water_color = FloatVectorProperty(name="Color", size=3, default=[1, 1, 1], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_level = FloatProperty(name="Level", default=0.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_displace = FloatProperty(name="Displace", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_speed = FloatProperty(name="Speed", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_freq = FloatProperty(name="Freq", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_density = FloatProperty(name="Density", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_refract = FloatProperty(name="Refract", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_water_reflect = FloatProperty(name="Reflect", default=1.0, update=assets.invalidate_shader_cache) + bpy.types.Material.export_uvs = BoolProperty(name="Export UVs", default=False) bpy.types.Material.export_vcols = BoolProperty(name="Export VCols", default=False) bpy.types.Material.export_tangents = BoolProperty(name="Export Tangents", default=False) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 4bbe9b45..7cbdccfb 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1,16 +1,16 @@ -import bpy -import webbrowser import os -from bpy.types import Menu, Panel, UIList -from bpy.props import * -import arm.utils -import arm.make as make -import arm.make_state as state + +import bpy + +import arm.api import arm.assets as assets import arm.log as log -import arm.proxy -import arm.api +import arm.make as make +import arm.make_state as state +import arm.props as props import arm.props_properties +import arm.proxy +import arm.utils # Menu in object region class ARM_PT_ObjectPropsPanel(bpy.types.Panel): @@ -145,6 +145,49 @@ class ARM_PT_DataPropsPanel(bpy.types.Panel): layout.prop(obj.data, 'arm_autobake') pass +class ARM_PT_WorldPropsPanel(bpy.types.Panel): + bl_label = "Armory World Properties" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + world = context.world + if world is None: + return + + layout.prop(world, 'arm_use_fog') + col = layout.column(align=True) + col.enabled = world.arm_use_fog + col.prop(world, 'arm_fog_color') + col.prop(world, 'arm_fog_amounta') + col.prop(world, 'arm_fog_amountb') + + layout.prop(world, 'arm_use_clouds') + col = layout.column(align=True) + col.enabled = world.arm_use_clouds + col.prop(world, 'arm_clouds_lower') + col.prop(world, 'arm_clouds_upper') + col.prop(world, 'arm_clouds_precipitation') + col.prop(world, 'arm_clouds_secondary') + col.prop(world, 'arm_clouds_wind') + col.prop(world, 'arm_clouds_steps') + + layout.prop(world, "arm_use_water") + col = layout.column(align=True) + col.enabled = world.arm_use_water + col.prop(world, 'arm_water_level') + col.prop(world, 'arm_water_density') + col.prop(world, 'arm_water_displace') + col.prop(world, 'arm_water_speed') + col.prop(world, 'arm_water_freq') + col.prop(world, 'arm_water_refract') + col.prop(world, 'arm_water_reflect') + col.prop(world, 'arm_water_color') + class ARM_PT_ScenePropsPanel(bpy.types.Panel): bl_label = "Armory Props" bl_space_type = "PROPERTIES" @@ -826,25 +869,7 @@ class ARM_PT_RenderPathWorldPanel(bpy.types.Panel): colb.enabled = rpdat.arm_radiance colb.prop(rpdat, 'arm_radiance_size') layout.prop(rpdat, 'arm_clouds') - col = layout.column() - col.enabled = rpdat.arm_clouds - col.prop(rpdat, 'arm_clouds_lower') - col.prop(rpdat, 'arm_clouds_upper') - col.prop(rpdat, 'arm_clouds_precipitation') - col.prop(rpdat, 'arm_clouds_secondary') - col.prop(rpdat, 'arm_clouds_wind') - col.prop(rpdat, 'arm_clouds_steps') layout.prop(rpdat, "rp_water") - col = layout.column() - col.enabled = rpdat.rp_water - col.prop(rpdat, 'arm_water_level') - col.prop(rpdat, 'arm_water_density') - col.prop(rpdat, 'arm_water_displace') - col.prop(rpdat, 'arm_water_speed') - col.prop(rpdat, 'arm_water_freq') - col.prop(rpdat, 'arm_water_refract') - col.prop(rpdat, 'arm_water_reflect') - col.prop(rpdat, 'arm_water_color') class ARM_PT_RenderPathPostProcessPanel(bpy.types.Panel): bl_label = "Post Process" @@ -978,11 +1003,6 @@ class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): col.enabled = rpdat.arm_grain col.prop(rpdat, 'arm_grain_strength') layout.prop(rpdat, 'arm_fog') - col = layout.column() - col.enabled = rpdat.arm_fog - col.prop(rpdat, 'arm_fog_color') - col.prop(rpdat, 'arm_fog_amounta') - col.prop(rpdat, 'arm_fog_amountb') layout.separator() layout.prop(rpdat, "rp_autoexposure") col = layout.column() @@ -1433,6 +1453,7 @@ def register(): bpy.utils.register_class(ARM_PT_PhysicsPropsPanel) bpy.utils.register_class(ARM_PT_DataPropsPanel) bpy.utils.register_class(ARM_PT_ScenePropsPanel) + bpy.utils.register_class(ARM_PT_WorldPropsPanel) bpy.utils.register_class(InvalidateCacheButton) bpy.utils.register_class(InvalidateMaterialCacheButton) bpy.utils.register_class(ARM_PT_MaterialPropsPanel) @@ -1482,6 +1503,7 @@ def unregister(): bpy.utils.unregister_class(ARM_PT_ParticlesPropsPanel) bpy.utils.unregister_class(ARM_PT_PhysicsPropsPanel) bpy.utils.unregister_class(ARM_PT_DataPropsPanel) + bpy.utils.unregister_class(ARM_PT_WorldPropsPanel) bpy.utils.unregister_class(ARM_PT_ScenePropsPanel) bpy.utils.unregister_class(InvalidateCacheButton) bpy.utils.unregister_class(InvalidateMaterialCacheButton) From 729c2ddc136010134a9be346322e2652f14990ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 22 Jun 2020 22:37:21 +0200 Subject: [PATCH 172/230] Begin with world shader generation --- blender/arm/exporter.py | 2 +- blender/arm/make_world.py | 52 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index fd196814..2349f004 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1947,7 +1947,7 @@ class ArmoryExporter: """Exports the world of the scene.""" world = self.scene.world if world is not None: - out_world = {'name': world.name} + out_world = {'name': arm.utils.safestr(world.name)} self.post_export_world(world, out_world) self.output['world_datas'].append(out_world) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 055fe874..7cbf2af2 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -8,15 +8,63 @@ import arm.utils import arm.node_utils as node_utils import arm.log as log import arm.make_state as state +from arm.material import make_shader +from arm.material.shader import ShaderContext, Shader callback = None def build(): worlds = [] for scene in bpy.data.scenes: - if scene.arm_export and scene.world != None and scene.world not in worlds: + if scene.arm_export and scene.world is not None and scene.world not in worlds: worlds.append(scene.world) - build_node_tree(scene.world) + create_world_shaders(scene.world) + + +def create_world_shaders(world: bpy.types.World): + """Creates fragment and vertex shaders for the given world.""" + world_name = arm.utils.safestr(world.name) + + shader_data = {'name': world_name + '_data', 'contexts': []} + shader_props = { + 'name': 'world_' + world_name, + 'depth_write': False, + 'compare_mode': 'less', + 'cull_mode': 'clockwise', + 'color_attachments': ['_HDR'], + 'vertex_elements': [{'name': 'pos', 'data': 'float3'}, {'name': 'nor', 'data': 'float3'}] + } + + # ShaderContext expects a material, but using a world also works + shader_context = ShaderContext(world, shader_data, shader_props) + vert = shader_context.make_vert() + frag = shader_context.make_frag() + + vert.add_out('vec3 normal') + vert.add_uniform('mat4 SMVP') + + frag.add_include('compiled.inc') + frag.add_in('vec3 normal') + frag.add_out('vec4 fragColor') + + vert.write('''normal = nor; + vec4 position = SMVP * vec4(pos, 1.0); + gl_Position = vec4(position);''') + + build_node_tree(world, frag, vert) + + frag.write('fragColor = vec4(0.0);') + + # TODO: Rework shader export so that it doesn't depend on materials + # to prevent workaround code like this + rel_path = os.path.join(arm.utils.build_dir(), 'compiled', 'Shaders') + full_path = os.path.join(arm.utils.get_fp(), rel_path) + if not os.path.exists(full_path): + os.makedirs(full_path) + + # Output: world_[world_name].[frag/vert].glsl + make_shader.write_shader(rel_path, shader_context.vert, 'vert', world_name, 'world') + make_shader.write_shader(rel_path, shader_context.frag, 'frag', world_name, 'world') def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): From 1a12ee280a78fe43e4e700ba6687e36ee4ab3c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 24 Jun 2020 00:18:32 +0200 Subject: [PATCH 173/230] Export world shader data file --- blender/arm/exporter.py | 14 +++++++++++--- blender/arm/make_world.py | 16 ++++++++++++---- blender/arm/material/make_shader.py | 4 ++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 2349f004..29d5afbe 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -30,6 +30,7 @@ import arm.material.cycles as cycles import arm.material.make as make_material import arm.material.mat_batch as mat_batch import arm.utils +from arm import make_world @unique @@ -124,8 +125,10 @@ class ArmoryExporter: self.camera_array = {} self.speaker_array = {} self.material_array = [] + self.world_array = [] self.particle_system_array = {} + # `True` if there is at least one spawned camera in the scene self.camera_spawned = False @@ -1947,10 +1950,15 @@ class ArmoryExporter: """Exports the world of the scene.""" world = self.scene.world if world is not None: - out_world = {'name': arm.utils.safestr(world.name)} + world_name = arm.utils.safestr(world.name) - self.post_export_world(world, out_world) - self.output['world_datas'].append(out_world) + if world_name not in self.world_array: + self.world_array.append(world_name) + out_world = {'name': world_name} + + make_world.create_world_shaders(world, self.output['material_datas']) + self.post_export_world(world, out_world) + self.output['world_datas'].append(out_world) def export_objects(self, scene): """Exports all supported blender objects. diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 7cbf2af2..60db6a32 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -2,13 +2,15 @@ import bpy import os from bpy.types import NodeTree, Node, NodeSocket from bpy.props import * +from typing import List + import arm.write_probes as write_probes import arm.assets as assets import arm.utils import arm.node_utils as node_utils import arm.log as log import arm.make_state as state -from arm.material import make_shader +from arm.material import make_shader, mat_state from arm.material.shader import ShaderContext, Shader callback = None @@ -18,14 +20,13 @@ def build(): for scene in bpy.data.scenes: if scene.arm_export and scene.world is not None and scene.world not in worlds: worlds.append(scene.world) - create_world_shaders(scene.world) + # create_world_shaders(scene.world) -def create_world_shaders(world: bpy.types.World): +def create_world_shaders(world: bpy.types.World, out_shader_datas: List): """Creates fragment and vertex shaders for the given world.""" world_name = arm.utils.safestr(world.name) - shader_data = {'name': world_name + '_data', 'contexts': []} shader_props = { 'name': 'world_' + world_name, 'depth_write': False, @@ -34,6 +35,7 @@ def create_world_shaders(world: bpy.types.World): 'color_attachments': ['_HDR'], 'vertex_elements': [{'name': 'pos', 'data': 'float3'}, {'name': 'nor', 'data': 'float3'}] } + shader_data = {'name': world_name + '_data', 'contexts': [shader_props]} # ShaderContext expects a material, but using a world also works shader_context = ShaderContext(world, shader_data, shader_props) @@ -66,6 +68,12 @@ def create_world_shaders(world: bpy.types.World): make_shader.write_shader(rel_path, shader_context.vert, 'vert', world_name, 'world') make_shader.write_shader(rel_path, shader_context.frag, 'frag', world_name, 'world') + # Write shader data file + shader_data_file = 'world_' + world_name + '_data.arm' + arm.utils.write_arm(os.path.join(full_path, shader_data_file), {'shader_datas': shader_context.data}) + shader_data_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Shaders', shader_data_file) + assets.add_shader_data(shader_data_path) + def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): """Generates the shader code for the given world.""" diff --git a/blender/arm/material/make_shader.py b/blender/arm/material/make_shader.py index 91adbf53..b27f246b 100644 --- a/blender/arm/material/make_shader.py +++ b/blender/arm/material/make_shader.py @@ -55,7 +55,7 @@ def build(material, mat_users, mat_armusers): global_elems.append({'name': 'irot', 'data': 'float3'}) if bo.arm_instanced == 'Loc + Scale' or bo.arm_instanced == 'Loc + Rot + Scale': global_elems.append({'name': 'iscl', 'data': 'float3'}) - + mat_state.data.global_elems = global_elems bind_constants = dict() @@ -77,7 +77,7 @@ def build(material, mat_users, mat_armusers): if con != None: pass - + elif rp == 'mesh': con = make_mesh.make(rp, rpasses) From a91cf665e7f4ff8b01942fe4172148b29a815c50 Mon Sep 17 00:00:00 2001 From: tong Date: Thu, 25 Jun 2020 12:14:57 +0200 Subject: [PATCH 174/230] Add missing import --- blender/arm/nodes_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 082f283d..7d1a2479 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -2,7 +2,7 @@ import bpy from bpy.types import NodeTree from bpy.props import * import nodeitems_utils -from nodeitems_utils import NodeCategory +from nodeitems_utils import NodeCategory, NodeItem from arm.logicnode import * import webbrowser From c89a40da854b9e1fc3e8390f3d6436bf5d97d1ab Mon Sep 17 00:00:00 2001 From: tong Date: Thu, 25 Jun 2020 13:02:56 +0200 Subject: [PATCH 175/230] Add haxe --times flag on verbose not debug_console --- blender/arm/write_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index da5e6da5..ea774613 100755 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -225,6 +225,8 @@ project.addSources('Sources'); if wrd.arm_debug_console: assets.add_khafile_def('arm_debug') f.write(add_shaders(sdk_path + "/armory/Shaders/debug_draw/**", rel_path=rel_path)) + + if wrd.arm_verbose_output: f.write("project.addParameter('--times');\n") if export_ui: From d2a20234800526a9215b9ffe5a28ea2d5c074070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 26 Jun 2020 22:27:46 +0200 Subject: [PATCH 176/230] Make world shader data export working --- blender/arm/make.py | 9 +++++++-- blender/arm/make_world.py | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index e77953fb..f80ab6aa 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -169,8 +169,8 @@ def export_data(fp, sdk_path): # Write referenced shader passes if not os.path.isfile(build_dir + '/compiled/Shaders/shader_datas.arm') or state.last_world_defs != wrd.world_defs: - res = {} - res['shader_datas'] = [] + res = {'shader_datas': []} + for ref in assets.shader_passes: # Ensure shader pass source exists if not os.path.exists(raw_shaders_path + '/' + ref): @@ -180,7 +180,12 @@ def export_data(fp, sdk_path): compile_shader_pass(res, raw_shaders_path, ref, defs + cdefs, make_variants=has_config) else: compile_shader_pass(res, raw_shaders_path, ref, defs, make_variants=has_config) + + # Workaround to also export non-material world shaders + res['shader_datas'] += make_world.shader_datas + arm.utils.write_arm(shaders_path + '/shader_datas.arm', res) + for ref in assets.shader_passes: for s in assets.shader_passes_assets[ref]: assets.add_shader(shaders_path + '/' + s + '.glsl') diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 60db6a32..68f7ce29 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -14,6 +14,7 @@ from arm.material import make_shader, mat_state from arm.material.shader import ShaderContext, Shader callback = None +shader_datas = [] def build(): worlds = [] @@ -25,10 +26,12 @@ def build(): def create_world_shaders(world: bpy.types.World, out_shader_datas: List): """Creates fragment and vertex shaders for the given world.""" + global shader_datas world_name = arm.utils.safestr(world.name) + pass_name = 'World_' + world_name shader_props = { - 'name': 'world_' + world_name, + 'name': world_name, 'depth_write': False, 'compare_mode': 'less', 'cull_mode': 'clockwise', @@ -64,16 +67,20 @@ def create_world_shaders(world: bpy.types.World, out_shader_datas: List): if not os.path.exists(full_path): os.makedirs(full_path) - # Output: world_[world_name].[frag/vert].glsl - make_shader.write_shader(rel_path, shader_context.vert, 'vert', world_name, 'world') - make_shader.write_shader(rel_path, shader_context.frag, 'frag', world_name, 'world') + # Output: World_[world_name].[frag/vert].glsl + make_shader.write_shader(rel_path, shader_context.vert, 'vert', world_name, 'World') + make_shader.write_shader(rel_path, shader_context.frag, 'frag', world_name, 'World') # Write shader data file - shader_data_file = 'world_' + world_name + '_data.arm' + shader_data_file = pass_name + '_data.arm' arm.utils.write_arm(os.path.join(full_path, shader_data_file), {'shader_datas': shader_context.data}) shader_data_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Shaders', shader_data_file) assets.add_shader_data(shader_data_path) + assets.add_shader_pass(pass_name) + assets.shader_passes_assets[pass_name] = shader_context.data + shader_datas.append({'contexts': [shader_context.data], 'name': pass_name}) + def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): """Generates the shader code for the given world.""" From 11a3358879d54494ec538bcfeb737977071ddc9a Mon Sep 17 00:00:00 2001 From: tong Date: Sat, 27 Jun 2020 15:59:53 +0200 Subject: [PATCH 177/230] Replace if conditions with switch --- Sources/armory/renderpath/Postprocess.hx | 123 +++++++---------------- 1 file changed, 37 insertions(+), 86 deletions(-) diff --git a/Sources/armory/renderpath/Postprocess.hx b/Sources/armory/renderpath/Postprocess.hx index c62f5105..b063ca99 100644 --- a/Sources/armory/renderpath/Postprocess.hx +++ b/Sources/armory/renderpath/Postprocess.hx @@ -1,9 +1,8 @@ package armory.renderpath; -import iron.Scene; -import iron.object.Object; import iron.data.MaterialData; import iron.math.Vec4; +import iron.object.Object; class Postprocess { @@ -100,253 +99,208 @@ class Postprocess { public static function vec3Link(object:Object, mat:MaterialData, link:String):iron.math.Vec4 { var v:Vec4 = null; - if (link == "_globalWeight") { + switch link { + case "_globalWeight": var ppm_index = 0; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalTint") { + case "_globalTint": var ppm_index = 1; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalSaturation") { + case "_globalSaturation": var ppm_index = 2; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalContrast") { + case "_globalContrast": var ppm_index = 3; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalGamma") { + case "_globalGamma": var ppm_index = 4; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalGain") { + case "_globalGain": var ppm_index = 5; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } - if (link == "_globalOffset") { + case "_globalOffset": var ppm_index = 6; v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[ppm_index][0]; v.y = colorgrading_global_uniforms[ppm_index][1]; v.z = colorgrading_global_uniforms[ppm_index][2]; - } //Shadow ppm - if (link == "_shadowSaturation") { + case "_shadowSaturation": var ppm_index = 0; v = iron.object.Uniforms.helpVec; v.x = colorgrading_shadow_uniforms[ppm_index][0]; v.y = colorgrading_shadow_uniforms[ppm_index][1]; v.z = colorgrading_shadow_uniforms[ppm_index][2]; - } - if (link == "_shadowContrast") { + case "_shadowContrast": var ppm_index = 1; v = iron.object.Uniforms.helpVec; v.x = colorgrading_shadow_uniforms[ppm_index][0]; v.y = colorgrading_shadow_uniforms[ppm_index][1]; v.z = colorgrading_shadow_uniforms[ppm_index][2]; - } - if (link == "_shadowGamma") { + case "_shadowGamma": var ppm_index = 2; v = iron.object.Uniforms.helpVec; v.x = colorgrading_shadow_uniforms[ppm_index][0]; v.y = colorgrading_shadow_uniforms[ppm_index][1]; v.z = colorgrading_shadow_uniforms[ppm_index][2]; - } - if (link == "_shadowGain") { + case "_shadowGain": var ppm_index = 3; v = iron.object.Uniforms.helpVec; v.x = colorgrading_shadow_uniforms[ppm_index][0]; v.y = colorgrading_shadow_uniforms[ppm_index][1]; v.z = colorgrading_shadow_uniforms[ppm_index][2]; - } - if (link == "_shadowOffset") { + case "_shadowOffset": var ppm_index = 4; v = iron.object.Uniforms.helpVec; v.x = colorgrading_shadow_uniforms[ppm_index][0]; v.y = colorgrading_shadow_uniforms[ppm_index][1]; v.z = colorgrading_shadow_uniforms[ppm_index][2]; - } //Midtone ppm - if (link == "_midtoneSaturation") { + case "_midtoneSaturation": var ppm_index = 0; v = iron.object.Uniforms.helpVec; v.x = colorgrading_midtone_uniforms[ppm_index][0]; v.y = colorgrading_midtone_uniforms[ppm_index][1]; v.z = colorgrading_midtone_uniforms[ppm_index][2]; - } - if (link == "_midtoneContrast") { + case "_midtoneContrast": var ppm_index = 1; v = iron.object.Uniforms.helpVec; v.x = colorgrading_midtone_uniforms[ppm_index][0]; v.y = colorgrading_midtone_uniforms[ppm_index][1]; v.z = colorgrading_midtone_uniforms[ppm_index][2]; - } - if (link == "_midtoneGamma") { + case "_midtoneGamma": var ppm_index = 2; v = iron.object.Uniforms.helpVec; v.x = colorgrading_midtone_uniforms[ppm_index][0]; v.y = colorgrading_midtone_uniforms[ppm_index][1]; v.z = colorgrading_midtone_uniforms[ppm_index][2]; - } - if (link == "_midtoneGain") { + case "_midtoneGain": var ppm_index = 3; v = iron.object.Uniforms.helpVec; v.x = colorgrading_midtone_uniforms[ppm_index][0]; v.y = colorgrading_midtone_uniforms[ppm_index][1]; v.z = colorgrading_midtone_uniforms[ppm_index][2]; - } - if (link == "_midtoneOffset") { + case "_midtoneOffset": var ppm_index = 4; v = iron.object.Uniforms.helpVec; v.x = colorgrading_midtone_uniforms[ppm_index][0]; v.y = colorgrading_midtone_uniforms[ppm_index][1]; v.z = colorgrading_midtone_uniforms[ppm_index][2]; - } //Highlight ppm - if (link == "_highlightSaturation") { + case "_highlightSaturation": var ppm_index = 0; v = iron.object.Uniforms.helpVec; v.x = colorgrading_highlight_uniforms[ppm_index][0]; v.y = colorgrading_highlight_uniforms[ppm_index][1]; v.z = colorgrading_highlight_uniforms[ppm_index][2]; - } - if (link == "_highlightContrast") { + case "_highlightContrast": var ppm_index = 1; v = iron.object.Uniforms.helpVec; v.x = colorgrading_highlight_uniforms[ppm_index][0]; v.y = colorgrading_highlight_uniforms[ppm_index][1]; v.z = colorgrading_highlight_uniforms[ppm_index][2]; - } - if (link == "_highlightGamma") { + case "_highlightGamma": var ppm_index = 2; v = iron.object.Uniforms.helpVec; v.x = colorgrading_highlight_uniforms[ppm_index][0]; v.y = colorgrading_highlight_uniforms[ppm_index][1]; v.z = colorgrading_highlight_uniforms[ppm_index][2]; - } - if (link == "_highlightGain") { + case "_highlightGain": var ppm_index = 3; v = iron.object.Uniforms.helpVec; v.x = colorgrading_highlight_uniforms[ppm_index][0]; v.y = colorgrading_highlight_uniforms[ppm_index][1]; v.z = colorgrading_highlight_uniforms[ppm_index][2]; - } - if (link == "_highlightOffset") { + case "_highlightOffset": var ppm_index = 4; v = iron.object.Uniforms.helpVec; v.x = colorgrading_highlight_uniforms[ppm_index][0]; v.y = colorgrading_highlight_uniforms[ppm_index][1]; v.z = colorgrading_highlight_uniforms[ppm_index][2]; - } //Postprocess Components - if (link == "_PPComp1") { + case "_PPComp1": v = iron.object.Uniforms.helpVec; v.x = camera_uniforms[0]; //F-Number v.y = camera_uniforms[1]; //Shutter v.z = camera_uniforms[2]; //ISO - } - - if (link == "_PPComp2") { + case "_PPComp2": v = iron.object.Uniforms.helpVec; v.x = camera_uniforms[3]; //EC v.y = camera_uniforms[4]; //Lens Distortion v.z = camera_uniforms[5]; //DOF Autofocus - } - - if (link == "_PPComp3") { + case "_PPComp3": v = iron.object.Uniforms.helpVec; v.x = camera_uniforms[6]; //Distance v.y = camera_uniforms[7]; //Focal Length v.z = camera_uniforms[8]; //F-Stop - } - - if (link == "_PPComp4") { + case "_PPComp4": v = iron.object.Uniforms.helpVec; v.x = Std.int(camera_uniforms[9]); //Tonemapping v.y = camera_uniforms[10]; //Film Grain v.z = tonemapper_uniforms[0]; //Slope - } - - if (link == "_PPComp5") { + case "_PPComp5": v = iron.object.Uniforms.helpVec; v.x = tonemapper_uniforms[1]; //Toe v.y = tonemapper_uniforms[2]; //Shoulder v.z = tonemapper_uniforms[3]; //Black Clip - } - - if (link == "_PPComp6") { + case "_PPComp6": v = iron.object.Uniforms.helpVec; v.x = tonemapper_uniforms[4]; //White Clip v.y = lenstexture_uniforms[0]; //Center Min v.z = lenstexture_uniforms[1]; //Center Max - } - - if (link == "_PPComp7") { + case "_PPComp7": v = iron.object.Uniforms.helpVec; v.x = lenstexture_uniforms[2]; //Lum min v.y = lenstexture_uniforms[3]; //Lum max v.z = lenstexture_uniforms[4]; //Expo - } - - if (link == "_PPComp8") { + case "_PPComp8": v = iron.object.Uniforms.helpVec; v.x = colorgrading_global_uniforms[7][0]; //LUT R v.y = colorgrading_global_uniforms[7][1]; //LUT G v.z = colorgrading_global_uniforms[7][2]; //LUT B - } - - if (link == "_PPComp9") { + case "_PPComp9": v = iron.object.Uniforms.helpVec; v.x = ssr_uniforms[0]; //Step v.y = ssr_uniforms[1]; //StepMin v.z = ssr_uniforms[2]; //Search - } - - if (link == "_PPComp10") { + case "_PPComp10": v = iron.object.Uniforms.helpVec; v.x = ssr_uniforms[3]; //Falloff v.y = ssr_uniforms[4]; //Jitter v.z = bloom_uniforms[0]; //Bloom Threshold - } - - if (link == "_PPComp11") { + case "_PPComp11": v = iron.object.Uniforms.helpVec; v.x = bloom_uniforms[1]; //Bloom Strength v.y = bloom_uniforms[2]; //Bloom Radius v.z = ssao_uniforms[0]; //SSAO Strength - } - - if (link == "_PPComp12") { + case "_PPComp12": v = iron.object.Uniforms.helpVec; v.x = ssao_uniforms[1]; //SSAO Radius v.y = ssao_uniforms[2]; //SSAO Max Steps v.z = 0; - } - - if(link == "_PPComp13") { + case "_PPComp13": v = iron.object.Uniforms.helpVec; v.x = chromatic_aberration_uniforms[0]; //CA Strength v.y = chromatic_aberration_uniforms[1]; //CA Samples @@ -354,13 +308,10 @@ class Postprocess { } return v; - } public static function init() { - iron.object.Uniforms.externalVec3Links.push(vec3Link); - } } From 0f99b4a6272c0c1f055db3d26e88ccaa852d9535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 19:36:14 +0200 Subject: [PATCH 178/230] Fix world shader export so that drawing finally works --- blender/arm/make_world.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 68f7ce29..38cb121a 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -45,8 +45,12 @@ def create_world_shaders(world: bpy.types.World, out_shader_datas: List): vert = shader_context.make_vert() frag = shader_context.make_frag() + # Update name, make_vert() and make_frag() above need another name + # to work + shader_context.data['name'] = pass_name + vert.add_out('vec3 normal') - vert.add_uniform('mat4 SMVP') + vert.add_uniform('mat4 SMVP', link="_skydomeMatrix") frag.add_include('compiled.inc') frag.add_in('vec3 normal') @@ -73,7 +77,7 @@ def create_world_shaders(world: bpy.types.World, out_shader_datas: List): # Write shader data file shader_data_file = pass_name + '_data.arm' - arm.utils.write_arm(os.path.join(full_path, shader_data_file), {'shader_datas': shader_context.data}) + arm.utils.write_arm(os.path.join(full_path, shader_data_file), {'contexts': [shader_context.data]}) shader_data_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Shaders', shader_data_file) assets.add_shader_data(shader_data_path) From 620cb1f26e0461dad916eca924de927666423f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:06:38 +0200 Subject: [PATCH 179/230] Custom shader names in shader make functions --- blender/arm/material/shader.py | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index aa0a2f07..e30e68b7 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -125,28 +125,43 @@ class ShaderContext: c['is_image'] = is_image self.tunits.append(c) - def make_vert(self): - self.data['vertex_shader'] = self.matname + '_' + self.data['name'] + '.vert' + def make_vert(self, custom_name: str = None): + if custom_name is None: + self.data['vertex_shader'] = self.matname + '_' + self.data['name'] + '.vert' + else: + self.data['vertex_shader'] = custom_name + '.vert' self.vert = Shader(self, 'vert') return self.vert - def make_frag(self): - self.data['fragment_shader'] = self.matname + '_' + self.data['name'] + '.frag' + def make_frag(self, custom_name: str = None): + if custom_name is None: + self.data['fragment_shader'] = self.matname + '_' + self.data['name'] + '.frag' + else: + self.data['fragment_shader'] = custom_name + '.frag' self.frag = Shader(self, 'frag') return self.frag - def make_geom(self): - self.data['geometry_shader'] = self.matname + '_' + self.data['name'] + '.geom' + def make_geom(self, custom_name: str = None): + if custom_name is None: + self.data['geometry_shader'] = self.matname + '_' + self.data['name'] + '.geom' + else: + self.data['geometry_shader'] = custom_name + '.geom' self.geom = Shader(self, 'geom') return self.geom - def make_tesc(self): - self.data['tesscontrol_shader'] = self.matname + '_' + self.data['name'] + '.tesc' + def make_tesc(self, custom_name: str = None): + if custom_name is None: + self.data['tesscontrol_shader'] = self.matname + '_' + self.data['name'] + '.tesc' + else: + self.data['tesscontrol_shader'] = custom_name + '.tesc' self.tesc = Shader(self, 'tesc') return self.tesc - def make_tese(self): - self.data['tesseval_shader'] = self.matname + '_' + self.data['name'] + '.tese' + def make_tese(self, custom_name: str = None): + if custom_name is None: + self.data['tesseval_shader'] = self.matname + '_' + self.data['name'] + '.tese' + else: + self.data['tesseval_shader'] = custom_name + '.tese' self.tese = Shader(self, 'tese') return self.tese From 180c6065a61bb7db5f0e5559ce9433acae1ff3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:07:48 +0200 Subject: [PATCH 180/230] Fix shader names --- blender/arm/make_world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 38cb121a..5ffec71a 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -42,8 +42,8 @@ def create_world_shaders(world: bpy.types.World, out_shader_datas: List): # ShaderContext expects a material, but using a world also works shader_context = ShaderContext(world, shader_data, shader_props) - vert = shader_context.make_vert() - frag = shader_context.make_frag() + vert = shader_context.make_vert(custom_name="World_" + world_name) + frag = shader_context.make_frag(custom_name="World_" + world_name) # Update name, make_vert() and make_frag() above need another name # to work From e6cca30b536b3665ae9b99f1ece115fa8b599aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:08:05 +0200 Subject: [PATCH 181/230] Get background colors working --- blender/arm/make_world.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 5ffec71a..1a051851 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -62,8 +62,6 @@ def create_world_shaders(world: bpy.types.World, out_shader_datas: List): build_node_tree(world, frag, vert) - frag.write('fragColor = vec4(0.0);') - # TODO: Rework shader export so that it doesn't depend on materials # to prevent workaround code like this rel_path = os.path.join(arm.utils.build_dir(), 'compiled', 'Shaders') @@ -115,6 +113,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): if '_EnvSky' not in world.world_defs and '_EnvTex' not in world.world_defs: if '_EnvImg' not in world.world_defs: world.world_defs += '_EnvCol' + frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') # Irradiance json file name world.arm_envtex_name = world_name world.arm_envtex_irr_name = world_name @@ -124,6 +123,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): if bpy.context.scene is not None and hasattr(bpy.context.scene.render, 'film_transparent') and bpy.context.scene.render.film_transparent: world.world_defs += '_EnvTransp' world.world_defs += '_EnvCol' + frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') # Clouds enabled if rpdat.arm_clouds: @@ -132,6 +132,12 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: world.world_defs += '_EnvStr' + if '_EnvCol' in world.world_defs: + frag.write('fragColor = vec4(backgroundCol, 1.0);') + else: + # Placeholder, replace later + frag.write('fragColor = vec4(1.0, 1.0, 1.0, 1.0);') + def parse_world_output(world, node): if node.inputs[0].is_linked: surface_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) @@ -157,7 +163,7 @@ def parse_surface(world, node): color_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) parse_color(world, color_node) -def parse_color(world, node): +def parse_color(world: bpy.types.World, node: bpy.types.Node): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' From 32613312b3813b7ea7b0820dfe9b55384919e489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:26:05 +0200 Subject: [PATCH 182/230] Add newline after shader functions --- blender/arm/material/shader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index e30e68b7..1507cf50 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -378,7 +378,7 @@ class Shader: for c in self.constants: s += 'const ' + c + ';\n' for f in self.functions: - s += self.functions[f] + s += self.functions[f] + '\n' s += 'void main() {\n' s += self.main_attribs s += self.main_textures From 1aef57581adcbea6782cef2d9294895c173165d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:27:09 +0200 Subject: [PATCH 183/230] Reimplement hosek wilkie sky generation --- blender/arm/make_world.py | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 1a051851..00568442 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -98,7 +98,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): if world.node_tree is not None: output_node = node_utils.get_node_by_type(world.node_tree, 'OUTPUT_WORLD') if output_node is not None: - parse_world_output(world, output_node) + parse_world_output(world, output_node, frag) is_parsed = True if not is_parsed: @@ -130,20 +130,20 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): world.world_defs += '_EnvClouds' if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: - world.world_defs += '_EnvStr' + frag.add_uniform('float envmapStrength', link='_envmapStrength') if '_EnvCol' in world.world_defs: - frag.write('fragColor = vec4(backgroundCol, 1.0);') - else: - # Placeholder, replace later - frag.write('fragColor = vec4(1.0, 1.0, 1.0, 1.0);') + frag.write('fragColor.rgb = backgroundCol;') -def parse_world_output(world, node): + # Mark as non-opaque + frag.write('fragColor.a = 0.0;') + +def parse_world_output(world, node, frag: Shader): if node.inputs[0].is_linked: surface_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) - parse_surface(world, surface_node) + parse_surface(world, surface_node, frag) -def parse_surface(world, node): +def parse_surface(world, node, frag: Shader): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' @@ -161,9 +161,9 @@ def parse_surface(world, node): # Strength if node.inputs[0].is_linked: color_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) - parse_color(world, color_node) + parse_color(world, color_node, frag) -def parse_color(world: bpy.types.World, node: bpy.types.Node): +def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' @@ -278,6 +278,26 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node): world.world_defs += '_EnvSky' assets.add_khafile_def('arm_hosek') + frag.add_uniform('vec3 A', link="_hosekA") + frag.add_uniform('vec3 B', link="_hosekB") + frag.add_uniform('vec3 C', link="_hosekC") + frag.add_uniform('vec3 D', link="_hosekD") + frag.add_uniform('vec3 E', link="_hosekE") + frag.add_uniform('vec3 F', link="_hosekF") + frag.add_uniform('vec3 G', link="_hosekG") + frag.add_uniform('vec3 H', link="_hosekH") + frag.add_uniform('vec3 I', link="_hosekI") + frag.add_uniform('vec3 Z', link="_hosekZ") + frag.add_uniform('vec3 hosekSunDirection', link="_hosekSunDirection") + frag.add_function('''vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) { +\tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5)); +\treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta)); +}''') + frag.write('vec3 n = normalize(normal);') + frag.write('float cos_theta = clamp(n.z, 0.0, 1.0);') + frag.write('float cos_gamma = dot(n, hosekSunDirection);') + frag.write('float gamma_val = acos(cos_gamma);') + frag.write('fragColor.rgb = Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;') world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] world.arm_envtex_turbidity = node.turbidity From 702588b0c4b74b3a64624e06f13ba8e4fa8fa4e7 Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 28 Jun 2020 20:32:08 +0200 Subject: [PATCH 184/230] CMFT use available cpus --- blender/arm/write_probes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 94cb904d..04c8a02b 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -1,4 +1,5 @@ import bpy +import multiprocessing import os import sys import subprocess @@ -114,6 +115,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True wrd = bpy.data.worlds['Arm'] use_opencl = 'true' + cpu_count = multiprocessing.cpu_count() if arm.utils.get_os() == 'win': subprocess.call([ \ @@ -128,7 +130,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True '--glossBias', '3', '--lightingModel', 'blinnbrdf', '--edgeFixup', 'none', - '--numCpuProcessingThreads', '4', + '--numCpuProcessingThreads', str(cpu_count), '--useOpenCL', use_opencl, '--clVendor', 'anyGpuVendor', '--deviceType', 'gpu', @@ -154,7 +156,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True ' --glossBias 3' + \ ' --lightingModel blinnbrdf' + \ ' --edgeFixup none' + \ - ' --numCpuProcessingThreads 4' + \ + ' --numCpuProcessingThreads ' + str(cpu_count) + \ ' --useOpenCL ' + use_opencl + \ ' --clVendor anyGpuVendor' + \ ' --deviceType gpu' + \ From 1d0c96db116aa2b27a64edfd31232755661a88af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 20:47:40 +0200 Subject: [PATCH 185/230] Code cleanup and other small improvements --- blender/arm/make_world.py | 44 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 00568442..5d9a6091 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -16,6 +16,7 @@ from arm.material.shader import ShaderContext, Shader callback = None shader_datas = [] + def build(): worlds = [] for scene in bpy.data.scenes: @@ -98,15 +99,15 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): if world.node_tree is not None: output_node = node_utils.get_node_by_type(world.node_tree, 'OUTPUT_WORLD') if output_node is not None: - parse_world_output(world, output_node, frag) - is_parsed = True + is_parsed = parse_world_output(world, output_node, frag) + # No world nodes/no output node, use background color if not is_parsed: solid_mat = rpdat.arm_material_model == 'Solid' if rpdat.arm_irradiance and not solid_mat: world.world_defs += '_Irr' - c = world.color - world.arm_envtex_color = [c[0], c[1], c[2], 1.0] + col = world.color + world.arm_envtex_color = [col[0], col[1], col[2], 1.0] world.arm_envtex_strength = 1.0 # Clear to color if no texture or sky is provided @@ -138,30 +139,39 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): # Mark as non-opaque frag.write('fragColor.a = 0.0;') -def parse_world_output(world, node, frag: Shader): - if node.inputs[0].is_linked: - surface_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) - parse_surface(world, surface_node, frag) -def parse_surface(world, node, frag: Shader): +def parse_world_output(world: bpy.types.World, node_output: bpy.types.Node, frag: Shader) -> bool: + """Parse the world's output node. Return `False` when the node has + no connected surface input.""" + if not node_output.inputs[0].is_linked: + return False + + surface_node = node_utils.find_node_by_link(world.node_tree, node_output, node_output.inputs[0]) + parse_surface(world, surface_node, frag) + return True + + +def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Shader): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' - # Extract environment strength - if node.type == 'BACKGROUND': - + if node_surface.type in ('BACKGROUND', 'EMISSION'): # Append irradiance define if rpdat.arm_irradiance and not solid_mat: wrd.world_defs += '_Irr' - world.arm_envtex_color = node.inputs[0].default_value - world.arm_envtex_strength = node.inputs[1].default_value + # Extract environment strength + # Todo: follow/parse strength input + world.arm_envtex_strength = node_surface.inputs[1].default_value - # Strength - if node.inputs[0].is_linked: - color_node = node_utils.find_node_by_link(world.node_tree, node, node.inputs[0]) + # Color + if node_surface.inputs[0].is_linked: + color_node = node_utils.find_node_by_link(world.node_tree, node_surface, node_surface.inputs[0]) parse_color(world, color_node, frag) + else: + world.arm_envtex_color = node_surface.inputs[0].default_value + def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): wrd = bpy.data.worlds['Arm'] From 89fa3e8314d7cd225999c449e8a613bfeaca55de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 28 Jun 2020 22:45:54 +0200 Subject: [PATCH 186/230] Reimplement clouds --- blender/arm/exporter.py | 3 +- blender/arm/make_renderpath.py | 1 - blender/arm/make_world.py | 116 ++++++++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 29d5afbe..109a4b48 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1956,7 +1956,6 @@ class ArmoryExporter: self.world_array.append(world_name) out_world = {'name': world_name} - make_world.create_world_shaders(world, self.output['material_datas']) self.post_export_world(world, out_world) self.output['world_datas'].append(out_world) @@ -2811,7 +2810,7 @@ class ArmoryExporter: if '_EnvSky' in world.world_defs: # Sky data for probe - out_world['sun_direction'] = list(world.arm_envtex_sun_direction) + out_world['sun_direction'] = list(world.arm_envtex_sun_direction) out_world['turbidity'] = world.arm_envtex_turbidity out_world['ground_albedo'] = world.arm_envtex_ground_albedo diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index f0e76103..7889cb88 100755 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -134,7 +134,6 @@ def build(): assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) if rpdat.rp_background == 'World': - assets.add_shader_pass('world_pass') if '_EnvClouds' in wrd.world_defs: assets.add(assets_path + 'clouds_base.raw') assets.add_embedded_data('clouds_base.raw') diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 5d9a6091..ea803526 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -20,12 +20,13 @@ shader_datas = [] def build(): worlds = [] for scene in bpy.data.scenes: + # Only export worlds from enabled scenes if scene.arm_export and scene.world is not None and scene.world not in worlds: worlds.append(scene.world) - # create_world_shaders(scene.world) + create_world_shaders(scene.world) -def create_world_shaders(world: bpy.types.World, out_shader_datas: List): +def create_world_shaders(world: bpy.types.World): """Creates fragment and vertex shaders for the given world.""" global shader_datas world_name = arm.utils.safestr(world.name) @@ -90,6 +91,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): world_name = arm.utils.safestr(world.name) world.world_defs = '' rpdat = arm.utils.get_rp() + wrd = bpy.data.worlds['Arm'] if callback is not None: callback() @@ -127,17 +129,17 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') # Clouds enabled - if rpdat.arm_clouds: + if rpdat.arm_clouds and world.arm_use_clouds: world.world_defs += '_EnvClouds' + # Also set this flag globally so that the required textures are + # included + wrd.world_defs += '_EnvClouds' + frag_write_clouds(world, frag) if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: frag.add_uniform('float envmapStrength', link='_envmapStrength') - if '_EnvCol' in world.world_defs: - frag.write('fragColor.rgb = backgroundCol;') - - # Mark as non-opaque - frag.write('fragColor.a = 0.0;') + frag_write_main(world, frag) def parse_world_output(world: bpy.types.World, node_output: bpy.types.Node, frag: Shader) -> bool: @@ -330,3 +332,101 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): world.arm_envtex_name = 'hosek' world.arm_envtex_num_mips = 8 + + +def frag_write_clouds(world: bpy.types.World, frag: Shader): + """References: + GPU PRO 7 - Real-time Volumetric Cloudscapes + https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn + https://github.com/sebh/TileableVolumeNoise + """ + frag.add_uniform('sampler3D scloudsBase', link='$clouds_base.raw') + frag.add_uniform('sampler3D scloudsDetail', link='$clouds_detail.raw') + frag.add_uniform('sampler2D scloudsMap', link='$clouds_map.png') + frag.add_uniform('float time', link='_time') + + frag.add_const('float', 'cloudsLower', str(round(world.arm_clouds_lower * 100) / 100)) + frag.add_const('float', 'cloudsUpper', str(round(world.arm_clouds_upper * 100) / 100)) + frag.add_const('vec2', 'cloudsWind', 'vec2(' + str(round(world.arm_clouds_wind[0] * 100) / 100) + ',' + str(round(world.arm_clouds_wind[1] * 100) / 100) + ')') + frag.add_const('float', 'cloudsPrecipitation', str(round(world.arm_clouds_precipitation * 100) / 100)) + frag.add_const('float', 'cloudsSecondary', str(round(world.arm_clouds_secondary * 100) / 100)) + frag.add_const('float', 'cloudsSteps', str(round(world.arm_clouds_steps * 100) / 100)) + + frag.add_function('''float remap(float old_val, float old_min, float old_max, float new_min, float new_max) { +\treturn new_min + (((old_val - old_min) / (old_max - old_min)) * (new_max - new_min)); +}''') + + frag.add_function('''float getDensityHeightGradientForPoint(float height, float cloud_type) { +\tconst vec4 stratusGrad = vec4(0.02f, 0.05f, 0.09f, 0.11f); +\tconst vec4 stratocumulusGrad = vec4(0.02f, 0.2f, 0.48f, 0.625f); +\tconst vec4 cumulusGrad = vec4(0.01f, 0.0625f, 0.78f, 1.0f); +\tfloat stratus = 1.0f - clamp(cloud_type * 2.0f, 0, 1); +\tfloat stratocumulus = 1.0f - abs(cloud_type - 0.5f) * 2.0f; +\tfloat cumulus = clamp(cloud_type - 0.5f, 0, 1) * 2.0f; +\tvec4 cloudGradient = stratusGrad * stratus + stratocumulusGrad * stratocumulus + cumulusGrad * cumulus; +\treturn smoothstep(cloudGradient.x, cloudGradient.y, height) - smoothstep(cloudGradient.z, cloudGradient.w, height); +}''') + + frag.add_function('''float sampleCloudDensity(vec3 p) { +\tfloat cloud_base = textureLod(scloudsBase, p, 0).r * 40; // Base noise +\tvec3 weather_data = textureLod(scloudsMap, p.xy, 0).rgb; // Weather map +\tcloud_base *= getDensityHeightGradientForPoint(p.z, weather_data.b); // Cloud type +\tcloud_base = remap(cloud_base, weather_data.r, 1.0, 0.0, 1.0); // Coverage +\tcloud_base *= weather_data.r; +\tfloat cloud_detail = textureLod(scloudsDetail, p, 0).r * 2; // Detail noise +\tfloat cloud_detail_mod = mix(cloud_detail, 1.0 - cloud_detail, clamp(p.z * 10.0, 0, 1)); +\tcloud_base = remap(cloud_base, cloud_detail_mod * 0.2, 1.0, 0.0, 1.0); +\treturn cloud_base; +}''') + + func_cloud_radiance = 'float cloudRadiance(vec3 p, vec3 dir) {\n' + if '_EnvSky' in world.world_defs: + func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n' + else: + func_cloud_radiance += '\tvec3 sun_dir = vec3(0, 0, -1);\n' + func_cloud_radiance += '''\tconst int steps = 8; +\tfloat step_size = 0.5 / float(steps); +\tfloat d = 0.0; +\tp += sun_dir * step_size; +\tfor(int i = 0; i < steps; ++i) { +\t\td += sampleCloudDensity(p + sun_dir * float(i) * step_size); +\t} +\treturn 1.0 - d; +}''' + frag.add_function(func_cloud_radiance) + + frag.add_function('''vec3 traceClouds(vec3 sky, vec3 dir) { +\tconst float step_size = 0.5 / float(cloudsSteps); +\tfloat T = 1.0; +\tfloat C = 0.0; +\tvec2 uv = dir.xy / dir.z * 0.4 * cloudsLower + cloudsWind * time * 0.02; + +\tfor (int i = 0; i < cloudsSteps; ++i) { +\t\tfloat h = float(i) / float(cloudsSteps); +\t\tvec3 p = vec3(uv * 0.04, h); +\t\tfloat d = sampleCloudDensity(p); + +\t\tif (d > 0) { +\t\t\t// float radiance = cloudRadiance(p, dir); +\t\t\tC += T * exp(h) * d * step_size * 0.6 * cloudsPrecipitation; +\t\t\tT *= exp(-d * step_size); +\t\t\tif (T < 0.01) break; +\t\t} +\t\tuv += (dir.xy / dir.z) * step_size * cloudsUpper; +\t} + +\treturn vec3(C) + sky * T; +}''') + + +def frag_write_main(world: bpy.types.World, frag: Shader): + if '_EnvCol' in world.world_defs: + frag.write('fragColor.rgb = backgroundCol;') + + if '_EnvClouds' in world.world_defs: + if '_EnvCol' in world.world_defs: + frag.write('vec3 n = normalize(normal);') + frag.write('if (n.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, n), clamp(n.z * 5.0, 0, 1));') + + # Mark as non-opaque + frag.write('fragColor.a = 0.0;') From f89ac4ba7d2ffd11e1096f009ca346d50cf0e7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valent=C3=ADn=20Barros?= Date: Mon, 29 Jun 2020 12:44:21 +0200 Subject: [PATCH 187/230] Fixed `armory.trait.physics.bullet.PhysicsWorld.rayCast` for Hashlink. Just enabling the same code used for C++. --- Sources/armory/trait/physics/bullet/PhysicsWorld.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx index d130f79e..18a854e8 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -327,7 +327,7 @@ class PhysicsWorld extends Trait { #if js rayCallback.set_m_collisionFilterGroup(group); rayCallback.set_m_collisionFilterMask(mask); - #elseif cpp + #elseif (cpp || hl) rayCallback.m_collisionFilterGroup = group; rayCallback.m_collisionFilterMask = mask; #end @@ -348,7 +348,7 @@ class PhysicsWorld extends Trait { hitNormalWorld.set(norm.x(), norm.y(), norm.z()); rb = rbMap.get(untyped body.userIndex); hitInfo = new Hit(rb, hitPointWorld, hitNormalWorld); - #elseif cpp + #elseif (cpp || hl) var hit = rayCallback.m_hitPointWorld; hitPointWorld.set(hit.x(), hit.y(), hit.z()); var norm = rayCallback.m_hitNormalWorld; From 7dec8e004c1ff89530676b15dc49bcfe76a7313a Mon Sep 17 00:00:00 2001 From: tong Date: Tue, 30 Jun 2020 17:02:57 +0200 Subject: [PATCH 188/230] CMFT: add --silent param if not arm_verbose_output --- blender/arm/write_probes.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 04c8a02b..93412288 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -143,15 +143,18 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True '--outputNum', '1', '--output0', output_file_rad, '--output0params', 'hdr,rgbe,latlong']) + if wrd.arm_verbose_output: + print(cmd) + else: + cmd.append(' --silent') + subprocess.call([cmd]) else: - subprocess.call([ \ - cmft_path + \ + cmd = cmft_path + \ ' --input "' + scaled_file + '"' + \ ' --filter radiance' + \ ' --dstFaceSize ' + str(face_size) + \ ' --srcFaceSize ' + str(face_size) + \ ' --excludeBase false' + \ - #' --mipCount ' + str(mip_count) + \ ' --glossScale 8' + \ ' --glossBias 3' + \ ' --lightingModel blinnbrdf' + \ @@ -168,7 +171,12 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True ' --outputGammaDenominator 1.0' + \ ' --outputNum 1' + \ ' --output0 "' + output_file_rad + '"' + \ - ' --output0params hdr,rgbe,latlong'], shell=True) + ' --output0params hdr,rgbe,latlong' + if wrd.arm_verbose_output: + print(cmd) + else: + cmd += ' --silent' + subprocess.call([cmd], shell=True) # Remove size extensions in file name mip_w = int(face_size * 4) From 7f4a2788ccab6708c91aab516b3e30c7d89e40bb Mon Sep 17 00:00:00 2001 From: luboslenco Date: Tue, 30 Jun 2020 22:49:15 +0200 Subject: [PATCH 189/230] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index b9854531..c3d41ce9 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -12,7 +12,7 @@ import arm.proxy import arm.nodes_logic # Armory version -arm_version = '2020.6' +arm_version = '2020.7' arm_commit = '$Id$' def init_properties(): From 34a6a428d24fb457cc74363f35bc538870e9c540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valent=C3=ADn=20Barros?= Date: Wed, 1 Jul 2020 09:26:05 +0200 Subject: [PATCH 190/230] Fixed `armory.trait.PhysicsDrag.update` for Hashlink [fixes #1730] Just enabling the same code used for JavaScript. --- Sources/armory/trait/PhysicsDrag.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/trait/PhysicsDrag.hx b/Sources/armory/trait/PhysicsDrag.hx index 4c9d58b6..bef674b5 100755 --- a/Sources/armory/trait/PhysicsDrag.hx +++ b/Sources/armory/trait/PhysicsDrag.hx @@ -103,7 +103,7 @@ class PhysicsDrag extends Trait { dir.setZ(dir.z() * pickDist); var newPivotB = new bullet.Bt.Vector3(rayFrom.x() + dir.x(), rayFrom.y() + dir.y(), rayFrom.z() + dir.z()); - #if js + #if (js || hl) pickConstraint.getFrameOffsetA().setOrigin(newPivotB); #elseif cpp pickConstraint.setFrameOffsetAOrigin(newPivotB); From 8e66ade354847668744d08d860dc4cb991999094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 20:09:49 +0200 Subject: [PATCH 191/230] Fix and reimplement static background images --- blender/arm/exporter.py | 2 +- blender/arm/make_world.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 109a4b48..b57bf6b0 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2815,7 +2815,7 @@ class ArmoryExporter: out_world['ground_albedo'] = world.arm_envtex_ground_albedo disable_hdr = world.arm_envtex_name.endswith('.jpg') - print('_EnvTex' in world.world_defs) + if '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs: out_world['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] if disable_hdr: diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index ea803526..c57816fe 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -118,7 +118,8 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): world.world_defs += '_EnvCol' frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') # Irradiance json file name - world.arm_envtex_name = world_name + # Todo: this breaks static image backgrounds + # world.arm_envtex_name = world_name world.arm_envtex_irr_name = world_name write_probes.write_color_irradiance(world_name, world.arm_envtex_color) @@ -261,10 +262,16 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): # Static image background elif node.type == 'TEX_IMAGE': + world.world_defs += '_EnvImg' + + # Background texture + frag.add_uniform('sampler2D envmap', link='_envmap') + frag.add_uniform('vec2 screenSize', link='_screenSize') + image = node.image filepath = image.filepath - if image.packed_file != None: + if image.packed_file is not None: # Extract packed data filepath = arm.utils.build_dir() + '/compiled/Assets/unpacked' unpack_path = arm.utils.get_fp() + filepath @@ -423,6 +430,12 @@ def frag_write_main(world: bpy.types.World, frag: Shader): if '_EnvCol' in world.world_defs: frag.write('fragColor.rgb = backgroundCol;') + elif '_EnvImg' in world.world_defs: + # Will have to get rid of gl_FragCoord, pass texture coords from + # vertex shader + frag.write('vec2 texco = gl_FragCoord.xy / screenSize;') + frag.write('fragColor.rgb = texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength;') + if '_EnvClouds' in world.world_defs: if '_EnvCol' in world.world_defs: frag.write('vec3 n = normalize(normal);') From 2ff04969087e4cbe011549c2f230f67b0010242c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 20:19:07 +0200 Subject: [PATCH 192/230] Per world environment maps + LDR support --- blender/arm/make_world.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index c57816fe..5edd4a45 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -183,6 +183,10 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): # Env map included if node.type == 'TEX_ENVIRONMENT' and node.image is not None: + world.world_defs += '_EnvTex' + + frag.add_include('std/math.glsl') + frag.add_uniform('sampler2D envmap', link='_envmap') image = node.image filepath = image.filepath @@ -251,8 +255,6 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): world.arm_envtex_num_mips = mip_count - # Append envtex define - world.world_defs += '_EnvTex' # Append LDR define if disable_hdr: world.world_defs += '_EnvLDR' @@ -430,16 +432,29 @@ def frag_write_main(world: bpy.types.World, frag: Shader): if '_EnvCol' in world.world_defs: frag.write('fragColor.rgb = backgroundCol;') + # Static background image elif '_EnvImg' in world.world_defs: # Will have to get rid of gl_FragCoord, pass texture coords from # vertex shader frag.write('vec2 texco = gl_FragCoord.xy / screenSize;') frag.write('fragColor.rgb = texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength;') + # Environment texture + # Check for _EnvSky too to prevent case when sky radiance is enabled + elif '_EnvTex' in world.world_defs and '_EnvSky' not in world.world_defs: + frag.write('vec3 n = normalize(normal);') + frag.write('fragColor.rgb = texture(envmap, envMapEquirect(n)).rgb * envmapStrength;') + + if '_EnvLDR' in world.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));') + if '_EnvClouds' in world.world_defs: if '_EnvCol' in world.world_defs: frag.write('vec3 n = normalize(normal);') frag.write('if (n.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, n), clamp(n.z * 5.0, 0, 1));') + if '_EnvLDR' in world.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));') + # Mark as non-opaque frag.write('fragColor.a = 0.0;') From d0e936994710a2b52670cb1de6cc2a3031d7303d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 20:57:01 +0200 Subject: [PATCH 193/230] Revert water/fog UI and remove now unused properties --- blender/arm/props.py | 15 --------------- blender/arm/props_renderpath.py | 30 ++++++++++++----------------- blender/arm/props_ui.py | 34 +++++++++++++++------------------ 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index e0a5b598..b789a041 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -270,11 +270,6 @@ def init_properties(): bpy.types.World.world_defs = StringProperty(name="World Shader Defs", default='') bpy.types.World.compo_defs = StringProperty(name="Compositor Shader Defs", default='') - bpy.types.World.arm_use_fog = BoolProperty(name="Volumetric Fog", default=False, update=assets.invalidate_shader_cache) - bpy.types.World.arm_fog_color = FloatVectorProperty(name="Color", size=3, subtype='COLOR', default=[0.5, 0.6, 0.7], min=0, max=1, update=assets.invalidate_shader_cache) - bpy.types.World.arm_fog_amounta = FloatProperty(name="Amount A", default=0.25, update=assets.invalidate_shader_cache) - bpy.types.World.arm_fog_amountb = FloatProperty(name="Amount B", default=0.5, update=assets.invalidate_shader_cache) - bpy.types.World.arm_use_clouds = BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_lower = FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_upper = FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) @@ -283,16 +278,6 @@ def init_properties(): bpy.types.World.arm_clouds_precipitation = FloatProperty(name="Precipitation", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_steps = IntProperty(name="Steps", default=24, min=1, max=240, update=assets.invalidate_shader_cache) - bpy.types.World.arm_use_water = BoolProperty(name="Water", description="Water surface pass", default=False, update=props_renderpath.update_renderpath) - bpy.types.World.arm_water_color = FloatVectorProperty(name="Color", size=3, default=[1, 1, 1], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_level = FloatProperty(name="Level", default=0.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_displace = FloatProperty(name="Displace", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_speed = FloatProperty(name="Speed", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_freq = FloatProperty(name="Freq", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_density = FloatProperty(name="Density", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_refract = FloatProperty(name="Refract", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.World.arm_water_reflect = FloatProperty(name="Reflect", default=1.0, update=assets.invalidate_shader_cache) - bpy.types.Material.export_uvs = BoolProperty(name="Export UVs", default=False) bpy.types.Material.export_vcols = BoolProperty(name="Export VCols", default=False) bpy.types.Material.export_tangents = BoolProperty(name="Export Tangents", default=False) diff --git a/blender/arm/props_renderpath.py b/blender/arm/props_renderpath.py index 62e281df..ef34ec76 100644 --- a/blender/arm/props_renderpath.py +++ b/blender/arm/props_renderpath.py @@ -233,12 +233,12 @@ class ArmRPListItem(bpy.types.PropertyGroup): ('Clear', 'Clear', 'Clear'), ('Off', 'No Clear', 'Off'), ], - name="Background", description="Background type", default='World', update=update_renderpath) + name="Background", description="Background type", default='World', update=update_renderpath) arm_irradiance: BoolProperty(name="Irradiance", description="Generate spherical harmonics", default=True, update=assets.invalidate_shader_cache) arm_radiance: BoolProperty(name="Radiance", description="Generate radiance textures", default=True, update=assets.invalidate_shader_cache) arm_radiance_size: EnumProperty( items=[('512', '512', '512'), - ('1024', '1024', '1024'), + ('1024', '1024', '1024'), ('2048', '2048', '2048')], name="Map Size", description="Prefiltered map size", default='1024', update=assets.invalidate_envmap_data) rp_autoexposure: BoolProperty(name="Auto Exposure", description="Adjust exposure based on luminance", default=False, update=update_renderpath) @@ -296,19 +296,19 @@ class ArmRPListItem(bpy.types.PropertyGroup): rp_translucency: BoolProperty(name="Translucency", description="Current render-path state", default=False) rp_translucency_state: EnumProperty( items=[('On', 'On', 'On'), - ('Off', 'Off', 'Off'), + ('Off', 'Off', 'Off'), ('Auto', 'Auto', 'Auto')], name="Translucency", description="Order independent translucency", default='Auto', update=update_translucency_state) rp_decals: BoolProperty(name="Decals", description="Current render-path state", default=False) rp_decals_state: EnumProperty( items=[('On', 'On', 'On'), - ('Off', 'Off', 'Off'), + ('Off', 'Off', 'Off'), ('Auto', 'Auto', 'Auto')], name="Decals", description="Decals pass", default='Auto', update=update_decals_state) rp_overlays: BoolProperty(name="Overlays", description="Current render-path state", default=False) rp_overlays_state: EnumProperty( items=[('On', 'On', 'On'), - ('Off', 'Off', 'Off'), + ('Off', 'Off', 'Off'), ('Auto', 'Auto', 'Auto')], name="Overlays", description="X-Ray pass", default='Auto', update=update_overlays_state) rp_sss: BoolProperty(name="SSS", description="Current render-path state", default=False) @@ -329,7 +329,7 @@ class ArmRPListItem(bpy.types.PropertyGroup): ('Shader', 'Shader', 'Shader')], name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data) rp_stereo: BoolProperty(name="VR", description="Stereo rendering", default=False, update=update_renderpath) - rp_water: BoolProperty(name="Water", description="Water surface pass", default=False, update=update_renderpath) + rp_water: BoolProperty(name="Water", description="Enable water surface pass", default=False, update=update_renderpath) rp_pp: BoolProperty(name="Realtime postprocess", description="Realtime postprocess", default=False, update=update_renderpath) rp_gi: EnumProperty( # TODO: remove in 0.8 items=[('Off', 'Off', 'Off'), @@ -350,13 +350,13 @@ class ArmRPListItem(bpy.types.PropertyGroup): ('0.5', '0.5', '0.5'), ('0.25', '0.25', '0.25')], name="Resolution Z", description="3D texture z resolution multiplier", default='1.0', update=update_renderpath) - arm_clouds: BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache) + arm_clouds: BoolProperty(name="Clouds", description="Enable clouds pass", default=False, update=assets.invalidate_shader_cache) arm_ssrs: BoolProperty(name="SSRS", description="Screen-space ray-traced shadows", default=False, update=assets.invalidate_shader_cache) arm_micro_shadowing: BoolProperty(name="Micro Shadowing", description="Micro shadowing based on ambient occlusion", default=False, update=assets.invalidate_shader_cache) arm_texture_filter: EnumProperty( items=[('Anisotropic', 'Anisotropic', 'Anisotropic'), - ('Linear', 'Linear', 'Linear'), - ('Point', 'Closest', 'Point'), + ('Linear', 'Linear', 'Linear'), + ('Point', 'Closest', 'Point'), ('Manual', 'Manual', 'Manual')], name="Texture Filtering", description="Set Manual to honor interpolation setting on Image Texture node", default='Anisotropic') arm_material_model: EnumProperty( @@ -380,7 +380,7 @@ class ArmRPListItem(bpy.types.PropertyGroup): name="Resolution", description="Resolution to perform rendering at", default='Display', update=update_renderpath) arm_rp_resolution_size: IntProperty(name="Size", description="Resolution height in pixels(for example 720p), width is auto-fit to preserve aspect ratio", default=720, min=0, update=update_renderpath) arm_rp_resolution_filter: EnumProperty( - items=[('Linear', 'Linear', 'Linear'), + items=[('Linear', 'Linear', 'Linear'), ('Point', 'Closest', 'Point')], name="Filter", description="Scaling filter", default='Linear') rp_dynres: BoolProperty(name="Dynamic Resolution", description="Dynamic resolution scaling for performance", default=False, update=update_renderpath) @@ -397,7 +397,7 @@ class ArmRPListItem(bpy.types.PropertyGroup): ('4', '4', '4'), ('8', '8', '8'), ('16', '16', '16')], - name="MSAA", description="Samples per pixel usable for render paths drawing directly to framebuffer", default='1') + name="MSAA", description="Samples per pixel usable for render paths drawing directly to framebuffer", default='1') arm_voxelgi_cones: EnumProperty( items=[('9', '9', '9'), @@ -412,13 +412,7 @@ class ArmRPListItem(bpy.types.PropertyGroup): arm_voxelgi_range: FloatProperty(name="Range", description="Maximum range", default=2.0, update=assets.invalidate_shader_cache) arm_voxelgi_aperture: FloatProperty(name="Aperture", description="Cone aperture for shadow trace", default=1.0, update=assets.invalidate_shader_cache) arm_sss_width: FloatProperty(name="Width", description="SSS blur strength", default=1.0, update=assets.invalidate_shader_cache) - arm_clouds_lower: FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) - arm_clouds_upper: FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) - arm_clouds_wind: FloatVectorProperty(name="Wind", default=[1.0, 0.0], size=2, update=assets.invalidate_shader_cache) - arm_clouds_secondary: FloatProperty(name="Secondary", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) - arm_clouds_precipitation: FloatProperty(name="Precipitation", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) - arm_clouds_steps: IntProperty(name="Steps", default=24, min=1, max=240, update=assets.invalidate_shader_cache) - arm_water_color: FloatVectorProperty(name="Color", size=3, default=[1,1,1], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) + arm_water_color: FloatVectorProperty(name="Color", size=3, default=[1, 1, 1], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) arm_water_level: FloatProperty(name="Level", default=0.0, update=assets.invalidate_shader_cache) arm_water_displace: FloatProperty(name="Displace", default=1.0, update=assets.invalidate_shader_cache) arm_water_speed: FloatProperty(name="Speed", default=1.0, update=assets.invalidate_shader_cache) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 7cbdccfb..938fb830 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -159,13 +159,6 @@ class ARM_PT_WorldPropsPanel(bpy.types.Panel): if world is None: return - layout.prop(world, 'arm_use_fog') - col = layout.column(align=True) - col.enabled = world.arm_use_fog - col.prop(world, 'arm_fog_color') - col.prop(world, 'arm_fog_amounta') - col.prop(world, 'arm_fog_amountb') - layout.prop(world, 'arm_use_clouds') col = layout.column(align=True) col.enabled = world.arm_use_clouds @@ -176,18 +169,6 @@ class ARM_PT_WorldPropsPanel(bpy.types.Panel): col.prop(world, 'arm_clouds_wind') col.prop(world, 'arm_clouds_steps') - layout.prop(world, "arm_use_water") - col = layout.column(align=True) - col.enabled = world.arm_use_water - col.prop(world, 'arm_water_level') - col.prop(world, 'arm_water_density') - col.prop(world, 'arm_water_displace') - col.prop(world, 'arm_water_speed') - col.prop(world, 'arm_water_freq') - col.prop(world, 'arm_water_refract') - col.prop(world, 'arm_water_reflect') - col.prop(world, 'arm_water_color') - class ARM_PT_ScenePropsPanel(bpy.types.Panel): bl_label = "Armory Props" bl_space_type = "PROPERTIES" @@ -870,6 +851,16 @@ class ARM_PT_RenderPathWorldPanel(bpy.types.Panel): colb.prop(rpdat, 'arm_radiance_size') layout.prop(rpdat, 'arm_clouds') layout.prop(rpdat, "rp_water") + col = layout.column(align=True) + col.enabled = rpdat.rp_water + col.prop(rpdat, 'arm_water_level') + col.prop(rpdat, 'arm_water_density') + col.prop(rpdat, 'arm_water_displace') + col.prop(rpdat, 'arm_water_speed') + col.prop(rpdat, 'arm_water_freq') + col.prop(rpdat, 'arm_water_refract') + col.prop(rpdat, 'arm_water_reflect') + col.prop(rpdat, 'arm_water_color') class ARM_PT_RenderPathPostProcessPanel(bpy.types.Panel): bl_label = "Post Process" @@ -1003,6 +994,11 @@ class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): col.enabled = rpdat.arm_grain col.prop(rpdat, 'arm_grain_strength') layout.prop(rpdat, 'arm_fog') + col = layout.column(align=True) + col.enabled = rpdat.arm_fog + col.prop(rpdat, 'arm_fog_color') + col.prop(rpdat, 'arm_fog_amounta') + col.prop(rpdat, 'arm_fog_amountb') layout.separator() layout.prop(rpdat, "rp_autoexposure") col = layout.column() From 70b0219a0585e69b4ee64464cc5036ab88ccd237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 21:06:02 +0200 Subject: [PATCH 194/230] Reorganize imports and remove unused ones --- blender/arm/make.py | 27 ++++++++++++++------------- blender/arm/make_world.py | 15 ++++++--------- blender/arm/props.py | 10 +++------- blender/arm/props_renderpath.py | 8 +++----- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index f80ab6aa..4cbe3347 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -1,25 +1,26 @@ -import os import glob -import time -import shutil -import bpy import json +import os +import shutil +import time import stat -from bpy.props import * import subprocess import threading import webbrowser -import arm.utils -import arm.write_data as write_data -import arm.make_logic as make_logic -import arm.make_renderpath as make_renderpath -import arm.make_world as make_world -import arm.make_state as state + +import bpy + import arm.assets as assets -import arm.log as log +from arm.exporter import ArmoryExporter import arm.lib.make_datas import arm.lib.server -from arm.exporter import ArmoryExporter +import arm.log as log +import arm.make_logic as make_logic +import arm.make_renderpath as make_renderpath +import arm.make_state as state +import arm.make_world as make_world +import arm.utils +import arm.write_data as write_data scripts_mtime = 0 # Monitor source changes profile_time = 0 diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 5edd4a45..c97c4924 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -1,17 +1,14 @@ -import bpy import os -from bpy.types import NodeTree, Node, NodeSocket -from bpy.props import * -from typing import List -import arm.write_probes as write_probes +import bpy + import arm.assets as assets -import arm.utils -import arm.node_utils as node_utils import arm.log as log -import arm.make_state as state -from arm.material import make_shader, mat_state +from arm.material import make_shader from arm.material.shader import ShaderContext, Shader +import arm.node_utils as node_utils +import arm.utils +import arm.write_probes as write_probes callback = None shader_datas = [] diff --git a/blender/arm/props.py b/blender/arm/props.py index b789a041..44668b41 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -1,15 +1,11 @@ import bpy from bpy.props import * -import os -import shutil -import arm.props_ui as props_ui + import arm.assets as assets -import arm.log as log -import arm.utils import arm.make -import arm.props_renderpath as props_renderpath -import arm.proxy import arm.nodes_logic +import arm.proxy +import arm.utils # Armory version arm_version = '2020.6' diff --git a/blender/arm/props_renderpath.py b/blender/arm/props_renderpath.py index ef34ec76..e5ba673b 100644 --- a/blender/arm/props_renderpath.py +++ b/blender/arm/props_renderpath.py @@ -1,10 +1,8 @@ -import os -import shutil +import bpy +from bpy.props import * + import arm.assets as assets import arm.utils -import bpy -from bpy.types import Menu, Panel, UIList -from bpy.props import * def update_preset(self, context): rpdat = arm.utils.get_rp() From d9231f6f9a22610bbfd8b8daf0655f8267e6b42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 21:11:25 +0200 Subject: [PATCH 195/230] Remove not used world generation callback --- blender/arm/make_world.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index c97c4924..e47548ac 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -10,14 +10,14 @@ import arm.node_utils as node_utils import arm.utils import arm.write_probes as write_probes -callback = None +# See make.py shader_datas = [] def build(): worlds = [] for scene in bpy.data.scenes: - # Only export worlds from enabled scenes + # Only export worlds from enabled scenes that are not yet exported if scene.arm_export and scene.world is not None and scene.world not in worlds: worlds.append(scene.world) create_world_shaders(scene.world) @@ -90,9 +90,6 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): rpdat = arm.utils.get_rp() wrd = bpy.data.worlds['Arm'] - if callback is not None: - callback() - # Traverse world node tree is_parsed = False if world.node_tree is not None: From fb5ac23f7e487597355fd91cae238c55b6170742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 21:13:40 +0200 Subject: [PATCH 196/230] Revert "Remove not used world generation callback" This reverts commit d9231f6f9a22610bbfd8b8daf0655f8267e6b42c. --- blender/arm/make_world.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index e47548ac..c97c4924 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -10,14 +10,14 @@ import arm.node_utils as node_utils import arm.utils import arm.write_probes as write_probes -# See make.py +callback = None shader_datas = [] def build(): worlds = [] for scene in bpy.data.scenes: - # Only export worlds from enabled scenes that are not yet exported + # Only export worlds from enabled scenes if scene.arm_export and scene.world is not None and scene.world not in worlds: worlds.append(scene.world) create_world_shaders(scene.world) @@ -90,6 +90,9 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): rpdat = arm.utils.get_rp() wrd = bpy.data.worlds['Arm'] + if callback is not None: + callback() + # Traverse world node tree is_parsed = False if world.node_tree is not None: From 73bbc3cf206fc338a0dafad910bffc22a2bee09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 21:36:03 +0200 Subject: [PATCH 197/230] Cleanup world shader generation --- blender/arm/make_world.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index c97c4924..7bddca95 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -311,11 +311,6 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): \tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5)); \treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta)); }''') - frag.write('vec3 n = normalize(normal);') - frag.write('float cos_theta = clamp(n.z, 0.0, 1.0);') - frag.write('float cos_gamma = dot(n, hosekSunDirection);') - frag.write('float gamma_val = acos(cos_gamma);') - frag.write('fragColor.rgb = Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;') world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] world.arm_envtex_turbidity = node.turbidity @@ -426,6 +421,9 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): def frag_write_main(world: bpy.types.World, frag: Shader): + if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvClouds' in world.world_defs: + frag.write('vec3 n = normalize(normal);') + if '_EnvCol' in world.world_defs: frag.write('fragColor.rgb = backgroundCol;') @@ -437,17 +435,20 @@ def frag_write_main(world: bpy.types.World, frag: Shader): frag.write('fragColor.rgb = texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength;') # Environment texture - # Check for _EnvSky too to prevent case when sky radiance is enabled + # Also check for _EnvSky to prevent case when sky radiance is enabled elif '_EnvTex' in world.world_defs and '_EnvSky' not in world.world_defs: - frag.write('vec3 n = normalize(normal);') frag.write('fragColor.rgb = texture(envmap, envMapEquirect(n)).rgb * envmapStrength;') if '_EnvLDR' in world.world_defs: frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));') + if '_EnvSky' in world.world_defs: + frag.write('float cos_theta = clamp(n.z, 0.0, 1.0);') + frag.write('float cos_gamma = dot(n, hosekSunDirection);') + frag.write('float gamma_val = acos(cos_gamma);') + frag.write('fragColor.rgb = Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;') + if '_EnvClouds' in world.world_defs: - if '_EnvCol' in world.world_defs: - frag.write('vec3 n = normalize(normal);') frag.write('if (n.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, n), clamp(n.z * 5.0, 0, 1));') if '_EnvLDR' in world.world_defs: From 0bb4bfd11a5776d46f27b0bd79689edcf3760c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 1 Jul 2020 21:36:15 +0200 Subject: [PATCH 198/230] Reimplement _EnvTransp --- blender/arm/make_world.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 7bddca95..dd988838 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -426,6 +426,8 @@ def frag_write_main(world: bpy.types.World, frag: Shader): if '_EnvCol' in world.world_defs: frag.write('fragColor.rgb = backgroundCol;') + if '_EnvTransp' in world.world_defs: + frag.write('return;') # Static background image elif '_EnvImg' in world.world_defs: From 8d07d6290e125602f5981249d89a256dc273cdba Mon Sep 17 00:00:00 2001 From: tong Date: Thu, 2 Jul 2020 18:37:29 +0200 Subject: [PATCH 199/230] Fix param name --- blender/arm/exporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 69360baf..c3270643 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2653,11 +2653,11 @@ class ArmoryExporter: # Wrong modifier type return - out_trait['parameters'] = [str(soft_type), str(bend), str(soft_mod.settings.mass), str(bobject.arm_soft_body_margin)] + out_trait['parameters'] = [str(soft_type), str(bend), str(modifier.settings.mass), str(bobject.arm_soft_body_margin)] o['traits'].append(out_trait) if soft_type == 0: - ArmoryExporter.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) + ArmoryExporter.add_hook_mod(o, bobject, '', modifier.settings.vertex_group_mass) @staticmethod def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): From 128622c96d265e41702906cbf861837982876583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 3 Jul 2020 01:08:50 +0200 Subject: [PATCH 200/230] Remove unused argument --- blender/arm/make_world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index dd988838..dc4272d8 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -59,7 +59,7 @@ def create_world_shaders(world: bpy.types.World): vec4 position = SMVP * vec4(pos, 1.0); gl_Position = vec4(position);''') - build_node_tree(world, frag, vert) + build_node_tree(world, frag) # TODO: Rework shader export so that it doesn't depend on materials # to prevent workaround code like this @@ -83,7 +83,7 @@ def create_world_shaders(world: bpy.types.World): shader_datas.append({'contexts': [shader_context.data], 'name': pass_name}) -def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader): +def build_node_tree(world: bpy.types.World, frag: Shader): """Generates the shader code for the given world.""" world_name = arm.utils.safestr(world.name) world.world_defs = '' From 9389c5f44d7a438a19f8354540ca98c6a5b2b36f Mon Sep 17 00:00:00 2001 From: tong Date: Sat, 4 Jul 2020 23:02:33 +0200 Subject: [PATCH 201/230] Fix unset object prop --- blender/arm/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 58a5808b..693146c2 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -536,6 +536,8 @@ def safestr(s: str) -> str: return ''.join([i if ord(i) < 128 else '_' for i in s]) def asset_name(bdata): + if bdata == None: + return None s = bdata.name # Append library name if linked if bdata.library is not None: From 313bbbfa8400504b1840e5cd4d1f96c1047b565f Mon Sep 17 00:00:00 2001 From: tong Date: Sun, 5 Jul 2020 13:25:10 +0200 Subject: [PATCH 202/230] Initialize var to null --- Sources/armory/system/Starter.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx index 39b85c33..f656e8ed 100644 --- a/Sources/armory/system/Starter.hx +++ b/Sources/armory/system/Starter.hx @@ -11,7 +11,7 @@ class Starter { public static function main(scene: String, mode: Int, resize: Bool, min: Bool, max: Bool, w: Int, h: Int, msaa: Int, vsync: Bool, getRenderPath: Void->iron.RenderPath) { - var tasks : Int; + var tasks : Int = null; function start() { if (tasks > 0) return; From 5be18db472b7221a24ad37fb3468127efcd978a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 5 Jul 2020 18:13:17 +0200 Subject: [PATCH 203/230] Reset wrd.world_defs for each export --- blender/arm/make_world.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index dc4272d8..7bbcf875 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -15,6 +15,7 @@ shader_datas = [] def build(): + bpy.data.worlds['Arm'].world_defs = '' worlds = [] for scene in bpy.data.scenes: # Only export worlds from enabled scenes From ae07dab862939fcec3a57e2537ac61aac118bd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 5 Jul 2020 23:30:55 +0200 Subject: [PATCH 204/230] Renderpaths: load new world shaders --- Sources/armory/renderpath/RenderPathDeferred.hx | 9 ++------- Sources/armory/renderpath/RenderPathForward.hx | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 90e243f6..1d0de0b7 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -1,6 +1,7 @@ package armory.renderpath; import iron.RenderPath; +import iron.Scene; class RenderPathDeferred { @@ -46,12 +47,6 @@ class RenderPathDeferred { } #end - #if (rp_background == "World") - { - path.loadShader("shader_datas/world_pass/world_pass"); - } - #end - #if (rp_translucency) { Inc.initTranslucency(); @@ -649,7 +644,7 @@ class RenderPathDeferred { #if (rp_background == "World") { path.setTarget("tex"); // Re-binds depth - path.drawSkydome("shader_datas/world_pass/world_pass"); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref); } #end diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index fc74be45..57237195 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -1,6 +1,7 @@ package armory.renderpath; import iron.RenderPath; +import iron.Scene; class RenderPathForward { @@ -35,7 +36,7 @@ class RenderPathForward { #if (rp_background == "World") { RenderPathCreator.setTargetMeshes(); - path.drawSkydome("shader_datas/world_pass/world_pass"); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref); } #end @@ -71,12 +72,6 @@ class RenderPathForward { } #end - #if (rp_background == "World") - { - path.loadShader("shader_datas/world_pass/world_pass"); - } - #end - #if rp_render_to_texture { path.createDepthBuffer("main", "DEPTH24"); From 8afc3c43e315d4508bad0499a3a54c74ea95f262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 16:58:52 +0200 Subject: [PATCH 205/230] Fix world export when another blend file was opened before --- blender/arm/make_world.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 7bbcf875..419b1367 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -15,8 +15,12 @@ shader_datas = [] def build(): + global shader_datas + bpy.data.worlds['Arm'].world_defs = '' worlds = [] + shader_datas = [] + for scene in bpy.data.scenes: # Only export worlds from enabled scenes if scene.arm_export and scene.world is not None and scene.world not in worlds: From 068fe0fadae1651e83b3d693c5b984000c7a0653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 17:10:29 +0200 Subject: [PATCH 206/230] Remove replaced world_pass --- Shaders/world_pass/world_pass.frag.glsl | 168 ------------------------ Shaders/world_pass/world_pass.json | 115 ---------------- Shaders/world_pass/world_pass.vert.glsl | 14 -- 3 files changed, 297 deletions(-) delete mode 100644 Shaders/world_pass/world_pass.frag.glsl delete mode 100644 Shaders/world_pass/world_pass.json delete mode 100644 Shaders/world_pass/world_pass.vert.glsl diff --git a/Shaders/world_pass/world_pass.frag.glsl b/Shaders/world_pass/world_pass.frag.glsl deleted file mode 100644 index ac290f25..00000000 --- a/Shaders/world_pass/world_pass.frag.glsl +++ /dev/null @@ -1,168 +0,0 @@ -#version 450 - -#include "compiled.inc" -#ifdef _EnvTex -#include "std/math.glsl" -#endif - -#ifdef _EnvCol - uniform vec3 backgroundCol; -#endif -#ifdef _EnvSky - uniform vec3 A; - uniform vec3 B; - uniform vec3 C; - uniform vec3 D; - uniform vec3 E; - uniform vec3 F; - uniform vec3 G; - uniform vec3 H; - uniform vec3 I; - uniform vec3 Z; - uniform vec3 hosekSunDirection; -#endif -#ifdef _EnvClouds - uniform sampler3D scloudsBase; - uniform sampler3D scloudsDetail; - uniform sampler2D scloudsMap; - uniform float time; -#endif -#ifdef _EnvTex - uniform sampler2D envmap; -#endif -#ifdef _EnvImg // Static background - uniform vec2 screenSize; - uniform sampler2D envmap; -#endif - -#ifdef _EnvStr -uniform float envmapStrength; -#endif - -in vec3 normal; -out vec4 fragColor; - -#ifdef _EnvSky -vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) { - vec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5)); - return (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta)); -} -#endif - -#ifdef _EnvClouds -// GPU PRO 7 - Real-time Volumetric Cloudscapes -// https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn -// https://github.com/sebh/TileableVolumeNoise -float remap(float old_val, float old_min, float old_max, float new_min, float new_max) { - return new_min + (((old_val - old_min) / (old_max - old_min)) * (new_max - new_min)); -} - -float getDensityHeightGradientForPoint(float height, float cloud_type) { - const vec4 stratusGrad = vec4(0.02f, 0.05f, 0.09f, 0.11f); - const vec4 stratocumulusGrad = vec4(0.02f, 0.2f, 0.48f, 0.625f); - const vec4 cumulusGrad = vec4(0.01f, 0.0625f, 0.78f, 1.0f); - float stratus = 1.0f - clamp(cloud_type * 2.0f, 0, 1); - float stratocumulus = 1.0f - abs(cloud_type - 0.5f) * 2.0f; - float cumulus = clamp(cloud_type - 0.5f, 0, 1) * 2.0f; - vec4 cloudGradient = stratusGrad * stratus + stratocumulusGrad * stratocumulus + cumulusGrad * cumulus; - return smoothstep(cloudGradient.x, cloudGradient.y, height) - smoothstep(cloudGradient.z, cloudGradient.w, height); -} - -float sampleCloudDensity(vec3 p) { - float cloud_base = textureLod(scloudsBase, p, 0).r * 40; // Base noise - vec3 weather_data = textureLod(scloudsMap, p.xy, 0).rgb; // Weather map - cloud_base *= getDensityHeightGradientForPoint(p.z, weather_data.b); // Cloud type - cloud_base = remap(cloud_base, weather_data.r, 1.0, 0.0, 1.0); // Coverage - cloud_base *= weather_data.r; - float cloud_detail = textureLod(scloudsDetail, p, 0).r * 2; // Detail noise - float cloud_detail_mod = mix(cloud_detail, 1.0 - cloud_detail, clamp(p.z * 10.0, 0, 1)); - cloud_base = remap(cloud_base, cloud_detail_mod * 0.2, 1.0, 0.0, 1.0); - return cloud_base; -} - -float cloudRadiance(vec3 p, vec3 dir){ - #ifdef _EnvSky - vec3 sun_dir = hosekSunDirection; - #else - vec3 sun_dir = vec3(0, 0, -1); - #endif - const int steps = 8; - float step_size = 0.5 / float(steps); - float d = 0.0; - p += sun_dir * step_size; - for(int i = 0; i < steps; ++i) { - d += sampleCloudDensity(p + sun_dir * float(i) * step_size); - } - return 1.0 - d; -} - -vec3 traceClouds(vec3 sky, vec3 dir) { - const float step_size = 0.5 / float(cloudsSteps); - float T = 1.0; - float C = 0.0; - vec2 uv = dir.xy / dir.z * 0.4 * cloudsLower + cloudsWind * time * 0.02; - - for (int i = 0; i < cloudsSteps; ++i) { - float h = float(i) / float(cloudsSteps); - vec3 p = vec3(uv * 0.04, h); - float d = sampleCloudDensity(p); - - if (d > 0) { - // float radiance = cloudRadiance(p, dir); - C += T * exp(h) * d * step_size * 0.6 * cloudsPrecipitation; - T *= exp(-d * step_size); - if (T < 0.01) break; - } - uv += (dir.xy / dir.z) * step_size * cloudsUpper; - } - - return vec3(C) + sky * T; -} -#endif // _EnvClouds - -void main() { - -#ifdef _EnvCol - fragColor.rgb = backgroundCol; -#ifdef _EnvTransp - return; -#endif -#ifdef _EnvClouds - vec3 n = normalize(normal); -#endif -#endif - -#ifndef _EnvSky // Prevent case when sky radiance is enabled -#ifdef _EnvTex - vec3 n = normalize(normal); - fragColor.rgb = texture(envmap, envMapEquirect(n)).rgb * envmapStrength; - #ifdef _EnvLDR - fragColor.rgb = pow(fragColor.rgb, vec3(2.2)); - #endif -#endif -#endif - -#ifdef _EnvImg // Static background - // Will have to get rid of gl_FragCoord, pass tc from VS - vec2 texco = gl_FragCoord.xy / screenSize; - fragColor.rgb = texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength; -#endif - -#ifdef _EnvSky - vec3 n = normalize(normal); - float cos_theta = clamp(n.z, 0.0, 1.0); - float cos_gamma = dot(n, hosekSunDirection); - float gamma_val = acos(cos_gamma); - fragColor.rgb = Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength; -#endif - -#ifdef _EnvClouds - if (n.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, n), clamp(n.z * 5.0, 0, 1)); -#endif - -#ifdef _LDR - fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); -#endif - - fragColor.a = 0.0; // Mark as non-opaque -} diff --git a/Shaders/world_pass/world_pass.json b/Shaders/world_pass/world_pass.json deleted file mode 100644 index 4be4597a..00000000 --- a/Shaders/world_pass/world_pass.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "contexts": [ - { - "name": "world_pass", - "depth_write": false, - "compare_mode": "less", - "cull_mode": "clockwise", - "links": [ - { - "name": "SMVP", - "link": "_skydomeMatrix" - }, - { - "name": "backgroundCol", - "link": "_backgroundCol", - "ifdef": ["_EnvCol"] - }, - { - "name": "A", - "link": "_hosekA", - "ifdef": ["_EnvSky"] - }, - { - "name": "B", - "link": "_hosekB", - "ifdef": ["_EnvSky"] - }, - { - "name": "C", - "link": "_hosekC", - "ifdef": ["_EnvSky"] - }, - { - "name": "D", - "link": "_hosekD", - "ifdef": ["_EnvSky"] - }, - { - "name": "E", - "link": "_hosekE", - "ifdef": ["_EnvSky"] - }, - { - "name": "F", - "link": "_hosekF", - "ifdef": ["_EnvSky"] - }, - { - "name": "G", - "link": "_hosekG", - "ifdef": ["_EnvSky"] - }, - { - "name": "H", - "link": "_hosekH", - "ifdef": ["_EnvSky"] - }, - { - "name": "I", - "link": "_hosekI", - "ifdef": ["_EnvSky"] - }, - { - "name": "Z", - "link": "_hosekZ", - "ifdef": ["_EnvSky"] - }, - { - "name": "hosekSunDirection", - "link": "_hosekSunDirection", - "ifdef": ["_EnvSky"] - }, - { - "name": "time", - "link": "_time", - "ifdef": ["_EnvClouds"] - }, - { - "name": "scloudsBase", - "link": "$clouds_base.raw", - "ifdef": ["_EnvClouds"] - }, - { - "name": "scloudsDetail", - "link": "$clouds_detail.raw", - "ifdef": ["_EnvClouds"] - }, - { - "name": "scloudsMap", - "link": "$clouds_map.png", - "ifdef": ["_EnvClouds"] - }, - { - "name": "screenSize", - "link": "_screenSize", - "ifdef": ["_EnvImg"] - }, - { - "name": "envmap", - "link": "_envmap", - "ifdef": ["_EnvTex", "_EnvImg"] - }, - { - "name": "envmapStrength", - "link": "_envmapStrength", - "ifdef": ["_EnvStr"] - } - ], - "texture_params": [], - "vertex_shader": "world_pass.vert.glsl", - "fragment_shader": "world_pass.frag.glsl", - "color_attachments": ["_HDR"] - } - ] -} diff --git a/Shaders/world_pass/world_pass.vert.glsl b/Shaders/world_pass/world_pass.vert.glsl deleted file mode 100644 index bd27b1b3..00000000 --- a/Shaders/world_pass/world_pass.vert.glsl +++ /dev/null @@ -1,14 +0,0 @@ -#version 450 - -in vec3 pos; -in vec3 nor; - -out vec3 normal; - -uniform mat4 SMVP; - -void main() { - normal = nor; - vec4 position = SMVP * vec4(pos, 1.0); - gl_Position = vec4(position); -} From 60b4f5b42d7c3ee2647795cd27338a9114ca7b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 18:04:18 +0200 Subject: [PATCH 207/230] Cleanup probe export --- blender/arm/write_probes.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 94cb904d..4f8575c2 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -260,32 +260,31 @@ def write_sky_irradiance(base_name): for i in range(0, len(irradiance_floats)): irradiance_floats[i] /= 2 - envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') if not os.path.exists(envpath): os.makedirs(envpath) - output_file = envpath + '/' + base_name + '_irradiance' + output_file = os.path.join(envpath, base_name + '_irradiance') - sh_json = {} - sh_json['irradiance'] = irradiance_floats + sh_json = {'irradiance': irradiance_floats} arm.utils.write_arm(output_file + '.arm', sh_json) assets.add(output_file + '.arm') def write_color_irradiance(base_name, col): - # Constant color - irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13] # Adjust to Cycles + """Constant color irradiance""" + # Adjust to Cycles + irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13] for i in range(0, 24): irradiance_floats.append(0.0) - envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') if not os.path.exists(envpath): os.makedirs(envpath) - output_file = envpath + '/' + base_name + '_irradiance' + output_file = os.path.join(envpath, base_name + '_irradiance') - sh_json = {} - sh_json['irradiance'] = irradiance_floats + sh_json = {'irradiance': irradiance_floats} arm.utils.write_arm(output_file + '.arm', sh_json) assets.add(output_file + '.arm') From 01e3395d186a0a34006461ed8c3b376932f6f789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 18:04:35 +0200 Subject: [PATCH 208/230] Implement irradiance for static background images --- blender/arm/make_world.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 419b1367..2816e475 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -119,10 +119,10 @@ def build_node_tree(world: bpy.types.World, frag: Shader): if '_EnvImg' not in world.world_defs: world.world_defs += '_EnvCol' frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') - # Irradiance json file name - # Todo: this breaks static image backgrounds - # world.arm_envtex_name = world_name - world.arm_envtex_irr_name = world_name + + # Irradiance json file name + world.arm_envtex_name = world_name + world.arm_envtex_irr_name = world_name write_probes.write_color_irradiance(world_name, world.arm_envtex_color) # film_transparent @@ -292,7 +292,24 @@ def parse_color(world: bpy.types.World, node: bpy.types.Node, frag: Shader): # Reference image name tex_file = arm.utils.extract_filename(image.filepath) + base = tex_file.rsplit('.', 1) + ext = base[1].lower() + + if ext == 'hdr': + target_format = 'HDR' + else: + target_format = 'JPEG' + + # Generate prefiltered envmaps world.arm_envtex_name = tex_file + world.arm_envtex_irr_name = tex_file.rsplit('.', 1)[0] + + disable_hdr = target_format == 'JPEG' + + mip_count = world.arm_envtex_num_mips + mip_count = write_probes.write_probes(filepath, disable_hdr, mip_count, arm_radiance=rpdat.arm_radiance) + + world.arm_envtex_num_mips = mip_count # Append sky define elif node.type == 'TEX_SKY': From b994b1d575832d7fdc51b9c2f7eea04bbfe1d3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 18:14:15 +0200 Subject: [PATCH 209/230] Remove cloud constants from compiled.inc --- blender/arm/write_data.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index da5e6da5..c13d321f 100755 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -465,15 +465,7 @@ const vec2 shadowmapSize = vec2(""" + str(shadowmap_size) + """, """ + str(shado const float shadowmapCubePcfSize = """ + str((round(rpdat.arm_pcfsize * 100) / 100) / 1000) + """; const int shadowmapCascades = """ + str(rpdat.rp_shadowmap_cascades) + """; """) - if rpdat.arm_clouds: - f.write( -"""const float cloudsLower = """ + str(round(rpdat.arm_clouds_lower * 100) / 100) + """; -const float cloudsUpper = """ + str(round(rpdat.arm_clouds_upper * 100) / 100) + """; -const vec2 cloudsWind = vec2(""" + str(round(rpdat.arm_clouds_wind[0] * 100) / 100) + """, """ + str(round(rpdat.arm_clouds_wind[1] * 100) / 100) + """); -const float cloudsPrecipitation = """ + str(round(rpdat.arm_clouds_precipitation * 100) / 100) + """; -const float cloudsSecondary = """ + str(round(rpdat.arm_clouds_secondary * 100) / 100) + """; -const int cloudsSteps = """ + str(rpdat.arm_clouds_steps) + """; -""") + if rpdat.rp_water: f.write( """const float waterLevel = """ + str(round(rpdat.arm_water_level * 100) / 100) + """; From d06f07f2cf56fca4fe893465b38ea3e1ab25a72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 18:14:58 +0200 Subject: [PATCH 210/230] Update renderpath skydome drawing to Iron changes --- Sources/armory/renderpath/RenderPathDeferred.hx | 2 +- Sources/armory/renderpath/RenderPathForward.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 1d0de0b7..591c2477 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -644,7 +644,7 @@ class RenderPathDeferred { #if (rp_background == "World") { path.setTarget("tex"); // Re-binds depth - path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); } #end diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index 57237195..c151678a 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -36,7 +36,7 @@ class RenderPathForward { #if (rp_background == "World") { RenderPathCreator.setTargetMeshes(); - path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); } #end From 22371ec37ea255b74d537a8ba20d747c10ebc4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 18:16:58 +0200 Subject: [PATCH 211/230] Remove unused import --- blender/arm/exporter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b57bf6b0..cb6ac304 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -30,7 +30,6 @@ import arm.material.cycles as cycles import arm.material.make as make_material import arm.material.mat_batch as mat_batch import arm.utils -from arm import make_world @unique From 27b3aa5f49d97c37981cfe1aa97a56fc5063ce6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 6 Jul 2020 22:53:31 +0200 Subject: [PATCH 212/230] Fix renderpaths for scenes without a world --- Sources/armory/renderpath/RenderPathDeferred.hx | 6 ++++-- Sources/armory/renderpath/RenderPathForward.hx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 591c2477..8f263349 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -643,8 +643,10 @@ class RenderPathDeferred { #if (rp_background == "World") { - path.setTarget("tex"); // Re-binds depth - path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + if (Scene.active.raw.world_ref != null) { + path.setTarget("tex"); // Re-binds depth + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + } } #end diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index c151678a..8c139815 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -35,8 +35,10 @@ class RenderPathForward { #if (rp_background == "World") { - RenderPathCreator.setTargetMeshes(); - path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + if (Scene.active.raw.world_ref != null) { + RenderPathCreator.setTargetMeshes(); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + } } #end From 1f0a27bae0891063051f20a00c4d9f82e51851a7 Mon Sep 17 00:00:00 2001 From: tong Date: Mon, 6 Jul 2020 23:44:04 +0200 Subject: [PATCH 213/230] Initialize 0 --- Sources/armory/system/Starter.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx index f656e8ed..0f019c48 100644 --- a/Sources/armory/system/Starter.hx +++ b/Sources/armory/system/Starter.hx @@ -11,7 +11,7 @@ class Starter { public static function main(scene: String, mode: Int, resize: Bool, min: Bool, max: Bool, w: Int, h: Int, msaa: Int, vsync: Bool, getRenderPath: Void->iron.RenderPath) { - var tasks : Int = null; + var tasks = 0; function start() { if (tasks > 0) return; From 29a0b5632b7d7b68eb0e37c662fd66c524f29b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 7 Jul 2020 17:03:07 +0200 Subject: [PATCH 214/230] Print compositor flags on export --- blender/arm/make.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index 4cbe3347..a7227902 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -155,9 +155,10 @@ def export_data(fp, sdk_path): cdefs = arm.utils.def_strings_to_array(wrd.compo_defs) if wrd.arm_verbose_output: - print('Exported modules: ' + str(modules)) - print('Shader flags: ' + str(defs)) - print('Khafile flags: ' + str(assets.khafile_defs)) + print('Exported modules:', modules) + print('Shader flags:', defs) + print('Compositor flags:', cdefs) + print('Khafile flags:', assets.khafile_defs) # Render path is configurable at runtime has_config = wrd.arm_write_config or os.path.exists(arm.utils.get_fp() + '/Bundled/config.arm') From 156030075ae9ca2b0f110737cb23391495e44ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 7 Jul 2020 21:06:24 +0200 Subject: [PATCH 215/230] Fix SetSceneNode when using json export --- Sources/armory/logicnode/SetSceneNode.hx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/armory/logicnode/SetSceneNode.hx b/Sources/armory/logicnode/SetSceneNode.hx index 3c9a3d27..8068e1eb 100644 --- a/Sources/armory/logicnode/SetSceneNode.hx +++ b/Sources/armory/logicnode/SetSceneNode.hx @@ -13,6 +13,10 @@ class SetSceneNode extends LogicNode { override function run(from: Int) { var sceneName: String = inputs[1].get(); + #if arm_json + sceneName += ".json"; + #end + iron.Scene.setActive(sceneName, function(o: iron.object.Object) { root = o; runOutput(0); From 0c9752a89ab2e2bdd76961ee073a04e5e12ec8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 7 Jul 2020 22:42:54 +0200 Subject: [PATCH 216/230] Fix logging colors on Windows See: - https://docs.microsoft.com/en-us/windows/console/setconsolemode - https://docs.microsoft.com/en-us/windows/console/getstdhandle --- blender/arm/log.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/blender/arm/log.py b/blender/arm/log.py index ea2fdf8f..0c468fe6 100644 --- a/blender/arm/log.py +++ b/blender/arm/log.py @@ -7,6 +7,15 @@ ERROR = 31 if platform.system() == "Windows": HAS_COLOR_SUPPORT = platform.release() == "10" + + if HAS_COLOR_SUPPORT: + # Enable ANSI codes. Otherwise, the ANSI sequences might not be + # evaluated correctly for the first colored print statement. + import ctypes + kernel32 = ctypes.windll.kernel32 + # -11: stdout, 7 (0b111): ENABLE_PROCESSED_OUTPUT, ENABLE_WRAP_AT_EOL_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING + # see https://docs.microsoft.com/en-us/windows/console/setconsolemode + kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) else: HAS_COLOR_SUPPORT = True From a3223ee4558aa0bd2ade48378198df10d15c67df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 7 Jul 2020 22:43:20 +0200 Subject: [PATCH 217/230] Cleanup log.py --- blender/arm/log.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/blender/arm/log.py b/blender/arm/log.py index 0c468fe6..2623fc2e 100644 --- a/blender/arm/log.py +++ b/blender/arm/log.py @@ -31,22 +31,22 @@ def clear(clear_warnings=False): def format_text(text): return (text[:80] + '..') if len(text) > 80 else text # Limit str size -def log(text,color=None): +def log(text, color=None): if HAS_COLOR_SUPPORT and color is not None: csi = '\033[' - text = csi + str(color) + 'm' + text + csi + '0m'; + text = csi + str(color) + 'm' + text + csi + '0m' print(text) def debug(text): - log(text,DEBUG) + log(text, DEBUG) def info(text): global info_text - log(text,INFO) + log(text, INFO) info_text = format_text(text) def print_warn(text): - log('Warning: ' + text,WARN) + log('Warning: ' + text, WARN) def warn(text): global num_warnings @@ -54,4 +54,4 @@ def warn(text): print_warn(text) def error(text): - log('ERROR: ' + text,ERROR) + log('ERROR: ' + text, ERROR) From 4ca340024a15b48be3f4e2e2758bd941f25f580c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Tue, 7 Jul 2020 22:43:51 +0200 Subject: [PATCH 218/230] Fix logger usage --- blender/arm/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 693146c2..398e3d58 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -598,7 +598,7 @@ def get_cascade_size(rpdat): def check_saved(self): if bpy.data.filepath == "": msg = "Save blend file first" - self.report({"ERROR"}, msg) if self != None else log.print_info(msg) + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False return True @@ -613,18 +613,18 @@ def check_path(s): def check_sdkpath(self): s = get_sdk_path() - if check_path(s) == False: - msg = "SDK path '{0}' contains special characters. Please move SDK to different path for now.".format(s) - self.report({"ERROR"}, msg) if self != None else log.print_info(msg) + if not check_path(s): + msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now." + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False else: return True def check_projectpath(self): s = get_fp() - if check_path(s) == False: - msg = "Project path '{0}' contains special characters, build process may fail.".format(s) - self.report({"ERROR"}, msg) if self != None else log.print_info(msg) + if not check_path(s): + msg = f"Project path '{s}' contains special characters, build process may fail." + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False else: return True From e7d28ba1c69d3985806f080bd5f876bbb0ff4ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 8 Jul 2020 13:48:19 +0200 Subject: [PATCH 219/230] log.py: don't override user console settings --- blender/arm/log.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/blender/arm/log.py b/blender/arm/log.py index 2623fc2e..4f7b5256 100644 --- a/blender/arm/log.py +++ b/blender/arm/log.py @@ -13,9 +13,17 @@ if platform.system() == "Windows": # evaluated correctly for the first colored print statement. import ctypes kernel32 = ctypes.windll.kernel32 - # -11: stdout, 7 (0b111): ENABLE_PROCESSED_OUTPUT, ENABLE_WRAP_AT_EOL_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING + + # -11: stdout + handle_out = kernel32.GetStdHandle(-11) + + console_mode = ctypes.c_long() + kernel32.GetConsoleMode(handle_out, ctypes.byref(console_mode)) + + # 0b100: ENABLE_VIRTUAL_TERMINAL_PROCESSING, enables ANSI codes # see https://docs.microsoft.com/en-us/windows/console/setconsolemode - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) + console_mode.value |= 0b100 + kernel32.SetConsoleMode(handle_out, console_mode) else: HAS_COLOR_SUPPORT = True From c666b965b98c1141b07a8b3f6152b25af79fa6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 9 Jul 2020 18:46:42 +0200 Subject: [PATCH 220/230] Fix write_probes() on Windows Thanks to @Simonrazer for reporting this --- blender/arm/write_probes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index a893c831..11529394 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -118,7 +118,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True cpu_count = multiprocessing.cpu_count() if arm.utils.get_os() == 'win': - subprocess.call([ \ + cmd = [ cmft_path, '--input', scaled_file, '--filter', 'radiance', @@ -142,12 +142,13 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True '--outputGammaDenominator', '1.0', '--outputNum', '1', '--output0', output_file_rad, - '--output0params', 'hdr,rgbe,latlong']) + '--output0params', 'hdr,rgbe,latlong' + ] if wrd.arm_verbose_output: print(cmd) else: - cmd.append(' --silent') - subprocess.call([cmd]) + cmd.append('--silent') + subprocess.call(cmd) else: cmd = cmft_path + \ ' --input "' + scaled_file + '"' + \ From 8e60ff4851d8b4f290d35bfa42099ddb2d30157b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Jul 2020 00:17:29 +0200 Subject: [PATCH 221/230] Fix OnContactNode --- Sources/armory/logicnode/OnContactNode.hx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/armory/logicnode/OnContactNode.hx b/Sources/armory/logicnode/OnContactNode.hx index 81762590..701af2af 100644 --- a/Sources/armory/logicnode/OnContactNode.hx +++ b/Sources/armory/logicnode/OnContactNode.hx @@ -26,13 +26,15 @@ class OnContactNode extends LogicNode { #if arm_physics var physics = armory.trait.physics.PhysicsWorld.active; var rb1 = object1.getTrait(RigidBody); - var rbs = physics.getContacts(rb1); - if (rb1 != null && rbs != null) { - var rb2 = object2.getTrait(RigidBody); - for (rb in rbs) { - if (rb == rb2) { - contact = true; - break; + if (rb1 != null) { + var rbs = physics.getContacts(rb1); + if (rbs != null) { + var rb2 = object2.getTrait(RigidBody); + for (rb in rbs) { + if (rb == rb2) { + contact = true; + break; + } } } } From 26bdba90929bb37f397fce7923fb9bc79b5cf697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Jul 2020 15:36:18 +0200 Subject: [PATCH 222/230] Fix fetching scripts from sub-packages --- blender/arm/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 398e3d58..e6520e29 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -398,17 +398,18 @@ def fetch_script_names(): wrd = bpy.data.worlds['Arm'] # Sources wrd.arm_scripts_list.clear() - sources_path = get_fp() + '/Sources/' + safestr(wrd.arm_project_package) + sources_path = os.path.join(get_fp(), 'Sources', safestr(wrd.arm_project_package)) if os.path.isdir(sources_path): os.chdir(sources_path) # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python for file in glob.glob('**/*.hx', recursive=True): mod = file.rsplit('.')[0] + mod = mod.replace('\\', '/') mod_parts = mod.rsplit('/') - if re.match('^[A-Z][A-Za-z0-9_]*$',mod_parts[-1]): + if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]): wrd.arm_scripts_list.add().name = mod.replace(os.sep, '.') fetch_script_props(file) - + # Canvas wrd.arm_canvas_list.clear() canvas_path = get_fp() + '/Bundled/canvas' From d18a9917aa0630f9a7393e432ead3cc36c2357ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 12 Jul 2020 16:08:52 +0200 Subject: [PATCH 223/230] Fix sub-package export --- blender/arm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index e6520e29..2f1e54b5 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -407,7 +407,7 @@ def fetch_script_names(): mod = mod.replace('\\', '/') mod_parts = mod.rsplit('/') if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]): - wrd.arm_scripts_list.add().name = mod.replace(os.sep, '.') + wrd.arm_scripts_list.add().name = mod.replace('/', '.') fetch_script_props(file) # Canvas From 9a47d7759415a1d6a597b4ebd63ed2bde47775c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 13 Jul 2020 23:20:58 +0200 Subject: [PATCH 224/230] Implement receive shadow setting for mobile path + all light types --- Shaders/std/light.glsl | 118 ++++++++++++++------------- blender/arm/material/make_cluster.py | 9 +- blender/arm/material/make_mesh.py | 17 +++- 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/Shaders/std/light.glsl b/Shaders/std/light.glsl index ea3871fc..d5172808 100644 --- a/Shaders/std/light.glsl +++ b/Shaders/std/light.glsl @@ -70,7 +70,7 @@ uniform sampler2D sltcMag; vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, const vec3 lp, const vec3 lightCol, const vec3 albedo, const float rough, const float spec, const vec3 f0 #ifdef _ShadowMap - , int index, float bias + , int index, float bias, bool receiveShadow #endif #ifdef _Spot , bool isSpot, float spotA, float spotB, vec3 spotDir @@ -130,28 +130,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #ifdef _LTC #ifdef _ShadowMap - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; #endif @@ -164,28 +166,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co direct *= smoothstep(spotB, spotA, spotEffect); } #ifdef _ShadowMap - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; } @@ -196,17 +200,19 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #endif #ifdef _ShadowMap - #ifdef _SinglePoint - #ifndef _Spot - direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - #endif - #endif - #ifdef _Clusters - if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); - else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); - else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); - #endif + if (receiveShadow) { + #ifdef _SinglePoint + #ifndef _Spot + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #endif + #ifdef _Clusters + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + } #endif return direct; diff --git a/blender/arm/material/make_cluster.py b/blender/arm/material/make_cluster.py index 9428f996..6864386d 100644 --- a/blender/arm/material/make_cluster.py +++ b/blender/arm/material/make_cluster.py @@ -3,13 +3,14 @@ import bpy def write(vert, frag): wrd = bpy.data.worlds['Arm'] is_shadows = '_ShadowMap' in wrd.world_defs - + frag.add_include('std/clusters.glsl') frag.add_uniform('vec2 cameraProj', link='_cameraPlaneProj') frag.add_uniform('vec2 cameraPlane', link='_cameraPlane') frag.add_uniform('vec4 lightsArray[maxLights * 2]', link='_lightsArray') frag.add_uniform('sampler2D clustersData', link='_clustersData') if is_shadows: + frag.add_uniform('bool receiveShadow') frag.add_uniform('vec2 lightProj', link='_lightPlaneProj', included=True) frag.add_uniform('samplerCubeShadow shadowMapPoint[4]', included=True) vert.add_out('vec4 wvpposition') @@ -36,7 +37,7 @@ def write(vert, frag): frag.write('for (int i = 0; i < min(numLights, maxLightsCluster); i++) {') frag.write('int li = int(texelFetch(clustersData, ivec2(clusterI, i + 1), 0).r * 255);') - + frag.write('direct += sampleLight(') frag.write(' wposition,') frag.write(' n,') @@ -49,7 +50,7 @@ def write(vert, frag): frag.write(' specular,') frag.write(' f0') if is_shadows: - frag.write(' , li, lightsArray[li * 2].w') # bias + frag.write(' , li, lightsArray[li * 2].w, receiveShadow') # bias if '_Spot' in wrd.world_defs: frag.write(' , li > numPoints - 1') frag.write(' , lightsArray[li * 2 + 1].w') # cutoff @@ -59,4 +60,4 @@ def write(vert, frag): frag.write(' , voxels, voxpos') frag.write(');') - frag.write('}') # for numLights \ No newline at end of file + frag.write('}') # for numLights diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 5965e850..72a93975 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -366,8 +366,11 @@ def make_forward_mobile(con_mesh): vert.add_out('vec4 lightPosition') vert.add_uniform('mat4 LWVP', '_biasLightWorldViewProjectionMatrix') vert.write('lightPosition = LWVP * spos;') + frag.add_uniform('bool receiveShadow') frag.add_uniform('sampler2DShadow shadowMap') frag.add_uniform('float shadowsBias', '_sunShadowsBias') + + frag.write('if (receiveShadow) {') if '_CSM' in wrd.world_defs: frag.add_include('std/shadows.glsl') frag.add_uniform('vec4 casData[shadowmapCascades * 4 + 4]', '_cascadeData', included=True) @@ -381,6 +384,7 @@ def make_forward_mobile(con_mesh): else: frag.write(' svisibility = texture(shadowMap, vec3(lPos.xy, lPos.z - shadowsBias)).r;') frag.write('}') + frag.write('}') # receiveShadow frag.write('direct += basecol * sdotNL * sunCol * svisibility;') if '_SinglePoint' in wrd.world_defs: @@ -394,8 +398,11 @@ def make_forward_mobile(con_mesh): frag.write('vec3 l = normalize(ld);') frag.write('float dotNL = max(dot(n, l), 0.0);') if is_shadows: + frag.add_uniform('bool receiveShadow') frag.add_uniform('float pointBias', link='_pointShadowsBias') frag.add_include('std/shadows.glsl') + + frag.write('if (receiveShadow) {') if '_Spot' in wrd.world_defs: vert.add_out('vec4 spotPosition') vert.add_uniform('mat4 LWVPSpot0', link='_biasLightWorldViewProjectionMatrixSpot0') @@ -420,6 +427,7 @@ def make_forward_mobile(con_mesh): frag.write('visibility = float(texture(shadowMapPoint[0], vec3(-l + n * pointBias * 20)).r > compare);') else: frag.write('visibility = texture(shadowMapPoint[0], vec4(-l + n * pointBias * 20, compare)).r;') + frag.write('}') # receiveShadow frag.write('direct += basecol * dotNL * pointCol * attenuate(distance(wposition, pointPos)) * visibility;') @@ -593,7 +601,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): float dotNV = max(dot(n, vVec), 0.0); """) - sh = tese if tese != None else vert + sh = tese if tese is not None else vert sh.add_out('vec3 eyeDir') sh.add_uniform('vec3 eye', '_cameraPosition') sh.write('eyeDir = eye - wposition;') @@ -645,7 +653,6 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.write('indirect *= vec3(1.0 - traceAO(voxpos, n, voxels));') frag.write('vec3 direct = vec3(0.0);') - frag.add_uniform('bool receiveShadow') if '_Sun' in wrd.world_defs: frag.add_uniform('vec3 sunCol', '_sunColor') @@ -656,6 +663,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.write('float sdotNH = dot(n, sh);') frag.write('float sdotVH = dot(vVec, sh);') if is_shadows: + frag.add_uniform('bool receiveShadow') frag.add_uniform('sampler2DShadow shadowMap') frag.add_uniform('float shadowsBias', '_sunShadowsBias') frag.write('if (receiveShadow) {') @@ -665,7 +673,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.add_uniform('vec3 eye', '_cameraPosition') frag.write('svisibility = shadowTestCascade(shadowMap, eye, wposition + n * shadowsBias * 10, shadowsBias);') else: - if tese != None: + if tese is not None: tese.add_out('vec4 lightPosition') tese.add_uniform('mat4 LVP', '_biasLightViewProjectionMatrix') tese.write('lightPosition = LVP * vec4(wposition, 1.0);') @@ -694,6 +702,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.add_uniform('vec3 spotDir', link='_spotDirection') frag.add_uniform('vec2 spotData', link='_spotData') if is_shadows: + frag.add_uniform('bool receiveShadow') frag.add_uniform('float pointBias', link='_pointShadowsBias') if '_Spot' in wrd.world_defs: # Skip world matrix, already in world-space @@ -705,7 +714,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag.write('direct += sampleLight(') frag.write(' wposition, n, vVec, dotNV, pointPos, pointCol, albedo, roughness, specular, f0') if is_shadows: - frag.write(' , 0, pointBias') + frag.write(' , 0, pointBias, receiveShadow') if '_Spot' in wrd.world_defs: frag.write(' , true, spotData.x, spotData.y, spotDir') if '_VoxelShadow' in wrd.world_defs and '_VoxelAOvar' in wrd.world_defs: From 3654d34997b53b730e92f60f85218ed4843d4610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 13 Jul 2020 23:28:43 +0200 Subject: [PATCH 225/230] Update more shaders for receive shadow option --- .../deferred_light/deferred_light.frag.glsl | 4 +- .../deferred_light.frag.glsl | 4 +- Shaders/std/light_mobile.glsl | 68 ++++++++++--------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index 1f9a024f..68e3fe0e 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -346,7 +346,7 @@ void main() { fragColor.rgb += sampleLight( p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0 #ifdef _ShadowMap - , 0, pointBias + , 0, pointBias, true #endif #ifdef _Spot , true, spotData.x, spotData.y, spotDir @@ -400,7 +400,7 @@ void main() { occspec.y, f0 #ifdef _ShadowMap - , li, lightsArray[li * 2].w // bias + , li, lightsArray[li * 2].w, true // bias #endif #ifdef _Spot , li > numPoints - 1 diff --git a/Shaders/deferred_light_mobile/deferred_light.frag.glsl b/Shaders/deferred_light_mobile/deferred_light.frag.glsl index 034f2bec..698a8400 100644 --- a/Shaders/deferred_light_mobile/deferred_light.frag.glsl +++ b/Shaders/deferred_light_mobile/deferred_light.frag.glsl @@ -182,7 +182,7 @@ void main() { fragColor.rgb += sampleLight( p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0 #ifdef _ShadowMap - , 0, pointBias + , 0, pointBias, true #endif #ifdef _Spot , true, spotData.x, spotData.y, spotDir @@ -218,7 +218,7 @@ void main() { occspec.y, f0 #ifdef _ShadowMap - , li, lightsArray[li * 2].w // bias + , li, lightsArray[li * 2].w, true // bias #endif #ifdef _Spot , li > numPoints - 1 diff --git a/Shaders/std/light_mobile.glsl b/Shaders/std/light_mobile.glsl index 4696d370..65b5cceb 100644 --- a/Shaders/std/light_mobile.glsl +++ b/Shaders/std/light_mobile.glsl @@ -33,7 +33,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, const vec3 lp, const vec3 lightCol, const vec3 albedo, const float rough, const float spec, const vec3 f0 #ifdef _ShadowMap - , int index, float bias + , int index, float bias, bool receiveShadow #endif #ifdef _Spot , bool isSpot, float spotA, float spotB, vec3 spotDir @@ -60,28 +60,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co direct *= smoothstep(spotB, spotA, spotEffect); } #ifdef _ShadowMap - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; } @@ -89,15 +91,17 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #ifdef _ShadowMap #ifndef _Spot - #ifdef _SinglePoint - direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - #endif - #ifdef _Clusters - if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); - else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); - else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); - #endif + if (receiveShadow) { + #ifdef _SinglePoint + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #ifdef _Clusters + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + } #endif #endif From dac91efeb114a720bd7d85f189ebb08d06a06eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 13 Jul 2020 23:36:49 +0200 Subject: [PATCH 226/230] Fix indentation (spaces -> tabs) --- Shaders/std/light.glsl | 122 +++++++++++++++++----------------- Shaders/std/light_mobile.glsl | 70 +++++++++---------- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/Shaders/std/light.glsl b/Shaders/std/light.glsl index d5172808..293dad04 100644 --- a/Shaders/std/light.glsl +++ b/Shaders/std/light.glsl @@ -130,30 +130,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #ifdef _LTC #ifdef _ShadowMap - if (receiveShadow) { - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif - } + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; #endif @@ -166,30 +166,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co direct *= smoothstep(spotB, spotA, spotEffect); } #ifdef _ShadowMap - if (receiveShadow) { - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif - } + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; } @@ -200,19 +200,19 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #endif #ifdef _ShadowMap - if (receiveShadow) { - #ifdef _SinglePoint - #ifndef _Spot - direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - #endif - #endif - #ifdef _Clusters - if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); - else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); - else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); - #endif - } + if (receiveShadow) { + #ifdef _SinglePoint + #ifndef _Spot + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #endif + #ifdef _Clusters + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + } #endif return direct; diff --git a/Shaders/std/light_mobile.glsl b/Shaders/std/light_mobile.glsl index 65b5cceb..86558dd7 100644 --- a/Shaders/std/light_mobile.glsl +++ b/Shaders/std/light_mobile.glsl @@ -60,30 +60,30 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co direct *= smoothstep(spotB, spotA, spotEffect); } #ifdef _ShadowMap - if (receiveShadow) { - #ifdef _SinglePoint - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - #endif - #ifdef _Clusters - if (index == 0) { - vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); - } - else if (index == 1) { - vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); - } - else if (index == 2) { - vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); - } - else if (index == 3) { - vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); - direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); - } - #endif - } + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot0 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot1 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot2 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot3 * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } #endif return direct; } @@ -91,17 +91,17 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #ifdef _ShadowMap #ifndef _Spot - if (receiveShadow) { - #ifdef _SinglePoint - direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - #endif - #ifdef _Clusters - if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); - else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); - else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); - else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); - #endif - } + if (receiveShadow) { + #ifdef _SinglePoint + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #ifdef _Clusters + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + } #endif #endif From fbe144deccaebc5a897ef7fa971809fd01d5c5ec Mon Sep 17 00:00:00 2001 From: luboslenco Date: Mon, 20 Jul 2020 23:15:36 +0200 Subject: [PATCH 227/230] Fix material id access in deferred light --- Shaders/deferred_light/deferred_light.frag.glsl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index 68e3fe0e..c30e3da4 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -259,7 +259,7 @@ void main() { #endif #ifdef _Emission - if (g0.a == 1.0) { + if (matid == 1) { fragColor.rgb += g1.rgb; // materialid albedo = vec3(0.0); } @@ -320,7 +320,7 @@ void main() { fragColor.rgb += sdirect * svisibility * sunCol; // #ifdef _Hair // Aniso -// if (g0.a == 2.0) { +// if (matid == 2) { // const float shinyParallel = roughness; // const float shinyPerpendicular = 0.1; // const vec3 v = vec3(0.99146, 0.11664, 0.05832); @@ -330,7 +330,7 @@ void main() { // #endif #ifdef _SSS - if (g0.a == 2.0) { + if (matid == 2) { #ifdef _CSM int casi, casindex; mat4 LWVP = getCascadeMat(distance(eye, p), casi, casindex); @@ -366,7 +366,7 @@ void main() { #ifdef _Spot #ifdef _SSS - if (g0.a == 2.0) fragColor.rgb += fragColor.rgb * SSSSTransmittance(LWVPSpot0, p, n, normalize(pointPos - p), lightPlane.y, shadowMapSpot[0]); + if (matid == 2) fragColor.rgb += fragColor.rgb * SSSSTransmittance(LWVPSpot0, p, n, normalize(pointPos - p), lightPlane.y, shadowMapSpot[0]); #endif #endif From 5d914b3bf54fe230cd126f964535516f6e488a9f Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 23 Aug 2020 23:11:49 +0200 Subject: [PATCH 228/230] Merge lightmapper 0.3.1.2 --- blender/arm/lightmapper/__init__.py | 1 + blender/arm/lightmapper/assets/sound.ogg | Bin 0 -> 20436 bytes blender/arm/lightmapper/assets/tlm_data.blend | Bin 0 -> 126952 bytes blender/arm/lightmapper/icons/bake.png | Bin 0 -> 3308 bytes blender/arm/lightmapper/icons/clean.png | Bin 0 -> 3075 bytes blender/arm/lightmapper/icons/explore.png | Bin 0 -> 2967 bytes blender/arm/lightmapper/keymap/__init__.py | 7 + blender/arm/lightmapper/keymap/keymap.py | 21 + blender/arm/lightmapper/operators/__init__.py | 21 + .../lightmapper/operators/installopencv.py | 67 ++ blender/arm/lightmapper/operators/tlm.py | 156 +++++ blender/arm/lightmapper/panels/__init__.py | 24 + blender/arm/lightmapper/panels/light.py | 17 + blender/arm/lightmapper/panels/object.py | 59 ++ blender/arm/lightmapper/panels/scene.py | 322 +++++++++ blender/arm/lightmapper/panels/world.py | 17 + .../arm/lightmapper/preferences/__init__.py | 16 + .../preferences/addon_preferences.py | 72 ++ .../arm/lightmapper/properties/__init__.py | 33 + .../properties/denoiser/integrated.py | 4 + .../lightmapper/properties/denoiser/oidn.py | 39 ++ .../lightmapper/properties/denoiser/optix.py | 21 + .../arm/lightmapper/properties/filtering.py | 4 + blender/arm/lightmapper/properties/object.py | 121 ++++ .../lightmapper/properties/renderer/cycles.py | 87 +++ .../properties/renderer/luxcorerender.py | 0 .../properties/renderer/octanerender.py | 0 .../properties/renderer/radeonrays.py | 0 blender/arm/lightmapper/properties/scene.py | 264 ++++++++ blender/arm/lightmapper/utility/__init__.py | 0 blender/arm/lightmapper/utility/build.py | 585 +++++++++++++++++ blender/arm/lightmapper/utility/cycles/ao.py | 0 .../arm/lightmapper/utility/cycles/cache.py | 74 +++ .../lightmapper/utility/cycles/indirect.py | 0 .../lightmapper/utility/cycles/lightmap.py | 49 ++ .../arm/lightmapper/utility/cycles/nodes.py | 182 +++++ .../arm/lightmapper/utility/cycles/prepare.py | 452 +++++++++++++ .../utility/denoiser/integrated.py | 79 +++ .../arm/lightmapper/utility/denoiser/oidn.py | 200 ++++++ .../arm/lightmapper/utility/denoiser/optix.py | 7 + blender/arm/lightmapper/utility/encoding.py | 245 +++++++ .../lightmapper/utility/filtering/numpy.py | 49 ++ .../lightmapper/utility/filtering/opencv.py | 160 +++++ .../lightmapper/utility/filtering/shader.py | 160 +++++ blender/arm/lightmapper/utility/icon.py | 31 + .../utility/preconfiguration/object.py | 5 + blender/arm/lightmapper/utility/utility.py | 620 ++++++++++++++++++ blender/arm/props_ui.py | 293 ++++++++- 48 files changed, 4537 insertions(+), 27 deletions(-) create mode 100644 blender/arm/lightmapper/__init__.py create mode 100644 blender/arm/lightmapper/assets/sound.ogg create mode 100644 blender/arm/lightmapper/assets/tlm_data.blend create mode 100644 blender/arm/lightmapper/icons/bake.png create mode 100644 blender/arm/lightmapper/icons/clean.png create mode 100644 blender/arm/lightmapper/icons/explore.png create mode 100644 blender/arm/lightmapper/keymap/__init__.py create mode 100644 blender/arm/lightmapper/keymap/keymap.py create mode 100644 blender/arm/lightmapper/operators/__init__.py create mode 100644 blender/arm/lightmapper/operators/installopencv.py create mode 100644 blender/arm/lightmapper/operators/tlm.py create mode 100644 blender/arm/lightmapper/panels/__init__.py create mode 100644 blender/arm/lightmapper/panels/light.py create mode 100644 blender/arm/lightmapper/panels/object.py create mode 100644 blender/arm/lightmapper/panels/scene.py create mode 100644 blender/arm/lightmapper/panels/world.py create mode 100644 blender/arm/lightmapper/preferences/__init__.py create mode 100644 blender/arm/lightmapper/preferences/addon_preferences.py create mode 100644 blender/arm/lightmapper/properties/__init__.py create mode 100644 blender/arm/lightmapper/properties/denoiser/integrated.py create mode 100644 blender/arm/lightmapper/properties/denoiser/oidn.py create mode 100644 blender/arm/lightmapper/properties/denoiser/optix.py create mode 100644 blender/arm/lightmapper/properties/filtering.py create mode 100644 blender/arm/lightmapper/properties/object.py create mode 100644 blender/arm/lightmapper/properties/renderer/cycles.py create mode 100644 blender/arm/lightmapper/properties/renderer/luxcorerender.py create mode 100644 blender/arm/lightmapper/properties/renderer/octanerender.py create mode 100644 blender/arm/lightmapper/properties/renderer/radeonrays.py create mode 100644 blender/arm/lightmapper/properties/scene.py create mode 100644 blender/arm/lightmapper/utility/__init__.py create mode 100644 blender/arm/lightmapper/utility/build.py create mode 100644 blender/arm/lightmapper/utility/cycles/ao.py create mode 100644 blender/arm/lightmapper/utility/cycles/cache.py create mode 100644 blender/arm/lightmapper/utility/cycles/indirect.py create mode 100644 blender/arm/lightmapper/utility/cycles/lightmap.py create mode 100644 blender/arm/lightmapper/utility/cycles/nodes.py create mode 100644 blender/arm/lightmapper/utility/cycles/prepare.py create mode 100644 blender/arm/lightmapper/utility/denoiser/integrated.py create mode 100644 blender/arm/lightmapper/utility/denoiser/oidn.py create mode 100644 blender/arm/lightmapper/utility/denoiser/optix.py create mode 100644 blender/arm/lightmapper/utility/encoding.py create mode 100644 blender/arm/lightmapper/utility/filtering/numpy.py create mode 100644 blender/arm/lightmapper/utility/filtering/opencv.py create mode 100644 blender/arm/lightmapper/utility/filtering/shader.py create mode 100644 blender/arm/lightmapper/utility/icon.py create mode 100644 blender/arm/lightmapper/utility/preconfiguration/object.py create mode 100644 blender/arm/lightmapper/utility/utility.py diff --git a/blender/arm/lightmapper/__init__.py b/blender/arm/lightmapper/__init__.py new file mode 100644 index 00000000..8df717b4 --- /dev/null +++ b/blender/arm/lightmapper/__init__.py @@ -0,0 +1 @@ +__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file diff --git a/blender/arm/lightmapper/assets/sound.ogg b/blender/arm/lightmapper/assets/sound.ogg new file mode 100644 index 0000000000000000000000000000000000000000..9f581e4d90d32ca0f12a1dea9133563ac8dceaab GIT binary patch literal 20436 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�^+Z0ES?h5kG)%CaU0)$ka%AQ!!>VGx_x)DUPJ?qvKJGLuWU1N zbo+57jDhYUX1iY6lOKu6qZ>3!qGrl(-uze(U1W6LP|{z$cW?B&cjd1C+zBGxV14-e zcYA%==^R+59S%;-?`3=~IoOUJz6l0FjyYzR0meq;0Oiu9tw^9i^NO-}qoo${e!AOf z{#>r2AWEF}#TD$M9a}{)Qbl#U;qvk)J9PeozHBeM8wHNyYk}&$JtlzyS|aXX=?b<7 zJz_Px%-1RB8_k}g9jl$JZ$5QS$T=HC^6}gZe1k>L+n;FSA9X-98xsUexczHpBGN(0x(WlhGK-X2b&? zK>*DrQT2ZNJd9+Wsl%Cx?flk2WS~f}qDxnWB%Mhmo?9C8IY<)hak6;Mb5>wBg$3Ord)=&7`w-H5h$_95$>8KXSyMc=El$6X5AYlf9;#zE_B zb`vYx;&vH4nwuhE@Z@z@ z#4YFQ`((%AFQ=ec%SkH+SeApdK89E_|whfTj#V{HmkP&XX8ED2?|%gW2I(MFu3Wp zPMgHehHN1qY;KAteL%VO>gsm>X#CEv9s#({P+xubPn!OrsywGJ`PP{SUzprqoA6^W zT0Z}%U;=2-Xd%Ii+UbKH6{kR*Yl2-arWy_7*FyxzX3typmy^)%EnOOkdZx>jaeBJ# zmEZeyPbdKN8c{u9PA;|eKSZezX6@PM@AI#d5Zl3TFiv)F7q6b%o>R$tQ)}2bmh2xR zI!EnK&>4i3l)t%p)M0ImlrMe|*Lok+&WLE|Fe_r>43K8N*lGTv{?X&y-MO!zauQu< zbZD=xD>&N@S8nqC2O_KaiYc>;W;J)jvge6yb5jNHr*z{yH%di1Ma1W}s4$xRN|?`W zI4nyW1SX+bU{7|t^D8t5=~{>+B_-rUQ^)rQSLa=IEd+7vKFyV&RTfTE(X5!-+u*)n!-t)|w%g%0k2m9P zzerg$i-O^057{r)_6LkE^*sd%8M1CYU=^|x`T;DVEvq?@xY56(HxqHkR}(x@W0*U2WVxRf|jI`oaZYv-geB( zAMO_a^izDS_W9SxM||TFck$Yf)=)3SE8fepQsjQSrhLfAO;^MW$G20}lkUDxVI0$J zC$jEh-Z=XF^rj8ve*d<@EXvo%G^sl^vlG;+v()BM!g$(_Vb2my**r&l3^KYnjC3z3 zDej*M@r^dzjkG?I{+{*ioqxfP_Ifeu&CTM<(CWfa%J0-uE>o&C{Yy&#gJt%SG~9A5 zkA!>z{Q;lKVPdRfh1>0}n1sO;PiwwhK9AOTfcsKhsX`?Goi$>f$tdRPfxZXr+x#5= zhIh^IiO*G`Joi5$@Mu&tWvG+Mex^mV>;6he>_FJc$7NKXv~TUHaxsT5PNLP0Vm`P{ ztgK0)Fvg~#9VSR^Fm*_PM@BXvy$WN-z$?yMpuBmP32zLOE+F%V+z~wZTuMWCty16d zc0LK-P#;dUTdtiUtSQ{RpjdINSh=^zFhbK-nzD3lW9K7XpOTqDz6#}X_sRNw*-;ft zuD3#21!le}jRp~R*$u2kIGF+rEa4mMvJ!S#gHA~Tpyja)I%PgJ*CR7%!|h?iO)V+n zBO@C5c#Yisqz|ou0_yQghVmMmHZPQfpQJxNpTxug1`=*JS{yT@)bfuG8A*#dM>;yl zY*2oMbe8lQGqcbV0i!wwEuG@4_Se5IKBdX-%*3mCR2~PVC7wSDh$VKSPdG}@hVRTi zxXVZHK?Q@y$;p^Gdc+7AJ}fXP`k_^X*F3A>RmPo3mt@fK_@t^t%da#Ft5621MUm>s zrn6ama=E`;L1aGCTNE1VQR21|GlK138_;~$gc8nH@P_l?&^xh9xYO5K`o`M`SbBfa z%bO3(lMWBg+-FXN?8nxFgP&#=nLPA*^}N|idW*rxo2mYFlX(itZ86`U6N#-m6q)`= zjChYd%)pRRU{JfKbBZ=k7(RDZtX~vg#4q0>%BZfrEz#;Ik)APCi?iNVi$drWdoR7t za}Nk#s*j?L;bP;IvK`(h0HV4@K#i4!gG03)?|}EMpwmPI1`v;+nhz-bP4n|X%${<+ znn=xKU1~RwJG&c{_T@@Zy!8i8typ_PKQUK+Y z%e%Q$BkNnLYExu=WMgsv`_wUaTS~dz%2%erZ96l0J`cPIV~rXTicOaUs#oH^Pd#b6 zn1c=;zR|MrAh)5%j&p0p>@tyV)iZ1!!_y{3Ff^E2z33|YnWwbQk5J-zD6xbks09Pl z0b@LewV$#z0zc2mSioq zinPNF(x*P>OW$vzJXNHPS<;S&0|d;W3m?`EX1(u(_c(O~v8dYz?aWKHGSXa?wY!SE zxQb{`L&_gM7GfsB_I%$R6Z8A|Q3``h4FeN%oVr+wzR+Ts>bC1- z<35lv*gP}28xba{K}CDX=hRD=yz-;?lS`YYlDRmCCXuH}LaE&`pdEIzoAB-XkWE2| zvKI9H?QEhBW;kD8DN+lQl8(@mYcF@UFQ`EDrCiP^dh&y(r-Foun}U=! z%zrmYA*~ZBc4au<^S7XWpqA+A<7b*1U*{VKTy*Gh82xQ(J9)5yn!*DveX;w7=rk

W4s2c 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/icons/clean.png b/blender/arm/lightmapper/icons/clean.png new file mode 100644 index 0000000000000000000000000000000000000000..c9bdd9db2b529086df7a3c106a48ead90665729c GIT binary patch literal 3075 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/icons/explore.png b/blender/arm/lightmapper/icons/explore.png new file mode 100644 index 0000000000000000000000000000000000000000..e5486558f0c97acc35bedca0ab2eff0abf1ddd07 GIT binary patch literal 2967 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 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasList_index] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) + +class TLM_PT_Additional(bpy.types.Panel): + bl_label = "Additional" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "TLM_PT_Panel" + + def draw(self, context): + layout = self.layout + scene = context.scene + sceneProperties = scene.TLM_SceneProperties \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/world.py b/blender/arm/lightmapper/panels/world.py new file mode 100644 index 00000000..b3c5d294 --- /dev/null +++ b/blender/arm/lightmapper/panels/world.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_WorldMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/__init__.py b/blender/arm/lightmapper/preferences/__init__.py new file mode 100644 index 00000000..94cdfaea --- /dev/null +++ b/blender/arm/lightmapper/preferences/__init__.py @@ -0,0 +1,16 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import addon_preferences +#from . import build, clean, explore, encode, installopencv + +classes = [ + addon_preferences.TLM_AddonPreferences +] + +def register(): + for cls in classes: + register_class(cls) + +def unregister(): + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/addon_preferences.py b/blender/arm/lightmapper/preferences/addon_preferences.py new file mode 100644 index 00000000..ed52b239 --- /dev/null +++ b/blender/arm/lightmapper/preferences/addon_preferences.py @@ -0,0 +1,72 @@ +import bpy +from os.path import basename, dirname +from bpy.types import AddonPreferences +from .. operators import installopencv +import importlib + +class TLM_AddonPreferences(AddonPreferences): + + bl_idname = "thelightmapper" + + addon_keys = bpy.context.preferences.addons.keys() + + def draw(self, context): + + layout = self.layout + + box = layout.box() + row = box.row() + row.label(text="OpenCV") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is not None: + row.label(text="OpenCV installed") + else: + row.label(text="OpenCV not found - Install as administrator!", icon_value=2) + row = box.row() + row.operator("tlm.install_opencv_lightmaps", icon="PREFERENCES") + + box = layout.box() + row = box.row() + row.label(text="Blender Xatlas") + if "blender_xatlas" in self.addon_keys: + row.label(text="Blender Xatlas installed and available") + else: + row.label(text="Blender Xatlas not installed", icon_value=2) + row = box.row() + row.label(text="Github: https://github.com/mattedicksoncom/blender-xatlas") + + box = layout.box() + row = box.row() + row.label(text="RizomUV Bridge") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="UVPackmaster") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="Texel Density Checker") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="LuxCoreRender") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="OctaneRender") + row.label(text="Coming soon") + + # row = layout.row() + # row.label(text="PIP") + # row = layout.row() + # row.label(text="OIDN / Optix") + # row = layout.row() + # row.label(text="UVPackmaster") + # row = layout.row() + # row.label(text="Texel Density") diff --git a/blender/arm/lightmapper/properties/__init__.py b/blender/arm/lightmapper/properties/__init__.py new file mode 100644 index 00000000..5cf4bd65 --- /dev/null +++ b/blender/arm/lightmapper/properties/__init__.py @@ -0,0 +1,33 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import scene, object +from . renderer import cycles +from . denoiser import oidn, optix + +classes = [ + scene.TLM_SceneProperties, + object.TLM_ObjectProperties, + cycles.TLM_CyclesSceneProperties, + oidn.TLM_OIDNEngineProperties, + optix.TLM_OptixEngineProperties +] + +def register(): + for cls in classes: + register_class(cls) + + bpy.types.Scene.TLM_SceneProperties = bpy.props.PointerProperty(type=scene.TLM_SceneProperties) + bpy.types.Object.TLM_ObjectProperties = bpy.props.PointerProperty(type=object.TLM_ObjectProperties) + bpy.types.Scene.TLM_EngineProperties = bpy.props.PointerProperty(type=cycles.TLM_CyclesSceneProperties) + bpy.types.Scene.TLM_OIDNEngineProperties = bpy.props.PointerProperty(type=oidn.TLM_OIDNEngineProperties) + bpy.types.Scene.TLM_OptixEngineProperties = bpy.props.PointerProperty(type=optix.TLM_OptixEngineProperties) + +def unregister(): + for cls in classes: + unregister_class(cls) + + del bpy.types.Scene.TLM_SceneProperties + del bpy.types.Object.TLM_ObjectProperties + del bpy.types.Scene.TLM_EngineProperties + del bpy.types.Scene.TLM_OIDNEngineProperties + del bpy.types.Scene.TLM_OptixEngineProperties \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/integrated.py b/blender/arm/lightmapper/properties/denoiser/integrated.py new file mode 100644 index 00000000..165de7f7 --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/integrated.py @@ -0,0 +1,4 @@ +import bpy +from bpy.props import * + +class TLM_IntegratedDenoiseEngineProperties(bpy.types.PropertyGroup): \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/oidn.py b/blender/arm/lightmapper/properties/denoiser/oidn.py new file mode 100644 index 00000000..c7d72963 --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/oidn.py @@ -0,0 +1,39 @@ +import bpy +from bpy.props import * + +class TLM_OIDNEngineProperties(bpy.types.PropertyGroup): + tlm_oidn_path : StringProperty( + name="OIDN Path", + description="The path to the OIDN binaries", + default="", + subtype="FILE_PATH") + + tlm_oidn_verbose : BoolProperty( + name="Verbose", + description="TODO") + + tlm_oidn_threads : IntProperty( + name="Threads", + default=0, + min=0, + max=64, + description="Amount of threads to use. Set to 0 for auto-detect.") + + tlm_oidn_maxmem : IntProperty( + name="Tiling max Memory", + default=0, + min=512, + max=32768, + description="Use tiling for memory conservation. Set to 0 to disable tiling.") + + tlm_oidn_affinity : BoolProperty( + name="Set Affinity", + description="TODO") + + tlm_oidn_use_albedo : BoolProperty( + name="Use albedo map", + description="TODO") + + tlm_oidn_use_normal : BoolProperty( + name="Use normal map", + description="TODO") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/optix.py b/blender/arm/lightmapper/properties/denoiser/optix.py new file mode 100644 index 00000000..6b55875b --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/optix.py @@ -0,0 +1,21 @@ +import bpy +from bpy.props import * + +class TLM_OptixEngineProperties(bpy.types.PropertyGroup): + + tlm_optix_path : StringProperty( + name="Optix Path", + description="TODO", + default="", + subtype="FILE_PATH") + + tlm_optix_verbose : BoolProperty( + name="Verbose", + description="TODO") + + tlm_optix_maxmem : IntProperty( + name="Tiling max Memory", + default=0, + min=512, + max=32768, + description="Use tiling for memory conservation. Set to 0 to disable tiling.") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/filtering.py b/blender/arm/lightmapper/properties/filtering.py new file mode 100644 index 00000000..b153fe2b --- /dev/null +++ b/blender/arm/lightmapper/properties/filtering.py @@ -0,0 +1,4 @@ +import bpy +from bpy.props import * + +class TLM_FilteringProperties(bpy.types.PropertyGroup): \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/object.py b/blender/arm/lightmapper/properties/object.py new file mode 100644 index 00000000..cc20be5f --- /dev/null +++ b/blender/arm/lightmapper/properties/object.py @@ -0,0 +1,121 @@ +import bpy +from bpy.props import * + +class TLM_ObjectProperties(bpy.types.PropertyGroup): + + addon_keys = bpy.context.preferences.addons.keys() + + tlm_atlas_pointer : StringProperty( + name = "Atlas Group", + description = "Atlas Lightmap Group", + default = "") + + tlm_mesh_lightmap_use : BoolProperty( + name="Enable Lightmapping", + description="TODO", + default=False) + + tlm_mesh_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Lightmap Resolution", + description="TODO", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'),('SmartProject', 'Smart Project', 'TODO'),('CopyExisting', 'Copy Existing', 'TODO'),('AtlasGroup', 'Atlas Group', 'TODO')] + + if "blender_xatlas" in addon_keys: + unwrap_modes.append(('Xatlas', 'Xatlas', 'TODO')) + + tlm_mesh_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="TODO", + default='SmartProject') + + tlm_mesh_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + tlm_mesh_filter_override : BoolProperty( + name="Override filtering", + description="Override the scene specific filtering", + default=False) + + #FILTERING SETTINGS GROUP + tlm_mesh_filtering_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'Make use of OpenCV based image filtering (Requires it to be installed first in the preferences panel)'), + ('Numpy', 'Numpy', 'Make use of Numpy based image filtering (Integrated)')], + name = "Filtering library", + description="Select which filtering library to use.", + default='Numpy') + + #Numpy Filtering options + tlm_mesh_numpy_filtering_mode : EnumProperty( + items = [('Blur', 'Blur', 'Basic blur filtering.')], + name = "Filter", + description="TODO", + default='Blur') + + #OpenCV Filtering options + tlm_mesh_filtering_mode : EnumProperty( + items = [('Box', 'Box', 'Basic box blur'), + ('Gaussian', 'Gaussian', 'Gaussian blurring'), + ('Bilateral', 'Bilateral', 'Edge-aware filtering'), + ('Median', 'Median', 'Median blur')], + name = "Filter", + description="TODO", + default='Median') + + tlm_mesh_filtering_gaussian_strength : IntProperty( + name="Gaussian Strength", + default=3, + min=1, + max=50) + + tlm_mesh_filtering_iterations : IntProperty( + name="Filter Iterations", + default=5, + min=1, + max=50) + + tlm_mesh_filtering_box_strength : IntProperty( + name="Box Strength", + default=1, + min=1, + max=50) + + tlm_mesh_filtering_bilateral_diameter : IntProperty( + name="Pixel diameter", + default=3, + min=1, + max=50) + + tlm_mesh_filtering_bilateral_color_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_mesh_filtering_bilateral_coordinate_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_mesh_filtering_median_kernel : IntProperty( + name="Median kernel", + default=3, + min=1, + max=5) + \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/cycles.py b/blender/arm/lightmapper/properties/renderer/cycles.py new file mode 100644 index 00000000..aa0dce47 --- /dev/null +++ b/blender/arm/lightmapper/properties/renderer/cycles.py @@ -0,0 +1,87 @@ +import bpy +from bpy.props import * + +class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): + + tlm_mode : EnumProperty( + items = [('CPU', 'CPU', 'Use the processor to bake textures'), + ('GPU', 'GPU', 'Use the graphics card to bake textures')], + name = "Device", + description="Select whether to use the CPU or the GPU for baking", + default="CPU") + + tlm_quality : EnumProperty( + items = [('0', 'Exterior Preview', 'Best for fast exterior previz'), + ('1', 'Interior Preview', 'Best for fast interior previz with bounces'), + ('2', 'Medium', 'Best for complicated interior preview and final for isometric environments'), + ('3', 'High', 'Best used for final baking for 3rd person games'), + ('4', 'Production', 'Best for first-person and Archviz'), + ('5', 'Custom', 'Uses the cycles sample settings provided the user')], + name = "Quality", + description="Select baking quality", + default="0") + + tlm_resolution_scale : EnumProperty( + items = [('1', '1/1', '1'), + ('2', '1/2', '2'), + ('4', '1/4', '4'), + ('8', '1/8', '8')], + name = "Resolution scale", + description="Select resolution scale", + default="2") + + tlm_setting_supersample : EnumProperty( + items = [('none', 'None', 'No supersampling'), + ('2x', '2x', 'Double supersampling'), + ('4x', '4x', 'Quadruple supersampling')], + name = "Supersampling", + description="Supersampling scale", + default="none") + + tlm_bake_mode : EnumProperty( + items = [('Background', 'Background', 'More overhead; allows for network.'), + ('Foreground', 'Foreground', 'Direct in-session bake')], + name = "Baking mode", + description="Select bake mode", + default="Foreground") + + tlm_caching_mode : EnumProperty( + items = [('Copy', 'Copy', 'More overhead; allows for network.'), + ('Cache', 'Cache', 'Cache in separate blend'), + ('Node', 'Node restore', 'EXPERIMENTAL! Use with care')], + name = "Caching mode", + description="Select cache mode", + default="Copy") + + tlm_directional_mode : EnumProperty( + items = [('None', 'None', 'No directional information'), + ('Normal', 'Baked normal', 'Baked normal maps are taken into consideration')], + name = "Directional mode", + description="Select directional mode", + default="None") + + tlm_lightmap_savedir : StringProperty( + name="Lightmap Directory", + description="TODO", + default="Lightmaps", + subtype="FILE_PATH") + + tlm_dilation_margin : IntProperty( + name="Dilation margin", + default=4, + min=1, + max=64, + subtype='PIXEL') + + tlm_exposure_multiplier : FloatProperty( + name="Exposure Multiplier", + default=0, + description="0 to disable. Multiplies GI value") + + tlm_metallic_handling_mode : EnumProperty( + items = [('ignore', 'Ignore', 'No directional information'), + ('clamp', 'Clamp', 'Clamp to value 0.9'), + ('zero', 'Zero', 'Temporarily set to 0 during baking, and reapply after')], + name = "Metallic handling", + description="Set metallic handling mode to prevent black-baking.", + default="ignore") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/luxcorerender.py b/blender/arm/lightmapper/properties/renderer/luxcorerender.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/properties/renderer/octanerender.py b/blender/arm/lightmapper/properties/renderer/octanerender.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/properties/renderer/radeonrays.py b/blender/arm/lightmapper/properties/renderer/radeonrays.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/properties/scene.py b/blender/arm/lightmapper/properties/scene.py new file mode 100644 index 00000000..e1cddb62 --- /dev/null +++ b/blender/arm/lightmapper/properties/scene.py @@ -0,0 +1,264 @@ +import bpy +from bpy.props import * + +class TLM_SceneProperties(bpy.types.PropertyGroup): + + engines = [('Cycles', 'Cycles', 'Use Cycles for lightmapping')] + + #engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) + #engines.append(('OctaneRender', 'Octane Render', 'Use Octane Render for lightmapping')) + + tlm_lightmap_engine : EnumProperty( + items = engines, + name = "Lightmap Engine", + description="Select which lightmap engine to use.", + default='Cycles') + + #SETTINGS GROUP + tlm_setting_clean_option : EnumProperty( + items = [('Clean', 'Full Clean', 'Clean lightmap directory and revert all materials'), + ('CleanMarked', 'Clean marked', 'Clean only the objects marked for lightmapping')], + name = "Clean mode", + description="The cleaning mode, either full or partial clean. Be careful that you don't delete lightmaps you don't intend to delete.", + default='Clean') + + tlm_setting_keep_cache_files : BoolProperty( + name="Keep cache files", + description="Keep cache files (non-filtered and non-denoised)", + default=True) + + tlm_setting_renderer : EnumProperty( + items = [('CPU', 'CPU', 'Bake using the processor'), + ('GPU', 'GPU', 'Bake using the graphics card')], + name = "Device", + description="Select whether to use the CPU or the GPU", + default="CPU") + + tlm_setting_scale : EnumProperty( + items = [('8', '1/8', '1/8th of set scale'), + ('4', '1/4', '1/4th of set scale'), + ('2', '1/2', 'Half of set scale'), + ('1', '1/1', 'Full scale')], + name = "Lightmap Resolution scale", + description="Lightmap resolution scaling. Adjust for previewing.", + default="1") + + tlm_setting_supersample : EnumProperty( + items = [('2x', '2x', 'Double the sampling resolution'), + ('4x', '4x', 'Quadruple the sampling resolution')], + name = "Lightmap Supersampling", + description="Supersamples the baked lightmap. Increases bake time", + default="2x") + + tlm_setting_savedir : StringProperty( + name="Lightmap Directory", + description="Your baked lightmaps will be stored here.", + default="Lightmaps", + subtype="FILE_PATH") + + tlm_setting_exposure_multiplier : FloatProperty( + name="Exposure Multiplier", + default=0, + description="0 to disable. Multiplies GI value") + + tlm_alert_on_finish : BoolProperty( + name="Alert on finish", + description="Play a sound when the lightmaps are done.", + default=False) + + tlm_setting_apply_scale : BoolProperty( + name="Apply scale", + description="Apply the scale before unwrapping.", + default=True) + + tlm_play_sound : BoolProperty( + name="Play sound on finish", + description="Play sound on finish", + default=False) + + tlm_compile_statistics : BoolProperty( + name="Compile statistics", + description="Compile time statistics in the lightmap folder.", + default=True) + + tlm_apply_on_unwrap : BoolProperty( + name="Apply scale", + description="TODO", + default=False) + + #DENOISE SETTINGS GROUP + tlm_denoise_use : BoolProperty( + name="Enable denoising", + description="Enable denoising for lightmaps", + default=False) + + tlm_denoise_engine : EnumProperty( + items = [('Integrated', 'Integrated', 'Use the Blender native denoiser (Compositor; Slow)'), + ('OIDN', 'Intel Denoiser', 'Use Intel denoiser (CPU powered)'), + ('Optix', 'Optix Denoiser', 'Use Nvidia Optix denoiser (GPU powered)')], + name = "Denoiser", + description="Select which denoising engine to use.", + default='Integrated') + + #FILTERING SETTINGS GROUP + tlm_filtering_use : BoolProperty( + name="Enable filtering", + description="Enable filtering for lightmaps", + default=False) + + tlm_filtering_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'Make use of OpenCV based image filtering (Requires it to be installed first in the preferences panel)'), + ('Numpy', 'Numpy', 'Make use of Numpy based image filtering (Integrated)')], + name = "Filtering library", + description="Select which filtering library to use.", + default='Numpy') + + #Numpy Filtering options + tlm_numpy_filtering_mode : EnumProperty( + items = [('Blur', 'Blur', 'Basic blur filtering.')], + name = "Filter", + description="TODO", + default='Blur') + + #OpenCV Filtering options + tlm_filtering_mode : EnumProperty( + items = [('Box', 'Box', 'Basic box blur'), + ('Gaussian', 'Gaussian', 'Gaussian blurring'), + ('Bilateral', 'Bilateral', 'Edge-aware filtering'), + ('Median', 'Median', 'Median blur')], + name = "Filter", + description="TODO", + default='Median') + + tlm_filtering_gaussian_strength : IntProperty( + name="Gaussian Strength", + default=3, + min=1, + max=50) + + tlm_filtering_iterations : IntProperty( + name="Filter Iterations", + default=5, + min=1, + max=50) + + tlm_filtering_box_strength : IntProperty( + name="Box Strength", + default=1, + min=1, + max=50) + + tlm_filtering_bilateral_diameter : IntProperty( + name="Pixel diameter", + default=3, + min=1, + max=50) + + tlm_filtering_bilateral_color_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_filtering_bilateral_coordinate_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_filtering_median_kernel : IntProperty( + name="Median kernel", + default=3, + min=1, + max=5) + + #Encoding properties + tlm_encoding_use : BoolProperty( + name="Enable encoding", + description="Enable encoding for lightmaps", + default=False) + + tlm_encoding_mode : EnumProperty( + items = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), + ('LogLuv', 'LogLuv', '8-bit HDR encoding. Different.'), + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')], + name = "Encoding Mode", + description="TODO", + default='HDR') + + tlm_encoding_range : IntProperty( + name="Encoding range", + description="Higher gives a larger HDR range, but also gives more banding.", + default=6, + min=1, + max=10) + + tlm_encoding_armory_setup : BoolProperty( + name="Use Armory decoder", + description="TODO", + default=False) + + tlm_encoding_colorspace : EnumProperty( + items = [('XYZ', 'XYZ', 'TODO'), + ('sRGB', 'sRGB', 'TODO'), + ('NonColor', 'Non-Color', 'TODO'), + ('ACES', 'Linear ACES', 'TODO'), + ('Linear', 'Linear', 'TODO'), + ('FilmicLog', 'Filmic Log', 'TODO')], + name = "Color Space", + description="TODO", + default='Linear') + + tlm_compression : IntProperty( + name="PNG Compression", + description="0 = No compression. 100 = Maximum compression.", + default=0, + min=0, + max=100) + + tlm_format : EnumProperty( + items = [('RGBE', 'HDR', '32-bit RGBE encoded .hdr files. No compression available.'), + ('EXR', 'EXR', '32-bit OpenEXR format.')], + name = "Format", + description="Select default 32-bit format", + default='RGBE') + + tlm_override_object_settings : BoolProperty( + name="Override settings", + description="TODO", + default=False) + + tlm_mesh_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Lightmap Resolution", + description="TODO", + default='256') + + tlm_mesh_lightmap_unwrap_mode : EnumProperty( + items = [('Lightmap', 'Lightmap', 'TODO'), + ('SmartProject', 'Smart Project', 'TODO'), + ('CopyExisting', 'Copy Existing', 'TODO'), + ('AtlasGroup', 'Atlas Group', 'TODO')], + name = "Unwrap Mode", + description="TODO", + default='SmartProject') + + tlm_mesh_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + tlm_headless : BoolProperty( + name="Don't apply materials", + description="Headless; Do not apply baked materials on finish.", + default=False) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/__init__.py b/blender/arm/lightmapper/utility/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/utility/build.py b/blender/arm/lightmapper/utility/build.py new file mode 100644 index 00000000..33632daa --- /dev/null +++ b/blender/arm/lightmapper/utility/build.py @@ -0,0 +1,585 @@ +import bpy, os, importlib, subprocess, sys, threading, platform, aud +from . import encoding +from . cycles import lightmap, prepare, nodes, cache +from . denoiser import integrated, oidn +from . filtering import opencv +from os import listdir +from os.path import isfile, join +from time import time, sleep + +previous_settings = {} + +def prepare_build(self=0, background_mode=False): + + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Foreground" or background_mode==True: + + global start_time + start_time = time() + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + #We dynamically load the renderer and denoiser, instead of loading something we don't use + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + pass + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + #Timer start here bound to global + + if check_save(): + print("Please save your file first") + self.report({'INFO'}, "Please save your file first") + return{'FINISHED'} + + if check_denoiser(): + print("No denoise OIDN path assigned") + self.report({'INFO'}, "No denoise OIDN path assigned") + return{'FINISHED'} + + if check_materials(): + print("Error with material") + self.report({'INFO'}, "Error with material") + return{'FINISHED'} + + if opencv_check(): + if sceneProperties.tlm_filtering_use: + print("Error:Filtering - OpenCV not installed") + self.report({'INFO'}, "Error:Filtering - OpenCV not installed") + return{'FINISHED'} + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if not os.path.isdir(dirpath): + os.mkdir(dirpath) + + #Naming check + naming_check() + + ## RENDER DEPENDENCY FROM HERE + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + prepare.init(self, previous_settings) + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + #Renderer - Store settings + + #Renderer - Set settings + + #Renderer - Config objects, lights, world + + begin_build() + + else: + + filepath = bpy.data.filepath + + start_time = time() + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + #We dynamically load the renderer and denoiser, instead of loading something we don't use + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + pass + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + #Timer start here bound to global + + if check_save(): + print("Please save your file first") + self.report({'INFO'}, "Please save your file first") + return{'FINISHED'} + + if check_denoiser(): + print("No denoise OIDN path assigned") + self.report({'INFO'}, "No denoise OIDN path assigned") + return{'FINISHED'} + + if check_materials(): + print("Error with material") + self.report({'INFO'}, "Error with material") + return{'FINISHED'} + + if opencv_check(): + if sceneProperties.tlm_filtering_use: + print("Error:Filtering - OpenCV not installed") + self.report({'INFO'}, "Error:Filtering - OpenCV not installed") + return{'FINISHED'} + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if not os.path.isdir(dirpath): + os.mkdir(dirpath) + + #Naming check + naming_check() + + pipe_open([sys.executable,"-b",filepath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], finish_assemble) + +def finish_assemble(): + pass + #bpy.ops.wm.revert_mainfile() We cannot use this, as Blender crashes... + print("Background baking finished") + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + prepare.init(previous_settings) + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + pass + + manage_build(True) + +def pipe_open(args, callback): + + def thread_process(args, callback): + process = subprocess.Popen(args) + process.wait() + callback() + return + + thread = threading.Thread(target=thread_process, args=(args, callback)) + thread.start() + return thread + +def begin_build(): + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + lightmap.bake() + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + pass + + #Denoiser + + if sceneProperties.tlm_denoise_use: + + if sceneProperties.tlm_denoise_engine == "Integrated": + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + print(baked_image_array) + + denoiser = integrated.TLM_Integrated_Denoise() + + denoiser.load(baked_image_array) + + denoiser.setOutputDir(dirpath) + + denoiser.denoise() + + elif sceneProperties.tlm_denoise_engine == "OIDN": + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + oidnProperties = scene.TLM_OIDNEngineProperties + + denoiser = oidn.TLM_OIDN_Denoise(oidnProperties, baked_image_array, dirpath) + + denoiser.denoise() + + denoiser.clean() + + del denoiser + + else: + pass + + #Filtering + if sceneProperties.tlm_filtering_use: + + if sceneProperties.tlm_denoise_use: + useDenoise = True + else: + useDenoise = False + + filter = opencv.TLM_CV_Filtering + + filter.init(dirpath, useDenoise) + + if sceneProperties.tlm_encoding_use: + + if sceneProperties.tlm_encoding_mode == "HDR": + + if sceneProperties.tlm_format == "EXR": + + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "OPEN_EXR" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".exr") + + if sceneProperties.tlm_encoding_mode == "LogLuv": + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + encoding.encodeLogLuv(img, dirpath, 0) + + if sceneProperties.tlm_encoding_mode == "RGBM": + + print("ENCODING RGBM") + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + print("Encoding:" + str(file)) + encoding.encodeImageRGBM(img, sceneProperties.tlm_encoding_range, dirpath, 0) + + manage_build() + +def manage_build(background_pass=False): + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + if background_pass: + nodes.apply_lightmaps() + + nodes.apply_materials() #From here the name is changed... + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + formatEnc = ".hdr" + + if sceneProperties.tlm_encoding_use: + + if sceneProperties.tlm_encoding_mode == "HDR": + + if sceneProperties.tlm_format == "EXR": + + formatEnc = ".exr" + + if sceneProperties.tlm_encoding_mode == "LogLuv": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode == "RGBM": + + formatEnc = "_encoded.png" + + if not background_pass: + nodes.exchangeLightmapsToPostfix("_baked", end, formatEnc) + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": + pass + #bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath + "baked") #Crashes Blender + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + # for image in bpy.data.images: + # if image.name.endswith("_baked"): + # resolution = image.size[0] + # rescale = resolution / supersampling_scale + # image.scale(rescale, rescale) + # image.save() + + for image in bpy.data.images: + if image.users < 1: + bpy.data.images.remove(image) + + if scene.TLM_SceneProperties.tlm_headless: + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for obj in bpy.data.objects: + + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + img_name = obj.name + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + total_time = sec_to_hours((time() - start_time)) + print(total_time) + + reset_settings(previous_settings["settings"]) + + if scene.TLM_SceneProperties.tlm_alert_on_finish: + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/sound.ogg')) + + device = aud.Device() + sound = aud.Sound.file(sound_path) + device.play(sound) + print("ALERT!") + +def reset_settings(prev_settings): + scene = bpy.context.scene + cycles = scene.cycles + + cycles.samples = int(prev_settings[0]) + cycles.max_bounces = int(prev_settings[1]) + cycles.diffuse_bounces = int(prev_settings[2]) + cycles.glossy_bounces = int(prev_settings[3]) + cycles.transparent_max_bounces = int(prev_settings[4]) + cycles.transmission_bounces = int(prev_settings[5]) + cycles.volume_bounces = int(prev_settings[6]) + cycles.caustics_reflective = prev_settings[7] + cycles.caustics_refractive = prev_settings[8] + cycles.device = prev_settings[9] + scene.render.engine = prev_settings[10] + bpy.context.view_layer.objects.active = prev_settings[11] + + #for obj in prev_settings[12]: + # obj.select_set(True) + +def naming_check(): + + for obj in bpy.data.objects: + + if obj.type == "MESH": + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if obj.name != "": + + if "_" in obj.name: + obj.name = obj.name.replace("_",".") + if " " in obj.name: + obj.name = obj.name.replace(" ",".") + if "[" in obj.name: + obj.name = obj.name.replace("[",".") + if "]" in obj.name: + obj.name = obj.name.replace("]",".") + if "ø" in obj.name: + obj.name = obj.name.replace("ø","oe") + if "æ" in obj.name: + obj.name = obj.name.replace("æ","ae") + if "å" in obj.name: + obj.name = obj.name.replace("å","aa") + + for slot in obj.material_slots: + if "_" in slot.material.name: + slot.material.name = slot.material.name.replace("_",".") + if " " in slot.material.name: + slot.material.name = slot.material.name.replace(" ",".") + if "[" in slot.material.name: + slot.material.name = slot.material.name.replace("[",".") + if "[" in slot.material.name: + slot.material.name = slot.material.name.replace("]",".") + if "ø" in slot.material.name: + slot.material.name = slot.material.name.replace("ø","oe") + if "æ" in slot.material.name: + slot.material.name = slot.material.name.replace("æ","ae") + if "å" in slot.material.name: + slot.material.name = slot.material.name.replace("å","aa") + +def opencv_check(): + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is not None: + return 0 + else: + return 1 + +def check_save(): + if not bpy.data.is_saved: + + return 1 + + else: + + return 0 + +def check_denoiser(): + + scene = bpy.context.scene + + if scene.TLM_SceneProperties.tlm_denoise_use: + + if scene.TLM_SceneProperties.tlm_denoise_engine == "OIDN": + + oidnPath = scene.TLM_OIDNEngineProperties.tlm_oidn_path + + if scene.TLM_OIDNEngineProperties.tlm_oidn_path == "": + return 1 + + if platform.system() == "Windows": + if not scene.TLM_OIDNEngineProperties.tlm_oidn_path.endswith(".exe"): + return 1 + else: + return 0 + + # if scene.TLM_SceneProperties.tlm_denoise_use: + # if scene.TLM_SceneProperties.tlm_oidn_path == "": + # print("NO DENOISE PATH") + # return False + # else: + # return True + # else: + # return True + +def check_materials(): + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + + if mat is None: + print("MatNone") + mat = bpy.data.materials.new(name="Material") + mat.use_nodes = True + slot.material = mat + + nodes = mat.node_tree.nodes + + #TODO FINISH MATERIAL CHECK -> Nodes check + #Afterwards, redo build/utility + +def sec_to_hours(seconds): + a=str(seconds//3600) + b=str((seconds%3600)//60) + c=str((seconds%3600)%60) + d=["{} hours {} mins {} seconds".format(a, b, c)] + return d \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/ao.py b/blender/arm/lightmapper/utility/cycles/ao.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/utility/cycles/cache.py b/blender/arm/lightmapper/utility/cycles/cache.py new file mode 100644 index 00000000..a4269204 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/cache.py @@ -0,0 +1,74 @@ +import bpy + +#Todo - Check if already exists, in case multiple objects has the same material + + +def backup_material_copy(slot): + material = slot.material + dup = material.copy() + dup.name = "." + material.name + "_Original" + dup.use_fake_user = True + +def backup_material_cache(slot, path): + bpy.ops.wm.save_as_mainfile(filepath=path, copy=True) + +def backup_material_cache_restore(slot, path): + print("Restore cache") + +def backup_material_rename(obj): + if "TLM_PrevMatArray" in obj: + print("Has PrevMat B") + for slot in obj.material_slots: + if slot.material.name.endswith("_Original"): + newname = slot.material.name[1:-9] + if newname in bpy.data.materials: + bpy.data.materials.remove(bpy.data.materials[newname]) + slot.material.name = newname + + del obj["TLM_PrevMatArray"] + +def backup_material_restore(obj): + print("RESTORE") + + if "TLM_PrevMatArray" in obj: + + print("Has PrevMat A") + #Running through the slots + prevMatArray = obj["TLM_PrevMatArray"] + slotsLength = len(prevMatArray) + + if len(prevMatArray) > 0: + for idx, slot in enumerate(obj.material_slots): #For each slot, we get the index + #We only need the index, corresponds to the array index + try: + originalMaterial = prevMatArray[idx] + except IndexError: + originalMaterial = "" + + slot.material.user_clear() + + if "." + originalMaterial + "_Original" in bpy.data.materials: + slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + slot.material.use_fake_user = False + + + #slot.material = + + #Remove material after changin + #We only rename after every change is complete + + #if "." + material.name + "_Original" in bpy.data.materials: + + # material = slot.material + # if "." + material.name + "_Original" in bpy.data.materials: + # original = bpy.data.materials["." + material.name + "_Original"] + # slot.material = original + # material.name = material.name + # original.name = original.name[1:-9] + # original.use_fake_user = False + # material.user_clear() + # bpy.data.materials.remove(material) + # #Reset number + # else: + # pass + #Check if material has nodes with lightmap prefix \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/indirect.py b/blender/arm/lightmapper/utility/cycles/indirect.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/utility/cycles/lightmap.py b/blender/arm/lightmapper/utility/cycles/lightmap.py new file mode 100644 index 00000000..7f35e04d --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/lightmap.py @@ -0,0 +1,49 @@ +import bpy, os + +def bake(): + + for obj in bpy.data.objects: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(False) + + iterNum = 0 + currentIterNum = 0 + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + iterNum = iterNum - 1 + + for obj in bpy.data.objects: + if obj.type == 'MESH': + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + scene = bpy.context.scene + + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + obj.hide_render = False + scene.render.bake.use_clear = False + + print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + bpy.ops.object.select_all(action='DESELECT') + currentIterNum = currentIterNum + 1 + + for image in bpy.data.images: + if image.name.endswith("_baked"): + + saveDir = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + bakemap_path = os.path.join(saveDir, image.name) + filepath_ext = ".hdr" + image.filepath_raw = bakemap_path + filepath_ext + image.file_format = "HDR" + print("Saving to: " + image.filepath_raw) + image.save() \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/nodes.py b/blender/arm/lightmapper/utility/cycles/nodes.py new file mode 100644 index 00000000..174a1867 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/nodes.py @@ -0,0 +1,182 @@ +import bpy, os + +def apply_lightmaps(): + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + scene = bpy.context.scene + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + #Find nodes + for node in nodes: + if node.name == "Baked Image": + + extension = ".hdr" + + postfix = "_baked" + + if scene.TLM_SceneProperties.tlm_denoise_use: + postfix = "_denoised" + if scene.TLM_SceneProperties.tlm_filtering_use: + postfix = "_filtered" + + node.image.source = "FILE" + image_name = obj.name + postfix + extension #TODO FIX EXTENSION + node.image.filepath_raw = os.path.join(dirpath, image_name) + + +def apply_materials(): + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + uv_layers = obj.data.uv_layers + uv_layers.active_index = 0 + scene = bpy.context.scene + + decoding = False + + #Sort name + for slot in obj.material_slots: + mat = slot.material + if mat.name.endswith('_temp'): + old = slot.material + slot.material = bpy.data.materials[old.name.split('_' + obj.name)[0]] + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + tlm_exposure = bpy.data.node_groups.get("Exposure") + + if tlm_exposure == None: + load_library("Exposure") + + #Apply materials + for slot in obj.material_slots: + mat = slot.material + + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + foundBakedNode = False + + #Find nodes + for node in nodes: + if node.name == "Baked Image": + lightmapNode = node + lightmapNode.location = -800, 300 + lightmapNode.name = "TLM_Lightmap" + foundBakedNode = True + + img_name = obj.name + '_baked' + + if not foundBakedNode: + lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage") + lightmapNode.location = -300, 300 + lightmapNode.image = bpy.data.images[img_name] + lightmapNode.name = "TLM_Lightmap" + lightmapNode.interpolation = "Smart" + + #Find output node + outputNode = nodes[0] + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in node_tree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + #Find mainnode + mainNode = outputNode.inputs[0].links[0].from_node + + #Add all nodes first + #Add lightmap multipliction texture + mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB") + mixNode.name = "Lightmap_Multiplication" + mixNode.location = -300, 300 + mixNode.blend_type = 'MULTIPLY' + mixNode.inputs[0].default_value = 1.0 + + UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap") + UVLightmap.uv_map = "UVMap_Lightmap" + UVLightmap.name = "Lightmap_UV" + UVLightmap.location = -1000, 300 + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + ExposureNode = node_tree.nodes.new(type="ShaderNodeGroup") + ExposureNode.node_tree = bpy.data.node_groups["Exposure"] + ExposureNode.inputs[1].default_value = scene.TLM_EngineProperties.tlm_exposure_multiplier + ExposureNode.location = -500, 300 + ExposureNode.name = "Lightmap_Exposure" + + #Add Basecolor node + if len(mainNode.inputs[0].links) == 0: + baseColorValue = mainNode.inputs[0].default_value + baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB") + baseColorNode.outputs[0].default_value = baseColorValue + baseColorNode.location = ((mainNode.location[0] - 500, mainNode.location[1] - 300)) + baseColorNode.name = "Lightmap_BasecolorNode_A" + else: + baseColorNode = mainNode.inputs[0].links[0].from_node + baseColorNode.name = "LM_P" + + #Linking + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + mat.node_tree.links.new(lightmapNode.outputs[0], ExposureNode.inputs[0]) #Connect lightmap node to mixnode + mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode + else: + mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode + mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node + mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node + mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode + +def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): + + print(ext_postfix, new_postfix, formatHDR) + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + for node in nodes: + if node.name == "Baked Image" or node.name == "TLM_Lightmap": + img_name = node.image.filepath_raw + cutLen = len(ext_postfix + formatHDR) + print("Len:" + str(len(ext_postfix + formatHDR)) + "|" + ext_postfix + ".." + formatHDR) + + #Simple way to sort out objects with multiple materials + if formatHDR == ".hdr" or formatHDR == ".exr": + if not node.image.filepath_raw.endswith(new_postfix + formatHDR): + node.image.filepath_raw = img_name[:-cutLen] + new_postfix + formatHDR + else: + cutLen = len(ext_postfix + ".hdr") + if not node.image.filepath_raw.endswith(new_postfix + formatHDR): + node.image.filepath_raw = img_name[:-cutLen] + new_postfix + formatHDR + + for image in bpy.data.images: + image.reload() + +def load_library(asset_name): + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + + if bpy.data.filepath.endswith('tlm_data.blend'): # Prevent load in library itself + return + + data_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'Assets/tlm_data.blend')) + data_names = [asset_name] + + # Import + data_refs = data_names.copy() + with bpy.data.libraries.load(data_path, link=False) as (data_from, data_to): + data_to.node_groups = data_refs + + for ref in data_refs: + ref.use_fake_user = True \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/prepare.py b/blender/arm/lightmapper/utility/cycles/prepare.py new file mode 100644 index 00000000..32ae7127 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/prepare.py @@ -0,0 +1,452 @@ +import bpy + +from . import cache +from .. utility import * + +def assemble(): + + configure_world() + + configure_lights() + + configure_meshes() + +def init(self, prev_container): + + store_existing(prev_container) + + set_settings() + + configure_world() + + configure_lights() + + configure_meshes(self) + +def configure_world(): + pass + +def configure_lights(): + pass + +def configure_meshes(self): + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + iterNum = 0 + currentIterNum = 0 + + scene = bpy.context.scene + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + if "." + slot.name + '_Original' in bpy.data.materials: + print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original') + slot.material = bpy.data.materials["." + slot.name + '_Original'] + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + for obj in bpy.data.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + currentIterNum = currentIterNum + 1 + + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + + #Provide material if none exists + preprocess_material(obj, scene) + + #UV Layer management here + if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": + uv_layers = obj.data.uv_layers + if not "UVMap_Lightmap" in uv_layers: + print("UVMap made B") + uvmap = uv_layers.new(name="UVMap_Lightmap") + uv_layers.active_index = len(uv_layers) - 1 + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) + + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + print("Smart Project B") + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + #import blender_xatlas + #blender_xatlas.Unwrap_Lightmap_Group_Xatlas_2(bpy.context) + + #bpy.ops.object.setup_unwrap() + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": + + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing + + print("Copied Existing B") + + #Here we copy an existing map + pass + else: + print("Existing found...skipping") + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_Lightmap': + uv_layers.active_index = i + print("Lightmap shift B") + break + + #Sort out nodes + for slot in obj.material_slots: + + nodetree = slot.material.node_tree + + outputNode = nodetree.nodes[0] #Presumed to be material output node + + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + mainNode = outputNode.inputs[0].links[0].from_node + + if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: + + #TODO! FIND THE PRINCIPLED PBR + self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + + if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] + else: + self.report({'INFO'}, "No principled found. Seeking diffuse") + if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] + else: + self.report({'INFO'}, "No supported nodes. Continuing anyway.") + pass + + if mainNode.type == 'GROUP': + if mainNode.node_tree != "Armory PBR": + print("The material group is not supported!") + pass + + if (mainNode.type == "BSDF_PRINCIPLED"): + print("BSDF_Principled") + if scene.TLM_EngineProperties.tlm_directional_mode == "None": + print("Directional mode") + if not len(mainNode.inputs[19].links) == 0: + print("NOT LEN 0") + ninput = mainNode.inputs[19].links[0] + noutput = mainNode.inputs[19].links[0].from_node + nodetree.links.remove(noutput.outputs[0].links[0]) + + #Clamp metallic + # if(mainNode.inputs[4].default_value == 1 and scene.TLM_SceneProperties.tlm_clamp_metallic): + # mainNode.inputs[4].default_value = 0.99 + + if (mainNode.type == "BSDF_DIFFUSE"): + print("BSDF_Diffuse") + + for slot in obj.material_slots: + + nodetree = bpy.data.materials[slot.name].node_tree + nodes = nodetree.nodes + + #First search to get the first output material type + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + mainNode = node + break + + #Fallback to get search + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes.get("Material Output") + + #Last resort to first node in list + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes[0].inputs[0].links[0].from_node + + for node in nodes: + if "LM" in node.name: + nodetree.links.new(node.outputs[0], mainNode.inputs[0]) + + for node in nodes: + if "Lightmap" in node.name: + nodes.remove(node) + +def preprocess_material(obj, scene): + if len(obj.material_slots) == 0: + single = False + number = 0 + while single == False: + matname = obj.name + ".00" + str(number) + if matname in bpy.data.materials: + single = False + number = number + 1 + else: + mat = bpy.data.materials.new(name=matname) + mat.use_nodes = True + obj.data.materials.append(mat) + single = True + + #We copy the existing material slots to an ordered array, which corresponds to the slot index + matArray = [] + for slot in obj.material_slots: + matArray.append(slot.name) + + obj["TLM_PrevMatArray"] = matArray + + for slot in obj.material_slots: + + cache.backup_material_copy(slot) + + mat = slot.material + if mat.users > 1: + copymat = mat.copy() + slot.material = copymat + + # for slot in obj.material_slots: + # matname = slot.material.name + # originalName = "." + matname + "_Original" + # hasOriginal = False + # if originalName in bpy.data.materials: + # hasOriginal = True + # else: + # hasOriginal = False + + # if hasOriginal: + # cache.backup_material_restore(slot) + + # cache.backup_material_copy(slot) + + ############################ + + #Make a material backup and restore original if exists + # if scene.TLM_SceneProperties.tlm_caching_mode == "Copy": + # for slot in obj.material_slots: + # matname = slot.material.name + # originalName = "." + matname + "_Original" + # hasOriginal = False + # if originalName in bpy.data.materials: + # hasOriginal = True + # else: + # hasOriginal = False + + # if hasOriginal: + # matcache.backup_material_restore(slot) + + # matcache.backup_material_copy(slot) + + # else: #Cache blend + # #TEST CACHE + # filepath = bpy.data.filepath + # dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_SceneProperties.tlm_lightmap_savedir) + # path = dirpath + "/cache.blend" + # bpy.ops.wm.save_as_mainfile(filepath=path, copy=True) + #print("Warning: Cache blend not supported") + + # for mat in bpy.data.materials: + # if mat.name.endswith('_baked'): + # bpy.data.materials.remove(mat, do_unlink=True) + # for img in bpy.data.images: + # if img.name == obj.name + "_baked": + # bpy.data.images.remove(img, do_unlink=True) + + + #SOME ATLAS EXCLUSION HERE? + ob = obj + for slot in ob.material_slots: + #If temporary material already exists + if slot.material.name.endswith('_temp'): + continue + n = slot.material.name + '_' + ob.name + '_temp' + if not n in bpy.data.materials: + slot.material = slot.material.copy() + slot.material.name = n + + #Add images for baking + img_name = obj.name + '_baked' + #Resolution is object lightmap resolution divided by global scaler + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + res = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale) + + #If image not in bpy.data.images or if size changed, make a new image + if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != res or bpy.data.images[img_name].size[1] != res: + img = bpy.data.images.new(img_name, res, res, alpha=True, float_buffer=True) + + num_pixels = len(img.pixels) + result_pixel = list(img.pixels) + + for i in range(0,num_pixels,4): + # result_pixel[i+0] = scene.TLM_SceneProperties.tlm_default_color[0] + # result_pixel[i+1] = scene.TLM_SceneProperties.tlm_default_color[1] + # result_pixel[i+2] = scene.TLM_SceneProperties.tlm_default_color[2] + result_pixel[i+0] = 0.0 + result_pixel[i+1] = 0.0 + result_pixel[i+2] = 0.0 + result_pixel[i+3] = 1.0 + + img.pixels = result_pixel + + img.name = img_name + else: + img = bpy.data.images[img_name] + + for slot in obj.material_slots: + mat = slot.material + mat.use_nodes = True + nodes = mat.node_tree.nodes + + if "Baked Image" in nodes: + img_node = nodes["Baked Image"] + else: + img_node = nodes.new('ShaderNodeTexImage') + img_node.name = 'Baked Image' + img_node.location = (100, 100) + img_node.image = img + img_node.select = True + nodes.active = img_node + +def set_settings(): + + scene = bpy.context.scene + cycles = scene.cycles + scene.render.engine = "CYCLES" + sceneProperties = scene.TLM_SceneProperties + engineProperties = scene.TLM_EngineProperties + cycles.device = scene.TLM_EngineProperties.tlm_mode + + if engineProperties.tlm_quality == "0": + cycles.samples = 32 + cycles.max_bounces = 1 + cycles.diffuse_bounces = 1 + cycles.glossy_bounces = 1 + cycles.transparent_max_bounces = 1 + cycles.transmission_bounces = 1 + cycles.volume_bounces = 1 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "1": + cycles.samples = 64 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "2": + cycles.samples = 512 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "3": + cycles.samples = 1024 + cycles.max_bounces = 256 + cycles.diffuse_bounces = 256 + cycles.glossy_bounces = 256 + cycles.transparent_max_bounces = 256 + cycles.transmission_bounces = 256 + cycles.volume_bounces = 256 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "4": + cycles.samples = 2048 + cycles.max_bounces = 512 + cycles.diffuse_bounces = 512 + cycles.glossy_bounces = 512 + cycles.transparent_max_bounces = 512 + cycles.transmission_bounces = 512 + cycles.volume_bounces = 512 + cycles.caustics_reflective = True + cycles.caustics_refractive = True + else: #Custom + pass + +def store_existing(prev_container): + + scene = bpy.context.scene + cycles = scene.cycles + + selected = [] + + for obj in bpy.data.objects: + if obj.select_get(): + selected.append(obj.name) + + prev_container["settings"] = [ + cycles.samples, + cycles.max_bounces, + cycles.diffuse_bounces, + cycles.glossy_bounces, + cycles.transparent_max_bounces, + cycles.transmission_bounces, + cycles.volume_bounces, + cycles.caustics_reflective, + cycles.caustics_refractive, + cycles.device, + scene.render.engine, + bpy.context.view_layer.objects.active, + selected + ] \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/denoiser/integrated.py b/blender/arm/lightmapper/utility/denoiser/integrated.py new file mode 100644 index 00000000..1a2867c4 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/integrated.py @@ -0,0 +1,79 @@ +import bpy, os + +class TLM_Integrated_Denoise: + + image_array = [] + image_output_destination = "" + + def load(self, images): + self.image_array = images + + self.cull_undefined() + + def setOutputDir(self, dir): + self.image_output_destination = dir + + def cull_undefined(self): + + #Do a validation check before denoising + + cam = bpy.context.scene.camera + if not cam: + bpy.ops.object.camera_add() + + #Just select the first camera we find, needed for the compositor + for obj in bpy.data.objects: + if obj.type == "CAMERA": + bpy.context.scene.camera = obj + return + + def denoise(self): + + if not bpy.context.scene.use_nodes: + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + for image in self.image_array: + + print("Image...: " + image) + + img = bpy.data.images.load(self.image_output_destination + "/" + image) + + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.image = img + image_node.location = 0, 0 + + denoise_node = tree.nodes.new(type='CompositorNodeDenoise') + denoise_node.location = 300, 0 + + comp_node = tree.nodes.new('CompositorNodeComposite') + comp_node.location = 600, 0 + + links = tree.links + links.new(image_node.outputs[0], denoise_node.inputs[0]) + links.new(denoise_node.outputs[0], comp_node.inputs[0]) + + # set output resolution to image res + bpy.context.scene.render.resolution_x = img.size[0] + bpy.context.scene.render.resolution_y = img.size[1] + bpy.context.scene.render.resolution_percentage = 100 + + filePath = bpy.data.filepath + path = os.path.dirname(filePath) + + base = os.path.basename(image) + filename, file_extension = os.path.splitext(image) + filename = filename[:-6] + + bpy.data.scenes["Scene"].render.filepath = self.image_output_destination + "/" + filename + "_denoised" + file_extension + + denoised_image_path = self.image_output_destination + bpy.data.scenes["Scene"].render.image_settings.file_format = "HDR" + + bpy.ops.render.render(write_still=True) + + #Cleanup + comp_nodes = [image_node, denoise_node, comp_node] + for node in comp_nodes: + tree.nodes.remove(node) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/denoiser/oidn.py b/blender/arm/lightmapper/utility/denoiser/oidn.py new file mode 100644 index 00000000..c96f4552 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/oidn.py @@ -0,0 +1,200 @@ +import bpy, os, sys, re, platform, subprocess +import numpy as np + +class TLM_OIDN_Denoise: + + image_array = [] + + image_output_destination = "" + + denoised_array = [] + + def __init__(self, oidnProperties, img_array, dirpath): + + self.oidnProperties = oidnProperties + + self.image_array = img_array + + self.image_output_destination = dirpath + + self.check_binary() + + def check_binary(self): + + oidnPath = self.oidnProperties.tlm_oidn_path + + if oidnPath != "": + + file = os.path.basename(os.path.realpath(oidnPath)) + filename, file_extension = os.path.splitext(file) + + if(file_extension == ".exe"): + + #if file exists oidnDenoise or denoise + + pass + + else: + + #if file exists oidnDenoise or denoise + + self.oidnProperties.tlm_oidn_path = os.path.join(self.oidnProperties.tlm_oidn_path,"oidnDenoise.exe") + + else: + + print("Please provide OIDN path") + + def denoise(self): + + for image in self.image_array: + + if image not in self.denoised_array: + + image_path = os.path.join(self.image_output_destination, image) + + #Save to pfm + loaded_image = bpy.data.images.load(image_path, check_existing=False) + + width = loaded_image.size[0] + height = loaded_image.size[1] + + image_output_array = np.zeros([width, height, 3], dtype="float32") + image_output_array = np.array(loaded_image.pixels) + image_output_array = image_output_array.reshape(height, width, 4) + image_output_array = np.float32(image_output_array[:,:,:3]) + + image_output_denoise_destination = image_path[:-4] + ".pfm" + + image_output_denoise_result_destination = image_path[:-4] + "_denoised.pfm" + + with open(image_output_denoise_destination, "wb") as fileWritePFM: + self.save_pfm(fileWritePFM, image_output_array) + + #Denoise + print("Loaded image: " + str(loaded_image)) + + verbose = self.oidnProperties.tlm_oidn_verbose + affinity = self.oidnProperties.tlm_oidn_affinity + + if verbose: + print("Denoiser search: " + bpy.path.abspath(self.oidnProperties.tlm_oidn_path)) + v = "3" + else: + v = "0" + + if affinity: + a = "1" + else: + a = "0" + + threads = str(self.oidnProperties.tlm_oidn_threads) + maxmem = str(self.oidnProperties.tlm_oidn_maxmem) + + if platform.system() == 'Windows': + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + pipePath = [oidnPath, '-f', 'RTLightmap', '-hdr', image_output_denoise_destination, '-o', image_output_denoise_result_destination, '-verbose', v, '-threads', threads, '-affinity', a, '-maxmem', maxmem] + elif platform.system() == 'Darwin': + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v] + else: + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v] + + if not verbose: + denoisePipe = subprocess.Popen(pipePath, stdout=subprocess.PIPE, stderr=None, shell=True) + else: + denoisePipe = subprocess.Popen(pipePath, shell=True) + + denoisePipe.communicate()[0] + + with open(image_output_denoise_result_destination, "rb") as f: + denoise_data, scale = self.load_pfm(f) + + ndata = np.array(denoise_data) + ndata2 = np.dstack((ndata, np.ones((width,height)))) + img_array = ndata2.ravel() + + loaded_image.pixels = img_array + loaded_image.filepath_raw = image_output_denoise_result_destination = image_path[:-10] + "_denoised.hdr" + loaded_image.file_format = "HDR" + loaded_image.save() + + self.denoised_array.append(image) + + print(image_path) + + def clean(self): + + self.denoised_array.clear() + self.image_array.clear() + + for file in self.image_output_destination: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + #self.image_output_destination + + #Clean temporary files here.. + #...pfm + #...denoised.hdr + + + def load_pfm(self, file, as_flat_list=False): + #start = time() + + header = file.readline().decode("utf-8").rstrip() + if header == "PF": + color = True + elif header == "Pf": + color = False + else: + raise Exception("Not a PFM file.") + + dim_match = re.match(r"^(\d+)\s(\d+)\s$", file.readline().decode("utf-8")) + if dim_match: + width, height = map(int, dim_match.groups()) + else: + raise Exception("Malformed PFM header.") + + scale = float(file.readline().decode("utf-8").rstrip()) + if scale < 0: # little-endian + endian = "<" + scale = -scale + else: + endian = ">" # big-endian + + data = np.fromfile(file, endian + "f") + shape = (height, width, 3) if color else (height, width) + if as_flat_list: + result = data + else: + result = np.reshape(data, shape) + #print("PFM import took %.3f s" % (time() - start)) + return result, scale + + def save_pfm(self, file, image, scale=1): + #start = time() + + if image.dtype.name != "float32": + raise Exception("Image dtype must be float32 (got %s)" % image.dtype.name) + + if len(image.shape) == 3 and image.shape[2] == 3: # color image + color = True + elif len(image.shape) == 2 or len(image.shape) == 3 and image.shape[2] == 1: # greyscale + color = False + else: + raise Exception("Image must have H x W x 3, H x W x 1 or H x W dimensions.") + + file.write(b"PF\n" if color else b"Pf\n") + file.write(b"%d %d\n" % (image.shape[1], image.shape[0])) + + endian = image.dtype.byteorder + + if endian == "<" or endian == "=" and sys.byteorder == "little": + scale = -scale + + file.write(b"%f\n" % scale) + + image.tofile(file) + + #print("PFM export took %.3f s" % (time() - start)) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/denoiser/optix.py b/blender/arm/lightmapper/utility/denoiser/optix.py new file mode 100644 index 00000000..eae92b27 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/optix.py @@ -0,0 +1,7 @@ +import bpy, os + +class TLM_OIDN_Denoise: + + def __init__(self): + + pass \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/encoding.py b/blender/arm/lightmapper/utility/encoding.py new file mode 100644 index 00000000..bfc4cd7c --- /dev/null +++ b/blender/arm/lightmapper/utility/encoding.py @@ -0,0 +1,245 @@ +import bpy, math, os, gpu, bgl +import numpy as np +from . import utility +from fractions import Fraction +from gpu_extras.batch import batch_for_shader + +def encodeLogLuv(image, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + const mat3 cLogLuvM = mat3( 0.2209, 0.3390, 0.4184, 0.1138, 0.6780, 0.7319, 0.0102, 0.1130, 0.2969 ); + vec4 LinearToLogLuv( in vec4 value ) { + vec3 Xp_Y_XYZp = cLogLuvM * value.rgb; + Xp_Y_XYZp = max( Xp_Y_XYZp, vec3( 1e-6, 1e-6, 1e-6 ) ); + vec4 vResult; + vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z; + float Le = 2.0 * log2(Xp_Y_XYZp.y) + 127.0; + vResult.w = fract( Le ); + vResult.z = ( Le - ( floor( vResult.w * 255.0 ) ) / 255.0 ) / 255.0; + return vResult; + //return vec4(Xp_Y_XYZp,1); + } + + const mat3 cLogLuvInverseM = mat3( 6.0014, -2.7008, -1.7996, -1.3320, 3.1029, -5.7721, 0.3008, -1.0882, 5.6268 ); + vec4 LogLuvToLinear( in vec4 value ) { + float Le = value.z * 255.0 + value.w; + vec3 Xp_Y_XYZp; + Xp_Y_XYZp.y = exp2( ( Le - 127.0 ) / 2.0 ); + Xp_Y_XYZp.z = Xp_Y_XYZp.y / value.y; + Xp_Y_XYZp.x = value.x * Xp_Y_XYZp.z; + vec3 vRGB = cLogLuvInverseM * Xp_Y_XYZp.rgb; + //return vec4( max( vRGB, 0.0 ), 1.0 ); + return vec4( max( Xp_Y_XYZp, 0.0 ), 1.0 ); + } + + void main() + { + //fragColor = LinearToLogLuv(pow(texture(image, texCoord_interp), vec4(0.454))); + fragColor = LinearToLogLuv(texture(image, texCoord_interp)); + //fragColor = LogLuvToLinear(LinearToLogLuv(texture(image, texCoord_interp))); + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + + #Todo - Find a way to save + #bpy.ops.image.save_all_modified() + +def encodeImageRGBM(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + num_pixels = len(input_image.pixels) + result_pixel = list(input_image.pixels) + + for i in range(0,num_pixels,4): + for j in range(3): + result_pixel[i+j] *= 1.0 / maxRange; + result_pixel[i+3] = saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6)) + result_pixel[i+3] = math.ceil(result_pixel[i+3] * 255.0) / 255.0 + for j in range(3): + result_pixel[i+j] /= result_pixel[i+3] + + target_image.pixels = result_pixel + input_image = target_image + + #Save RGBM + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + input_image.save() + + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + # input_image.filepath_raw = outDir + "_encoded.png" + # input_image.file_format = "PNG" + # bpy.context.scene.render.image_settings.quality = quality + # input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + #input_image. + #input_image.save() + +def saturate(num, floats=True): + if num < 0: + num = 0 + elif num > (1 if floats else 255): + num = (1 if floats else 255) + return num + +def encodeImageRGBD(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + num_pixels = len(input_image.pixels) + result_pixel = list(input_image.pixels) + + for i in range(0,num_pixels,4): + + m = utility.saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6)) + d = max(maxRange / m, 1) + d = utility.saturate(math.floor(d) / 255 ) + + result_pixel[i] = result_pixel[i] * d * 255 / maxRange + result_pixel[i+1] = result_pixel[i+1] * d * 255 / maxRange + result_pixel[i+2] = result_pixel[i+2] * d * 255 / maxRange + result_pixel[i+3] = d + + target_image.pixels = result_pixel + + input_image = target_image + + #Save RGBD + input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/numpy.py b/blender/arm/lightmapper/utility/filtering/numpy.py new file mode 100644 index 00000000..7922dbd9 --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/numpy.py @@ -0,0 +1,49 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_NP_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + #opencv_process_image = cv2.imread(file_input, -1) + + print("Filtering: " + file_input) + + print(os.path.join(lightmap_dir, file)) + + if scene.TLM_SceneProperties.tlm_numpy_filtering_mode == "3x3 blur": + pass + + #filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + #cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/opencv.py b/blender/arm/lightmapper/utility/filtering/opencv.py new file mode 100644 index 00000000..69e27679 --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/opencv.py @@ -0,0 +1,160 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_CV_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + opencv_process_image = cv2.imread(file_input, -1) + + print("Filtering: " + os.path.basename(file_input)) + + obj_name = os.path.basename(file_input).split("_")[0] + + if bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override: + + print("OVERRIDE!") + + print(os.path.join(lightmap_dir, file)) + + objectProperties = bpy.data.objects[obj_name].TLM_ObjectProperties + + #TODO OVERRIDE FILTERING OPTION! REWRITE + if objectProperties.tlm_mesh_filtering_mode == "Box": + if objectProperties.tlm_mesh_filtering_box_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength + 1, objectProperties.tlm_mesh_filtering_box_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength, objectProperties.tlm_mesh_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Gaussian": + if objectProperties.tlm_mesh_filtering_gaussian_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength + 1, objectProperties.tlm_mesh_filtering_gaussian_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength, objectProperties.tlm_mesh_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Bilateral": + diameter_size = objectProperties.tlm_mesh_filtering_bilateral_diameter + sigma_color = objectProperties.tlm_mesh_filtering_bilateral_color_deviation + sigma_space = objectProperties.tlm_mesh_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if objectProperties.tlm_mesh_filtering_median_kernel % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel + 1, objectProperties.tlm_mesh_filtering_median_kernel + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel, objectProperties.tlm_mesh_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + else: + + print(os.path.join(lightmap_dir, file)) + + #TODO OVERRIDE FILTERING OPTION! + if scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + if scene.TLM_SceneProperties.tlm_filtering_box_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength + 1,scene.TLM_SceneProperties.tlm_filtering_box_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength,scene.TLM_SceneProperties.tlm_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + if scene.TLM_SceneProperties.tlm_filtering_gaussian_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + diameter_size = scene.TLM_SceneProperties.tlm_filtering_bilateral_diameter + sigma_color = scene.TLM_SceneProperties.tlm_filtering_bilateral_color_deviation + sigma_space = scene.TLM_SceneProperties.tlm_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if scene.TLM_SceneProperties.tlm_filtering_median_kernel % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1 , scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel, scene.TLM_SceneProperties.tlm_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + # if file.endswith(file_ending): + # print() + # baked_image_array.append(file) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/shader.py b/blender/arm/lightmapper/utility/filtering/shader.py new file mode 100644 index 00000000..55028702 --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/shader.py @@ -0,0 +1,160 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_Shader_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + opencv_process_image = cv2.imread(file_input, -1) + + print("Filtering: " + os.path.basename(file_input)) + + obj_name = os.path.basename(file_input).split("_")[0] + + if bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override: + + print("OVERRIDE!") + + print(os.path.join(lightmap_dir, file)) + + objectProperties = bpy.data.objects[obj_name].TLM_ObjectProperties + + #TODO OVERRIDE FILTERING OPTION! REWRITE + if objectProperties.tlm_mesh_filtering_mode == "Box": + if objectProperties.tlm_mesh_filtering_box_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength + 1, objectProperties.tlm_mesh_filtering_box_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength, objectProperties.tlm_mesh_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Gaussian": + if objectProperties.tlm_mesh_filtering_gaussian_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength + 1, objectProperties.tlm_mesh_filtering_gaussian_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength, objectProperties.tlm_mesh_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Bilateral": + diameter_size = objectProperties.tlm_mesh_filtering_bilateral_diameter + sigma_color = objectProperties.tlm_mesh_filtering_bilateral_color_deviation + sigma_space = objectProperties.tlm_mesh_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if objectProperties.tlm_mesh_filtering_median_kernel % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel + 1, objectProperties.tlm_mesh_filtering_median_kernel + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel, objectProperties.tlm_mesh_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + else: + + print(os.path.join(lightmap_dir, file)) + + #TODO OVERRIDE FILTERING OPTION! + if scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + if scene.TLM_SceneProperties.tlm_filtering_box_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength + 1,scene.TLM_SceneProperties.tlm_filtering_box_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength,scene.TLM_SceneProperties.tlm_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + if scene.TLM_SceneProperties.tlm_filtering_gaussian_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + diameter_size = scene.TLM_SceneProperties.tlm_filtering_bilateral_diameter + sigma_color = scene.TLM_SceneProperties.tlm_filtering_bilateral_color_deviation + sigma_space = scene.TLM_SceneProperties.tlm_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if scene.TLM_SceneProperties.tlm_filtering_median_kernel % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1 , scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel, scene.TLM_SceneProperties.tlm_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + # if file.endswith(file_ending): + # print() + # baked_image_array.append(file) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/icon.py b/blender/arm/lightmapper/utility/icon.py new file mode 100644 index 00000000..54f8acd8 --- /dev/null +++ b/blender/arm/lightmapper/utility/icon.py @@ -0,0 +1,31 @@ +import os +import bpy + +from bpy.utils import previews + +icons = None +directory = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'icons')) + +def id(identifier): + return image(identifier).icon_id + +def image(identifier): + def icon(identifier): + if identifier in icons: + return icons[identifier] + return icons.load(identifier, os.path.join(directory, identifier + '.png'), 'IMAGE') + + if icons: + return icon(identifier) + else: + create() + return icon(identifier) + + +def create(): + global icons + icons = previews.new() + + +def remove(): + previews.remove(icons) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/preconfiguration/object.py b/blender/arm/lightmapper/utility/preconfiguration/object.py new file mode 100644 index 00000000..103749f5 --- /dev/null +++ b/blender/arm/lightmapper/utility/preconfiguration/object.py @@ -0,0 +1,5 @@ +import bpy, os, re, sys + +def prepare(obj): + print("Preparing: " + obj.name) + pass \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/utility.py b/blender/arm/lightmapper/utility/utility.py new file mode 100644 index 00000000..6849c2bf --- /dev/null +++ b/blender/arm/lightmapper/utility/utility.py @@ -0,0 +1,620 @@ +import bpy.ops as O +import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh +from io import StringIO +from threading import Thread +from queue import Queue, Empty +from dataclasses import dataclass +from dataclasses import field +from typing import List + +########################################################### +########################################################### +# This set of utility functions are courtesy of LorenzWieseke +# +# Modified by Naxela +# +# https://github.com/Naxela/The_Lightmapper/tree/Lightmap-to-GLB +########################################################### + +class Node_Types: + output_node = 'OUTPUT_MATERIAL' + ao_node = 'AMBIENT_OCCLUSION' + image_texture = 'TEX_IMAGE' + pbr_node = 'BSDF_PRINCIPLED' + diffuse = 'BSDF_DIFFUSE' + mapping = 'MAPPING' + normal_map = 'NORMAL_MAP' + bump_map = 'BUMP' + attr_node = 'ATTRIBUTE' + +class Shader_Node_Types: + emission = "ShaderNodeEmission" + image_texture = "ShaderNodeTexImage" + mapping = "ShaderNodeMapping" + normal = "ShaderNodeNormalMap" + ao = "ShaderNodeAmbientOcclusion" + uv = "ShaderNodeUVMap" + mix = "ShaderNodeMixRGB" + +def select_object(self,obj): + C = bpy.context + try: + O.object.select_all(action='DESELECT') + C.view_layer.objects.active = obj + obj.select_set(True) + except: + self.report({'INFO'},"Object not in View Layer") + + +def select_obj_by_mat(self,mat): + D = bpy.data + for obj in D.objects: + if obj.type == "MESH": + object_materials = [ + slot.material for slot in obj.material_slots] + if mat in object_materials: + select_object(self,obj) + + +def save_image(image): + + filePath = bpy.data.filepath + path = os.path.dirname(filePath) + + try: + os.mkdir(path + "/tex") + except FileExistsError: + pass + + try: + os.mkdir(path + "/tex/" + str(image.size[0])) + except FileExistsError: + pass + + if image.file_format == "JPEG" : + file_ending = ".jpg" + elif image.file_format == "PNG" : + file_ending = ".png" + + savepath = path + "/tex/" + \ + str(image.size[0]) + "/" + image.name + file_ending + + image.filepath_raw = savepath + + # if "Normal" in image.name: + # bpy.context.scene.render.image_settings.quality = 90 + # image.save_render( filepath = image.filepath_raw, scene = bpy.context.scene ) + # else: + image.save() + + + + +def get_file_size(filepath): + size = "Unpack Files" + try: + path = bpy.path.abspath(filepath) + size = os.path.getsize(path) + size /= 1024 + except: + print("error getting file path for " + filepath) + + return (size) + + +def scale_image(image, newSize): + if (image.org_filepath != ''): + image.filepath = image.org_filepath + + image.org_filepath = image.filepath + image.scale(newSize[0], newSize[1]) + save_image(image) + + +def check_only_one_pbr(self,material): + check_ok = True + # get pbr shader + nodes = material.node_tree.nodes + pbr_node_type = Node_Types.pbr_node + pbr_nodes = find_node_by_type(nodes,pbr_node_type) + + # check only one pbr node + if len(pbr_nodes) == 0: + self.report({'INFO'}, 'No PBR Shader Found') + check_ok = False + + if len(pbr_nodes) > 1: + self.report({'INFO'}, 'More than one PBR Node found ! Clean before Baking.') + check_ok = False + + return check_ok + +# is material already the baked one +def check_is_org_material(self,material): + check_ok = True + if "_Bake" in material.name: + self.report({'INFO'}, 'Change back to org. Material') + check_ok = False + + return check_ok + + +def clean_empty_materials(self): + for obj in bpy.data.objects: + for slot in obj.material_slots: + mat = slot.material + if mat is None: + print("Removed Empty Materials from " + obj.name) + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.material_slot_remove() + +def get_pbr_inputs(pbr_node): + + base_color_input = pbr_node.inputs["Base Color"] + metallic_input = pbr_node.inputs["Metallic"] + specular_input = pbr_node.inputs["Specular"] + roughness_input = pbr_node.inputs["Roughness"] + normal_input = pbr_node.inputs["Normal"] + + pbr_inputs = {"base_color_input":base_color_input, "metallic_input":metallic_input,"specular_input":specular_input,"roughness_input":roughness_input,"normal_input":normal_input} + return pbr_inputs + +def find_node_by_type(nodes, node_type): + nodes_found = [n for n in nodes if n.type == node_type] + return nodes_found + +def find_node_by_type_recusivly(material, note_to_start, node_type, del_nodes_inbetween=False): + nodes = material.node_tree.nodes + if note_to_start.type == node_type: + return note_to_start + + for input in note_to_start.inputs: + for link in input.links: + current_node = link.from_node + if (del_nodes_inbetween and note_to_start.type != Node_Types.normal_map and note_to_start.type != Node_Types.bump_map): + nodes.remove(note_to_start) + return find_node_by_type_recusivly(material, current_node, node_type, del_nodes_inbetween) + + +def find_node_by_name_recusivly(node, idname): + if node.bl_idname == idname: + return node + + for input in node.inputs: + for link in input.links: + current_node = link.from_node + return find_node_by_name_recusivly(current_node, idname) + +def make_link(material, socket1, socket2): + links = material.node_tree.links + links.new(socket1, socket2) + + +def add_gamma_node(material, pbrInput): + nodeToPrincipledOutput = pbrInput.links[0].from_socket + + gammaNode = material.node_tree.nodes.new("ShaderNodeGamma") + gammaNode.inputs[1].default_value = 2.2 + gammaNode.name = "Gamma Bake" + + # link in gamma + make_link(material, nodeToPrincipledOutput, gammaNode.inputs["Color"]) + make_link(material, gammaNode.outputs["Color"], pbrInput) + + +def remove_gamma_node(material, pbrInput): + nodes = material.node_tree.nodes + gammaNode = nodes.get("Gamma Bake") + nodeToPrincipledOutput = gammaNode.inputs[0].links[0].from_socket + + make_link(material, nodeToPrincipledOutput, pbrInput) + material.node_tree.nodes.remove(gammaNode) + +def apply_ao_toggle(self,context): + all_materials = bpy.data.materials + ao_toggle = context.scene.toggle_ao + for mat in all_materials: + nodes = mat.node_tree.nodes + ao_node = nodes.get("AO Bake") + if ao_node is not None: + if ao_toggle: + emission_setup(mat,ao_node.outputs["Color"]) + else: + pbr_node = find_node_by_type(nodes,Node_Types.pbr_node)[0] + remove_node(mat,"Emission Bake") + reconnect_PBR(mat, pbr_node) + + +def emission_setup(material, node_output): + nodes = material.node_tree.nodes + emission_node = add_node(material,Shader_Node_Types.emission,"Emission Bake") + + # link emission to whatever goes into current pbrInput + emission_input = emission_node.inputs[0] + make_link(material, node_output, emission_input) + + # link emission to materialOutput + surface_input = nodes.get("Material Output").inputs[0] + emission_output = emission_node.outputs[0] + make_link(material, emission_output, surface_input) + +def link_pbr_to_output(material,pbr_node): + nodes = material.node_tree.nodes + surface_input = nodes.get("Material Output").inputs[0] + make_link(material,pbr_node.outputs[0],surface_input) + + +def reconnect_PBR(material, pbrNode): + nodes = material.node_tree.nodes + pbr_output = pbrNode.outputs[0] + surface_input = nodes.get("Material Output").inputs[0] + make_link(material, pbr_output, surface_input) + +def mute_all_texture_mappings(material, do_mute): + nodes = material.node_tree.nodes + for node in nodes: + if node.bl_idname == "ShaderNodeMapping": + node.mute = do_mute + +def add_node(material,shader_node_type,node_name): + nodes = material.node_tree.nodes + new_node = nodes.get(node_name) + if new_node is None: + new_node = nodes.new(shader_node_type) + new_node.name = node_name + new_node.label = node_name + return new_node + +def remove_node(material,node_name): + nodes = material.node_tree.nodes + node = nodes.get(node_name) + if node is not None: + nodes.remove(node) + +def lightmap_to_ao(material,lightmap_node): + nodes = material.node_tree.nodes + # -----------------------AO SETUP--------------------# + # create group data + gltf_settings = bpy.data.node_groups.get('glTF Settings') + if gltf_settings is None: + bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree') + + # add group to node tree + ao_group = nodes.get('glTF Settings') + if ao_group is None: + ao_group = nodes.new('ShaderNodeGroup') + ao_group.name = 'glTF Settings' + ao_group.node_tree = bpy.data.node_groups['glTF Settings'] + + # create group inputs + if ao_group.inputs.get('Occlusion') is None: + ao_group.inputs.new('NodeSocketFloat','Occlusion') + + # mulitply to control strength + mix_node = add_node(material,Shader_Node_Types.mix,"Adjust Lightmap") + mix_node.blend_type = "MULTIPLY" + mix_node.inputs["Fac"].default_value = 1 + mix_node.inputs["Color2"].default_value = [3,3,3,1] + + # position node + ao_group.location = (lightmap_node.location[0]+600,lightmap_node.location[1]) + mix_node.location = (lightmap_node.location[0]+300,lightmap_node.location[1]) + + make_link(material,lightmap_node.outputs['Color'],mix_node.inputs['Color1']) + make_link(material,mix_node.outputs['Color'],ao_group.inputs['Occlusion']) + + +########################################################### +########################################################### +# This utility function is modified from blender_xatlas +# and calls the object without any explicit object context +# thus allowing blender_xatlas to pack from background. +########################################################### +# Code is courtesy of mattedicksoncom +# Modified by Naxela +# +# https://github.com/mattedicksoncom/blender-xatlas/ +########################################################### + +def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): + + blender_xatlas = importlib.util.find_spec("blender_xatlas") + + if blender_xatlas is not None: + import blender_xatlas + else: + return 0 + + packOptions = bpy.context.scene.pack_tool + chartOptions = bpy.context.scene.chart_tool + sharedProperties = bpy.context.scene.shared_properties + + context = bpy.context + + if obj.type == 'MESH': + context.view_layer.objects.active = obj + if obj.data.users > 1: + obj.data = obj.data.copy() #make single user copy + uv_layers = obj.data.uv_layers + + #setup the lightmap uvs + uvName = "UVMap_Lightmap" + if sharedProperties.lightmapUVChoiceType == "NAME": + uvName = sharedProperties.lightmapUVName + elif sharedProperties.lightmapUVChoiceType == "INDEX": + if sharedProperties.lightmapUVIndex < len(uv_layers): + uvName = uv_layers[sharedProperties.lightmapUVIndex].name + + if not uvName in uv_layers: + uvmap = uv_layers.new(name=uvName) + uv_layers.active_index = len(uv_layers) - 1 + else: + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uvName: + uv_layers.active_index = i + obj.select_set(True) + + #save all the current edges + if sharedProperties.packOnly: + edgeDict = dict() + for obj in selected_objects: + if obj.type == 'MESH': + tempEdgeDict = dict() + tempEdgeDict['object'] = obj.name + tempEdgeDict['edges'] = [] + print(len(obj.data.edges)) + for i in range(0,len(obj.data.edges)): + setEdge = obj.data.edges[i] + tempEdgeDict['edges'].append(i) + edgeDict[obj.name] = tempEdgeDict + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') + else: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') + + bpy.ops.object.mode_set(mode='OBJECT') + + fakeFile = StringIO() + blender_xatlas.export_obj_simple.save( + context=bpy.context, + filepath=fakeFile, + mainUVChoiceType=sharedProperties.mainUVChoiceType, + uvIndex=sharedProperties.mainUVIndex, + uvName=sharedProperties.mainUVName, + use_selection=True, + use_animation=False, + use_mesh_modifiers=True, + use_edges=True, + use_smooth_groups=False, + use_smooth_groups_bitflags=False, + use_normals=True, + use_uvs=True, + use_materials=False, + use_triangles=False, + use_nurbs=False, + use_vertex_groups=False, + use_blen_objects=True, + group_by_object=False, + group_by_material=False, + keep_vertex_order=False, + ) + + file_path = os.path.dirname(os.path.abspath(blender_xatlas.__file__)) + if platform.system() == "Windows": + xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe") + elif platform.system() == "Linux": + xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender") + #need to set permissions for the process on linux + subprocess.Popen( + 'chmod u+x "' + xatlas_path + '"', + shell=True + ) + + #setup the arguments to be passed to xatlas------------------- + arguments_string = "" + for argumentKey in packOptions.__annotations__.keys(): + key_string = str(argumentKey) + if argumentKey is not None: + print(getattr(packOptions,key_string)) + attrib = getattr(packOptions,key_string) + if type(attrib) == bool: + if attrib == True: + arguments_string = arguments_string + " -" + str(argumentKey) + else: + arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) + + for argumentKey in chartOptions.__annotations__.keys(): + if argumentKey is not None: + key_string = str(argumentKey) + print(getattr(chartOptions,key_string)) + attrib = getattr(chartOptions,key_string) + if type(attrib) == bool: + if attrib == True: + arguments_string = arguments_string + " -" + str(argumentKey) + else: + arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) + + #add pack only option + if sharedProperties.packOnly: + arguments_string = arguments_string + " -packOnly" + + arguments_string = arguments_string + " -atlasLayout" + " " + sharedProperties.atlasLayout + + print(arguments_string) + #END setup the arguments to be passed to xatlas------------------- + + #RUN xatlas process + xatlas_process = subprocess.Popen( + r'"{}"'.format(xatlas_path) + ' ' + arguments_string, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=True + ) + + #shove the fake file in stdin + stdin = xatlas_process.stdin + value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly + stdin.write(value) + stdin.flush() + + #Get the output from xatlas + outObj = "" + while True: + output = xatlas_process.stdout.readline() + if not output: + break + outObj = outObj + (output.decode().strip() + "\n") + + #the objects after xatlas processing + # print(outObj) + + + #Setup for reading the output + @dataclass + class uvObject: + obName: string = "" + uvArray: List[float] = field(default_factory=list) + faceArray: List[int] = field(default_factory=list) + + convertedObjects = [] + uvArrayComplete = [] + + + #search through the out put for STARTOBJ + #then start reading the objects + obTest = None + startRead = False + for line in outObj.splitlines(): + + line_split = line.split() + + if not line_split: + continue + + line_start = line_split[0] # we compare with this a _lot_ + # print(line_start) + if line_start == "STARTOBJ": + print("Start reading the objects----------------------------------------") + startRead = True + # obTest = uvObject() + + if startRead: + #if it's a new obj + if line_start == 'o': + #if there is already an object append it + if obTest is not None: + convertedObjects.append(obTest) + + obTest = uvObject() #create new uv object + obTest.obName = line_split[1] + + if obTest is not None: + #the uv coords + if line_start == 'vt': + newUv = [float(line_split[1]),float(line_split[2])] + obTest.uvArray.append(newUv) + uvArrayComplete.append(newUv) + + #the face coords index + #faces are 1 indexed + if line_start == 'f': + #vert/uv/normal + #only need the uvs + newFace = [ + int(line_split[1].split("/")[1]), + int(line_split[2].split("/")[1]), + int(line_split[3].split("/")[1]) + ] + obTest.faceArray.append(newFace) + + #append the final object + convertedObjects.append(obTest) + # print(convertedObjects) + + + #apply the output------------------------------------------------------------- + #copy the uvs to the original objects + # objIndex = 0 + print("Applying the UVs----------------------------------------") + # print(convertedObjects) + for importObject in convertedObjects: + bpy.ops.object.select_all(action='DESELECT') + + obTest = importObject + + bpy.context.scene.objects[obTest.obName].select_set(True) + context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName] + bpy.ops.object.mode_set(mode = 'OBJECT') + + obj = bpy.context.active_object + me = obj.data + #convert to bmesh to create the new uvs + bm = bmesh.new() + bm.from_mesh(me) + + uv_layer = bm.loops.layers.uv.verify() + + nFaces = len(bm.faces) + #need to ensure lookup table for some reason? + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + + #loop through the faces + for faceIndex in range(nFaces): + faceGroup = obTest.faceArray[faceIndex] + + bm.faces[faceIndex].loops[0][uv_layer].uv = ( + uvArrayComplete[faceGroup[0] - 1][0], + uvArrayComplete[faceGroup[0] - 1][1]) + + bm.faces[faceIndex].loops[1][uv_layer].uv = ( + uvArrayComplete[faceGroup[1] - 1][0], + uvArrayComplete[faceGroup[1] - 1][1]) + + bm.faces[faceIndex].loops[2][uv_layer].uv = ( + uvArrayComplete[faceGroup[2] - 1][0], + uvArrayComplete[faceGroup[2] - 1][1]) + + # objIndex = objIndex + 3 + + # print(objIndex) + #assign the mesh back to the original mesh + bm.to_mesh(me) + #END apply the output------------------------------------------------------------- + + + #Start setting the quads back again------------------------------------------------------------- + if sharedProperties.packOnly: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + for edges in edgeDict: + edgeList = edgeDict[edges] + currentObject = bpy.context.scene.objects[edgeList['object']] + bm = bmesh.new() + bm.from_mesh(currentObject.data) + if hasattr(bm.edges, "ensure_lookup_table"): + bm.edges.ensure_lookup_table() + + #assume that all the triangulated edges come after the original edges + newEdges = [] + for edge in range(len(edgeList['edges']), len(bm.edges)): + newEdge = bm.edges[edge] + newEdge.select = True + newEdges.append(newEdge) + + bmesh.ops.dissolve_edges(bm, edges=newEdges, use_verts=False, use_face_split=False) + bpy.ops.object.mode_set(mode='OBJECT') + bm.to_mesh(currentObject.data) + bm.free() + bpy.ops.object.mode_set(mode='EDIT') + + #End setting the quads back again------------------------------------------------------------ + + print("Finished Xatlas----------------------------------------") \ No newline at end of file diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 938fb830..062f8eff 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -12,6 +12,9 @@ import arm.props_properties import arm.proxy import arm.utils +from arm.lightmapper.utility import icon +from arm.lightmapper.properties.denoiser import oidn, optix + # Menu in object region class ARM_PT_ObjectPropsPanel(bpy.types.Panel): bl_label = "Armory Props" @@ -1030,38 +1033,274 @@ class ARM_PT_BakePanel(bpy.types.Panel): scn = bpy.data.scenes[context.scene.name] row = layout.row(align=True) - row.alignment = 'EXPAND' - row.operator("arm.bake_textures", icon="RENDER_STILL") - row.operator("arm.bake_apply") + row.prop(scn, "arm_bakemode", expand=True) - col = layout.column() - col.prop(scn, 'arm_bakelist_scale') - col.prop(scn.cycles, "samples") + if scn.arm_bakemode == "Static Map": - layout.prop(scn, 'arm_bakelist_unwrap') + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("arm.bake_textures", icon="RENDER_STILL") + row.operator("arm.bake_apply") - rows = 2 - if len(scn.arm_bakelist) > 1: - rows = 4 - row = layout.row() - row.template_list("ARM_UL_BakeList", "The_List", scn, "arm_bakelist", scn, "arm_bakelist_index", rows=rows) - col = row.column(align=True) - col.operator("arm_bakelist.new_item", icon='ADD', text="") - col.operator("arm_bakelist.delete_item", icon='REMOVE', text="") - col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") + col = layout.column() + col.prop(scn, 'arm_bakelist_scale') + col.prop(scn.cycles, "samples") - if len(scn.arm_bakelist) > 1: - col.separator() - op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="") - op.direction = 'UP' - op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="") - op.direction = 'DOWN' + layout.prop(scn, 'arm_bakelist_unwrap') - if scn.arm_bakelist_index >= 0 and len(scn.arm_bakelist) > 0: - item = scn.arm_bakelist[scn.arm_bakelist_index] - layout.prop_search(item, "obj", bpy.data, "objects", text="Object") - layout.prop(item, "res_x") - layout.prop(item, "res_y") + rows = 2 + if len(scn.arm_bakelist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_BakeList", "The_List", scn, "arm_bakelist", scn, "arm_bakelist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_bakelist.new_item", icon='ADD', text="") + col.operator("arm_bakelist.delete_item", icon='REMOVE', text="") + col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") + + if len(scn.arm_bakelist) > 1: + col.separator() + op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if scn.arm_bakelist_index >= 0 and len(scn.arm_bakelist) > 0: + item = scn.arm_bakelist[scn.arm_bakelist_index] + layout.prop_search(item, "obj", bpy.data, "objects", text="Object") + layout.prop(item, "res_x") + layout.prop(item, "res_y") + + else: + + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + row = layout.row(align=True) + + row = layout.row(align=True) + + #We list LuxCoreRender as available, by default we assume Cycles exists + row.prop(sceneProperties, "tlm_lightmap_engine") + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + #CYCLES SETTINGS HERE + engineProperties = scene.TLM_EngineProperties + + row = layout.row(align=True) + row.label(text="General Settings") + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_apply_on_unwrap") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_headless") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_on_finish") + + row = layout.row(align=True) + row.label(text="Cycles Settings") + + row = layout.row(align=True) + row.prop(engineProperties, "tlm_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_quality") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_resolution_scale") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_bake_mode") + + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row = layout.row(align=True) + row.label(text="Warning! Background mode is currently unstable", icon_value=2) + row = layout.row(align=True) + row.prop(engineProperties, "tlm_caching_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_directional_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_dilation_margin") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_exposure_multiplier") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_setting_supersample") + + elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + #LUXCORE SETTINGS HERE + luxcore_available = False + + #Look for Luxcorerender in the renderengine classes + for engine in bpy.types.RenderEngine.__subclasses__(): + if engine.bl_idname == "LUXCORE": + luxcore_available = True + break + + row = layout.row(align=True) + if not luxcore_available: + row.label(text="Please install BlendLuxCore.") + else: + row.label(text="LuxCoreRender not yet available.") + + elif sceneProperties.tlm_lightmap_engine == "OctaneRender": + + #LUXCORE SETTINGS HERE + octane_available = False + + row = layout.row(align=True) + row.label(text="Octane Render not yet available.") + + + ################## + #DENOISE SETTINGS! + row = layout.row(align=True) + row.label(text="Denoise Settings") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_denoise_use") + row = layout.row(align=True) + + if sceneProperties.tlm_denoise_use: + row.prop(sceneProperties, "tlm_denoise_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_denoise_engine == "Integrated": + row.label(text="No options for Integrated.") + elif sceneProperties.tlm_denoise_engine == "OIDN": + denoiseProperties = scene.TLM_OIDNEngineProperties + row.prop(denoiseProperties, "tlm_oidn_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_threads") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_maxmem") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_affinity") + # row = layout.row(align=True) + # row.prop(denoiseProperties, "tlm_denoise_ao") + elif sceneProperties.tlm_denoise_engine == "Optix": + denoiseProperties = scene.TLM_OptixEngineProperties + row.prop(denoiseProperties, "tlm_optix_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_maxmem") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_denoise_ao") + + + ################## + #FILTERING SETTINGS! + row = layout.row(align=True) + row.label(text="Filtering Settings") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_filtering_use") + row = layout.row(align=True) + + if sceneProperties.tlm_filtering_use: + + row.prop(sceneProperties, "tlm_filtering_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_filtering_engine == "OpenCV": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") + row = layout.row(align=True) + if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") + + + ################## + #ENCODING SETTINGS! + row = layout.row(align=True) + row.label(text="Encoding Settings") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_use") + row = layout.row(align=True) + + if sceneProperties.tlm_encoding_use: + + row.prop(sceneProperties, "tlm_encoding_mode", expand=True) + if sceneProperties.tlm_encoding_mode == "RGBM" or sceneProperties.tlm_encoding_mode == "RGBD": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + if sceneProperties.tlm_encoding_mode == "LogLuv": + pass + if sceneProperties.tlm_encoding_mode == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + + row = layout.row(align=True) + row.label(text="Encoding Settings") + row = layout.row(align=True) + + row = layout.row(align=True) + row.operator("tlm.enable_selection") + row = layout.row(align=True) + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_object_settings") + + if sceneProperties.tlm_override_object_settings: + + row = layout.row(align=True) + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": + + if scene.TLM_AtlasList_index >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasList_index] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) + class ArmGenLodButton(bpy.types.Operator): '''Automatically generate LoD levels''' From 495d485f392461fa8c95cb7ca74d7491c7406d42 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 24 Aug 2020 22:12:31 +0200 Subject: [PATCH 229/230] Add lightmapping props to object, register lightmapper --- .../lightmapper/utility/cycles/lightmap.py | 3 +- blender/arm/props_bake.py | 22 +++++ blender/arm/props_ui.py | 84 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/blender/arm/lightmapper/utility/cycles/lightmap.py b/blender/arm/lightmapper/utility/cycles/lightmap.py index 7f35e04d..e9449b9a 100644 --- a/blender/arm/lightmapper/utility/cycles/lightmap.py +++ b/blender/arm/lightmapper/utility/cycles/lightmap.py @@ -14,7 +14,8 @@ def bake(): if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: iterNum = iterNum + 1 - iterNum = iterNum - 1 + if iterNum > 1: + iterNum = iterNum - 1 for obj in bpy.data.objects: if obj.type == 'MESH': diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py index 2c83c224..f2554461 100644 --- a/blender/arm/props_bake.py +++ b/blender/arm/props_bake.py @@ -3,6 +3,7 @@ import arm.assets import bpy from bpy.types import Menu, Panel, UIList from bpy.props import * +from arm.lightmapper import operators, panels, properties, preferences, utility, keymap class ArmBakeListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") @@ -351,6 +352,19 @@ def register(): ('Smart UV Project', 'Smart UV Project', 'Smart UV Project')], name = "UV Unwrap", default='Smart UV Project') + + #Register lightmapper + bpy.types.Scene.arm_bakemode = EnumProperty( + items = [('Static Map', 'Static Map', 'Static Map'), + ('Lightmap', 'Lightmap', 'Lightmap')], + name = "Bake mode", default='Static Map') + + operators.register() + properties.register() + preferences.register() + panels.register() + keymap.register() + def unregister(): bpy.utils.unregister_class(ArmBakeListItem) bpy.utils.unregister_class(ARM_UL_BakeList) @@ -364,3 +378,11 @@ def unregister(): bpy.utils.unregister_class(ArmBakeAddSelectedButton) bpy.utils.unregister_class(ArmBakeClearAllButton) bpy.utils.unregister_class(ArmBakeRemoveBakedMaterialsButton) + + #Unregister lightmapper + + operators.unregister() + properties.unregister() + preferences.unregister() + panels.unregister() + keymap.unregister() \ No newline at end of file diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 062f8eff..c580e1f0 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -14,6 +14,7 @@ import arm.utils from arm.lightmapper.utility import icon from arm.lightmapper.properties.denoiser import oidn, optix +import importlib # Menu in object region class ARM_PT_ObjectPropsPanel(bpy.types.Panel): @@ -53,6 +54,49 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel): # Properties list arm.props_properties.draw_properties(layout, obj) + # Lightmapping props + if obj.type == "MESH": + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": + pass + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_filter_override: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") + row = layout.row(align=True) + if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + else: + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + class ARM_PT_ModifiersPropsPanel(bpy.types.Panel): bl_label = "Armory Props" bl_space_type = "PROPERTIES" @@ -1300,6 +1344,46 @@ class ARM_PT_BakePanel(bpy.types.Panel): row = layout.row(align=True) row.operator("tlm.remove_uv_selection") row = layout.row(align=True) + + ################## + #SELECTION OPERATORS! + + row = layout.row(align=True) + row.label(text="Selection Operators") + row = layout.row(align=True) + + row = layout.row(align=True) + row.operator("tlm.enable_selection") + row = layout.row(align=True) + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_object_settings") + + if sceneProperties.tlm_override_object_settings: + + row = layout.row(align=True) + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": + + if scene.TLM_AtlasList_index >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasList_index] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) class ArmGenLodButton(bpy.types.Operator): From 607f4e7b330df2930559bf6c4af3ca3ad0cd6d2f Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 30 Aug 2020 18:34:59 +0200 Subject: [PATCH 230/230] Update to 0.3.2.0 --- blender/arm/lightmapper/__init__.py | 2 +- blender/arm/lightmapper/assets/dash.ogg | Bin 0 -> 36333 bytes blender/arm/lightmapper/assets/gentle.ogg | Bin 0 -> 6615 bytes blender/arm/lightmapper/assets/noot.ogg | Bin 0 -> 20436 bytes blender/arm/lightmapper/assets/pingping.ogg | Bin 0 -> 9721 bytes blender/arm/lightmapper/panels/__init__.py | 24 -- blender/arm/lightmapper/panels/light.py | 17 - blender/arm/lightmapper/panels/object.py | 59 ---- blender/arm/lightmapper/panels/scene.py | 322 ------------------ blender/arm/lightmapper/panels/world.py | 17 - blender/arm/lightmapper/properties/scene.py | 15 +- blender/arm/lightmapper/utility/build.py | 55 ++- .../arm/lightmapper/utility/cycles/cache.py | 43 +-- .../arm/lightmapper/utility/cycles/nodes.py | 2 + .../arm/lightmapper/utility/cycles/prepare.py | 66 +--- .../arm/lightmapper/utility/denoiser/optix.py | 90 ++++- blender/arm/props.py | 2 +- blender/arm/props_bake.py | 4 +- blender/arm/props_ui.py | 2 +- 19 files changed, 174 insertions(+), 546 deletions(-) create mode 100644 blender/arm/lightmapper/assets/dash.ogg create mode 100644 blender/arm/lightmapper/assets/gentle.ogg create mode 100644 blender/arm/lightmapper/assets/noot.ogg create mode 100644 blender/arm/lightmapper/assets/pingping.ogg delete mode 100644 blender/arm/lightmapper/panels/__init__.py delete mode 100644 blender/arm/lightmapper/panels/light.py delete mode 100644 blender/arm/lightmapper/panels/object.py delete mode 100644 blender/arm/lightmapper/panels/scene.py delete mode 100644 blender/arm/lightmapper/panels/world.py diff --git a/blender/arm/lightmapper/__init__.py b/blender/arm/lightmapper/__init__.py index 8df717b4..ae20d2bd 100644 --- a/blender/arm/lightmapper/__init__.py +++ b/blender/arm/lightmapper/__init__.py @@ -1 +1 @@ -__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file +__all__ = ('Operators', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file diff --git a/blender/arm/lightmapper/assets/dash.ogg b/blender/arm/lightmapper/assets/dash.ogg new file mode 100644 index 0000000000000000000000000000000000000000..319b59505106fd66f823d8002c87abfd7dbe7664 GIT binary patch literal 36333 zcmbTe1yof}`zX8*jYtTHAgv$`(%m8r0)limNOv9tDUt3*N{|lel5P;>fWV^)D<-uoO?D=Q5E1^8FG338abtI84MB|@km9*!=i zHm-LKC@;zY00|~w7v=A{5u$oG@;~WrBm{)G%{Jc=NZeumPjW{6XChp%U&+M7l2?G8 zhmW1}Ip^H~YVfddFmp7wa1jMN&A?7UF0k`CjlGTOKjR?i|D0K*q_rRb7C3-YiKZ7i z5vvIR1OQ-6$Ap()DbG}xn8)aynkaWy>wO-Pni$bbY8J)W_wRy+$NV7xpaD=u?CAVW zdD}5TD^iANms~*`6@g+J%t*CmLF{iQ&&(W(>Wb_f#+c}_aCOkZr2`UjwC{=4?+Vj! z+E7xk2%~Trpe@dHxySrD_uW0N0h(YXuC0Ou6``%q`O!kZ8S5sbaMEMOk)Fb{v7)c1Og{yL5C^=982oV_<84gFU>_g^}@ z%YTj7p!0;FZXGey5Mos2qNzY8noUkVQZAWtRK zS$kc0WL-J(WQEBv83IE?rxMMp2NN`g0I#UdS)KF)ea^ZdBDhhK|hJmEG zqsad~1^ty5a3Qo=mdUnIEbds7zhr@e(~{<4N#FnHN+3db-a1YCMEcRFAYS@LZV7S5 zR9D3742GtX)QrCzv>zi|19{0bl-8S@Hj(Z+CVxM@@SmDDVFqS-$#<}g43UY~M`i-z z*?dpOESb{=$LdU^Z+>nf25S+C84UGJum73vUt&>Wm^s!ZxB`xd_?cnXglz!UF3dA` zN3rkP{!j6d0QqJzOWK$9j>bP4!<3k|h&F-BEe0LWu zuABw{LJ0m!@xP0|qWq5(7sN+04>8t`a1FEG6=mIHLfci{1k(38K`CbC1*Q07{bsgn zg`k{WVf~CQOJSnA0#o5X8U-?y#ynOMgY2)8gr>R-F%^Sa@!yQwryGAjIr(3C{D4A@ zkRHsFTx=TLqMENY-*~*$NpYWVcyBQ4z6A4Mf_*f46#9P()_*hy0F@@>uTCbKMsfCM z2CGP6|HI&aYmPHI3&};D5`T8$~`A2P*VDA5=Id>}ZL@;_yE*|ZFG$)radQ~9$seC-0PW&_P zWUGkulC9qw+eD<7Hhr=ms@QJ+f9C&aj;sqEm_f~v zaiRM!&FSQ%kOsA>hE@6KpEZh3f&_JwdGy~50D#We`-*?TH^?MgQ*_ z1NQ#lQJ&%f37Z4}WS}Wz>P-)M6)&lxup?vBM}t|8vB4BVnlSreD^+Sh?G>~6eX@0l z!D-G=f+JJ2Vq&S04+n*=QIZ^ejp|a2U09&mLj%+R07L$XJe~X@&nLNKy3wbRA`hT+ zJ|Zy)`RsusFDgKu*qdrR;YA$cKm_y;5Ipc8k;B=%tTg4`RKY5pEIJ8-CI11zdy~s9E(OQh zq}ve6XuP*o_%BfNeTmOMW7CSZT`Y7$*?j*4#vAB_vZ4aOgwj-U#HSrX8JIU2)6mq}Y_7SlL!znqHLcYC-yjBf_kR=gbLm%QO^Yw z#X!x!z!DIw2SEUULNEa+Kr|!)k`MxA1c0zr?PQQ_{nf0>M9R@-I_Z{?bn;Uo<2-pf z*;eTjJZi5sta}Kap-nbJ@W9FYyDlmKco(G} zC5WKleaY09=^FhwpV(|nP&P?@NDx6&kjUJd>58YWKz3(`84E$YOxHy9zYblYBoS<* zD15B|g3;>C^x!ZC0QlSq0cbj3#*JdXe0L952OB6r0Z1wOYe2#&P4ckGf9FvnmH`Q+ z7=K3yHD1jm3!6LQfRq8r0+RMlaDRIjm6U=8;`N(jP@oOoB>xXpAiy#<9w33vR~GH0 z&O@SYJ)i*4fI`KIL2G_rYJfC^SPF}&kbn>Xo-cz@lCcySBab;f5PJ`FcKL~*%aSjY z3g<-7Bz{+aPhd7C$yiwUnX`{3F<#wH%`8fik-l)|lN~JxDwNB>r0zi759+0_6>Q6U zNef?>B!VEPLW!#a2%06TpC#&ksGeagbmIb}t@2SpggF>@>EZwq)=mH*TxH@OlJKdz z6J!?(;0=0vDTTYJjDWU~)NF(^ELQz4NP8X0*+*f92i`guvtiPL6Cg5!3GU__lf=q$ zjRx;EWJ0h%)s;XBk!dy}h?VCW#krXY1|=wW0!}W&$3TkBe78md&}5Kyr@BT7Vq}Hf zqealT;Yl%4O8s}LE8d;=yTdD-{`UwF1la+S>Yt8~MD>5y zKs+!Y{`ZJPLU1YX!8ZC3LIo`Pf<*OucO#ezL)4|fz!{X9!o*h!OnHgm;z9Y5V*Crz z-}!s`JD*Pv_JSRW3XFMoc!h~~jY)qy)D^Hn7^K-<6Il7D{M*D-2wn(m{M$sRa2KJg zEBu}L-*^i5?wXhiL4JaI2?d@qv-PNOSBRj<5_2g@ybmoy1Jpt3zq=n0jW+E3RaeWJHriAL9ny<_vhH1Z|ChMD-D!NP0IVrk6}b zgi?Kp1)$_Jxqa6i6K!~{fCG9N8lY1mWCRTb{gp9jhVTdo3jpXh6zP7a2L!Q-1B}lZ zDTv18i*W%7f+yTBo|tmVDSgI>jNAf@or7qp0WKcC@9gZk!@wgqXIK!yJAx1z63kZ; zD0nK3D-rx6D;NyFh?9iCy&jmb058z>@S3d%pb*-mhmdzbEZ zvv<988_PBNAwCiD{z1rQWF@izEWaVAkZZ_QcS$hO8D@oZjUHHVAAe!}k^p&al{0z;T?7f1QUU_NeyRT6MWZSis^?#1)`woA;sn}c) z{5fi|fJ=b>yYS0u{N?A(-IH?z{ea6gJ=duZJ|%?RweE`@@??%U4UW68dRNM&j)Oh zS(EgJzrp0*`N_G|aJW=Ul{J_NpIsC&%Lr!cSV-$P*t7bzH77dmG=6xBsQPmF#j&g0 zfueCL!=v_8Ph*k0Z%gG|(1v>jvHp>W|E$)`(kOt;LoDDvgK}8K(s`Fn<;`fnp=;a6 z6lWh!n@|1L_F^%3rHPJcKir`q!3q0JM(XzBG`jsQv((7APG9L9g@ZnzDSo_RO z_oAJ{iTc;+=7V!{M=Cqfv;JRNYQX-PYJLb=^sNu8_dI#Su-K#hj6_DA9Z8!%EFm9q z0xy>Bz8?+c!8#UODFyc4H&-_GwdH7b{ie>TDBS+#xn*0?-V~s_Coa0gE;j#`Z!Qsa zA#|KU`g`T;Uxlr>j;gwoj%hXRDTgmLaTV&+i)r|;pxYY3iFWqRWH|0hw8T#Kkev8W z4K9%r>??VTU-a;Y)+A@QHw%mm;VBapJ^qos5={>6*;0d}X;wIwx6(N`5pDN3cCUP# zag~OG9=8;Ztaz}&O4o+XrYM7N29g-f14%2g zOm7-rJ)Zw;v;KX-b28g)lx9V5$d8By)w#^;IUX?%J{2$7HGjCKxaWDjoHcSK9mV7+HQ?NB`06Ii+x=ph$^TM14}rAjs!_wz{53 z#9&5)K#Z5SpDry-xSO^vy3fYgwdgGgr1Sz)Ks3y#g5y?!AkAf7#kts%^Hd_>5N zfFt^&&BkzXt#8WMvBsku!CoEPLHmb+3`8@9M$@TJe$tdd-(H=DuInxtKl;tl(vQI@ zdPZFt_(|O(5NC$eCmj)BbTT3txVaUW<@fQ0#HdkZ`&?7p{gQ(x=8ONqTXf7YRyx5J`;rph;Y zed{S-hD%*cy+0PV&IT8lu_?V`PS@elO^fKw7^$xMGS9`|*{5-+QI~0{im%Jgagd=0 zKTJ48!K@n3YNEg@9BG4`>ufAT2cihd_a^4>0^w<2J(Nb5Z)tIr=;8MC)x91f9B-s< zuUe;lHyaJ6q@o%wv=vRkO;d<8!qOLL55#^(THw!16B% zip7(KIRSu=M2WV!ENqrR2tVQBKOGr;%}_-p!qFE5Po_m z!MnxjepuGfp-^pc&})!9s;q$U@0R5j)SRlL^xv?&Z7vnpacgWlexY1G-()YA9BsO- zkyH;Um~JVYyYXP)vioMc9A|yY{zMsPAVJgCoP$il;*X+U7f)nR)9BB8TucWYr~^(r z8bN%re7WiAA8g5BSs1wd}OO^^ryVX7W_%2i48`TU_?yqA=9F z2~P{GI^W!9^KmSXfi22xRjW5mCS%aN4E#<1V#wk|JuOph=B+IFs6bfHrpcyu0XR!Q zB*=t*fwQDgaLa4M&#rVQI9qx)V2ee&zgGR95Np=h&?s72+&k0y(LQ2Vw8%fCEBq+tu2GI4U=kS2`^ z9$ge^h%*?3U*KmEv2XJI^yCvgGYejo^}l_26Pvf=$44Iuu$x)5j^!qdL^qXQOjac* z>D}mgqhS3C;11^EswwNENS!tH>^|mDZC5dS@+EKG>it+I^#KP{j`GQi-5)uO<|*V6 zR$^x&0WLG879GDxy30}c5LH1>Z8y*9km!$dpiu<%3_D{MqA&$}TtZ^PS9_lcKXXW&&>FrT5O-5WMDW$}aC9;{@8O1BHoi%&^fB78H> z)>E{w77P~_F4=*LKd0AAUUZBiRw-g99_7acuf7GAS0xXBY)MAb z=ndP^1(aR2x6NqKRlFN*_3Xl5cYTN7f_=NwD*Kuya!Z3I1E<&JeB&`dc2cvM(OED_ zdBC~&j7anfiSR&9j?#(NjXFKM?ZWNdylqlhQ3?B_`6I=`yRJg->YS(J)5Y)Bw+t6J zn&yTwEB=oSF`ph~*tOe=yHdG+)C-}q07>dQ8p3oqGIRjdTP;_{|ExCb=8UZ*9>1CjNa6~f7PpjGnWzA z9=l~T2p@ib$dl$YkpD68G5&ZbBa73nWMP4QA$k>H*w{dgbn-Tc=#T z+b1pCDWUTl>!+)ow=2#&dtYytc0v_N`dqSqi^l7cu)Vc<@VtX)LfH(O)+YH0zcbaX zumOIllkuD;bGG$|O8VQwLG3B7O@!Ew7duCUud2~Z5I)$G)zcr{ zJ{kTw99N%mm9VlFDqSm4TfqL_xx?>zWb$|p8R8?*OL}czZZ$^m+&9O7$|XYCkxJN;puWb(vos+`n71FDd5!5e0Dnw--aKJRj)Olg>wPtV6VUEuk(QDUeVayCCiN)^w{@z+{LTK4cjC~V)NBEU+Zple%OoA@ znKz@{RCvBV7l#`s6r4N~M}8+*yC*-8XgErg&*yPdeID2^E03`}Lg+^g$b(y!dzNZI zK_^~~BgE zA1NSrvPo9vhZP>L&^?;m8~wSP26TzTJ(CCmhuqY`ns%BoB;f;2xby<8kIrV>nqFIm z>kZN>^xEDf`Uco-s+Dyl92jTVBHv{pcRA~p5+C%lSQj~RT()WHdvB{sQ<@Qz1otvJ zF)ZU%Fu;FO`0FNdQ>8D(rHG-Cj{oYtF8*s$GS&;OW2=rXA#!$Q@#Aw&psC4xou2!g zu4-EGZMV{QtFJyYdN{nw_dVh6x+>81RgUr&#Cj0-DLr#i9#fU!PM=vxnL|g9`PePW z1M*!WO6!&D$)xIP;0f0i95|wyOIoU9|3n>rTh`V>Mv4ZJAl#rH zbL*+%%&@vR(iq1^**^EgpzE*WP6^s!A>()jt8x4At~SUX!%GOeu0$aZH?~~P6Zf!R zm#VuJvnD?%#M}guEr8zLF?8envyfiJK(3&s>&+NZB0ibbmsD|}OknGkQsC`D2RltV z!pUf=zS~1_bQZ;!Y=aVJC#y*>qpF;Jjq)}XQT0FGjI!`|`^PPHYfVfa9;2U3RnNhO5k?NY zJ;AGwl&-V41Dm+I-A`SZxDm2AF!!4>-1@T0tB#qhp5%gr0x<;pYs)`5J68NDhtTTE zVXnmk%`^Ab6lo8%D;$=dbW@e@y~53=J+NHm@3S78syY5+c|lR59Z!4x_I63rO1eFG z#Br;Wda^zJ{eO>TV($|4cU%rG8R0a#CaHdEnvZ$8CZF*-Xquoc|JM>van^!WI9B^} zQ7ds})nD0NH{6#E1JvKel@_xif_28w_t7MBHmlr?sh0y%d=bII8j)TE6saTJw33%yx+WaCxvgCN}o)x7oe> z9oY`^Ck6Oi|#9?$H#9G(m-Drm~FFQEX2Snn@lj^|nbpcDseJwG>>jwy<60+#Wi z3o*{W_DcG5vetP?wxg$PqgKDP3%A&yY;YMM8hPD}9RY;(OP<`Nt)NYmbc}u5f;@=^ z{N>fY0*aKor|SCOB(>>-D=xYFYR@FQon;&@k#$WMPED=Y=hbaB!wWD|-8qOh;$i~>6M`2Gl!#J6^w0ABfNc}we|dDRwZ z{6;dNC?zl92->qYj2C-39xI0z&PCIJcSi~&9eY=mAxRM)ln~dFvji^`P^@y?)r&F~ z&nX=5;K00jQhQyz1l20Dg^J>4oJhCex#(=SVamDuw!o)u->y~LS(~~i;eh9evg$b~ z&t~^~e#?U8XT@ooda4{6K_$mo)s~4pa{NB&@y-#Ca-Vxv_yWzuF@m!9Az~s z+t0RG`)c;ZC# zlh+0%rI9!Z;Nu}wV{IZ&5tA^&7mu&YVpbpzfUuj&Y-BBV|R?gf`g93MYv4K~bFCE4#1Jfb- zF;fRcRZpMQf0402CjfV|s^5P&8Zb&r>humNto;KRR5# zh<~t8O|G!)KbI5nbx`erzIQiA3$g~Y=!{=Q>L>E7Wk+;9Z6g^A#k9!NGjNR5qhh8* z9->9*AZi_IT*fW=mV=u*zP-|lxS73&UiZ}PIIOJX2dC9-u^!r+UJ-7K;z>`Ho9J_x zSl2_|pgCI7_QfF2gfLQaU7YO)&&z`fcCNGUC!aDQTFk||BP?$Wv3YawY2yNka-vC` zs>+E3goZy;A3o=v;+(r23A`1~$oU`vOzc$ykD*#G9sWqL|8msteu7>lj(L5ljCxqD zq+_26AHqGh81#~%&g~LRhZK3E21_Q%eX6VSacY;mXI`77%4`VmZIXW`yS1{At}Mwi zW);eO%^9d&7dP%BzM`pxjd*j8DrbV+uY#S&>|GBk8`~KQiVgofhFQLxDn7BFG4E+i zZM`A~00G1z&j*YNI7Ss}RGN=1jF*`{8r0obh((YT?Q4gol*o08%z+B|rtu2v;Vs0N z%_$pxUG&wpSaHU}ruwy?uWx`BPG3|$If8|KC2Fn)lQ1pLJjxRO5inkFy7bx%+!F`{ zo>%3`$HM4I8_67*T^%|~@5y|$%p*D3*XdbAB_WZ|F1bFPyFf0r51r^k7cUly|43s5 zCI1?m-=z(QNy!=|x9A(5+^|&Q)T;z~n-1s)yw2MeMO+h>Ud=Xs@Ys45z&A|j1%DQU zHkTj@S2~Ixl%1Yo=4g42)J;Lvm18}6^X5lZ&9w}fWYu_GfzwZFM8Mcdx;?y!ZS{=J z(34`wECGJ|iqhwp(B?5Wiz<9D19?pdtX`|~Q@U>^YN!M>I6e8Ur8ztrIJXC<9Nh?r zUs#mCpLWeJIl+Amu@(sB+!5^kTsywE9)DK8S=lOj)w>!2uxw!mhJWW1=Jjwa->7p>Y-R%i8`1;e<34xkf#y?gv$e z#IMfO`r7Xmg3k&NP5r35A zGp;<%X}YSmyFHhtd7$=#^lc=vn*PO+Cxyl{<>{2bnlCuGf{jLEduPQd7y6&yX>$;% zdKO!Wn{fU*RPjQ5Lws6dh+1{u#G5+tJ3qU*G19S z?Z_gY%8#nk=!8XN?47{r{I%W%fyL-?no&mYcQW~r6Ius0KIvy_@|5T_$m48F3_16y za3d`Q3YO)$=l7JAQM;Mll_z7QW4pJh`q`G>QCV#d*P@0r znJR)(QzH+nq1r0%r@o8KkC=J9h1rXKEs-NvK!M9{@STg430KLT``BI^tn|UB1m&+{ zoz*RFfr-|ktFy@Z00D3Gr+s(~$K`2(IL~v9PEX8rl~L9J=sP(04FHtV*xvR{KfSX{ z1Zbos1al=AYk2%tR^JxuY8MoZFUhYKSQL3v4vz}t6drA^Jh%I>CU5`6rF6EH0c3l?u_d9pE&KFR|bxB&WTDRCZA)z z&e?`UPFPA~NnDSA`$`k3ZO<=#V-D9$lBm)*?0EC3{r-2rvfjKZ@ft2RwPCOpO_%H)9mLov@CwZrfgmWvY#>L>HfGn z*U??`PzhJ{rn!MB5q@~vO->kTFucWh(HVSV1N_{fmoJMih%1zZ$t4_wkn>juJqsYR z1!`OeC@y}4N{}gBC9ye)QHes#{Ul5iC;%j=wCLnFsf#UB@7_(ZjKGt~Z+W*X^)^eF zuh0X50)vHv6~w)@jvW1otL%*y#q-2Yc3UTX6{V=UP!^klw)V>-hCE$x6#>mW8E_HM^Sc716<&w}Rq!h9t~;OCiw$ z)xkOZNnx-@J?^lm3ih{EmN>v%G)W-8dPDE(Tv;etXtPbNO4QnCRlpqe`4H|F35-O# z7Lky(rvA|y+l^)=FlfG42pBQLGJltYlWFO3pQtkb-X~@qQ>K|#{T7iFk}mDQ58M4t z$4@JNPamfZtE!02wemqPN_V3#`g!@PMmqZbNUhY;Cg(BRu{8=UigMEJcNjs&t`k&C z$E$&S8&jX{;fl4Eb`&tf^#oU&oTi>@VwKatdqEL`|!j&7MNHorI{ zY{a-R@y@!j1&E}l2-GCRL6>Q}FzF*edP)!V+xy&-pd5*8MG4y#f*g+1OOBoj(Y?6w z*i61Oa@!YqTHN1+3&99LA*HW}Zr(S;`sD@@c4sY@GS5oVA+IHcp7dXnoHC z2ng*ObBf*LPKTMfg}L!S7T{x)0B{^CMM)0D2lqlY=`Z<7L|0O)!ToHVHXF(7?sv(! zu}zoVS=$}u(rRzNNa2N!N_T9I{Dk{=%x8J|E~d0pKd`|YII^@SL@+e_-aOBl?}ud? z9DSDaD&%nw<{a!J1+2EtX7Bf1ehj5E+(ul^y#bm=0SNX4NA!&Hba&jII%!>oYpF+G z_@BA4=zI>lHuhRn%g5@sdT~cPv{%z~S+Ld+(KDp5S(6yT?;DruDMAeL)zNWW&@32 z*;M^Uv>qeK1nb`#e#8#g3SaH5BSufTrWhK}+_kD20z)v7+3P(HyH_%txjESmB)pow z5L@fhusO*-#G^r}Jq91H8uRk%nN+(ejLez$X|2Dn*@Y36^)932%$P>Hho;Bz9r=9t zuDn}PUse_4toC+WRK}9HQnCU5n(E3C>DK;*c9Mu1h)R`mz#+qVw1T(HTal`lnl1ea zt(uW^2ngciM;X+^-IW#7eh8r$5Rm#lSbXE>A{YC9xh6)X(|&EHr`Jdr^oZpeN~{Jy zSZw7Te?y*iu__dEA&NQDfw}{sC-3w!W2#N_quZOUzKO+KBJUTqm;KKPL_OQHN2lqZ0GlyM9$QoR9=yrKL9HT=6L!jeLczwowzJ+HT!)cacu-UU9-T>*b%Cpf6 z5R~`so*m!p1kOE|^hAprk1>p=6rCiw2!r1YHg3Kc82NhPbU#u0oJkHZ^Sz(Iwa*dX za@cYD5^7o!vU_u0bV~iLJ6g;W#4J^0uG62%A3}&+&Y+RLQVKZD6l0$Lz`}=`w_4i?Azw} zr}BMKMw`0o+0YMCSXamJByTNRV*lk7?7MYmHqa^LF{h6UY*H?xb$$tE!e9lYl~GgY z>C;vTiNM$3v2R~}Rfzb#$di!|2T81+J zcH-85R7sd&ZpGxX2bv~k@|oLKdis(Kf$^(WRIPa{Ch}Js39FCT-n2zP&*t{!)tT3g z;0vQ&8L?AxV?VO^iQ!=77n=JeigcYDherdqi%Ue`%gYZY$j5wK`0zZ8f<`V5;0%2C zJ6zM~4HQdmWInSn@&CAesH^qK1bXE?7p7dh$LDvMW!1s$ zE#w<*#w^I);fH!fpr2D;@cwoB_n1I;#{6g29G#qjKG_FHSt@%+gjb_B*Gq}Gz7kn$ z+6%(@;(Lk=27r>seF>Cn!79m(^|kwz-Pd#vCnO#p?BPmyUTj02kB6Vb%PhVZ^@Xi< zdRDzAS5DG<#l+Jhn;-8MoR&ckXygs1uJ!)rLPBJ`e~<4_G{cjt zCIZyfKTjbG&hr;BTGw)%=!3NfaFRv{p5`v|2vH<f)*s)h;iuyIl--#hafuTBanZ#fxLbO zqX*FXcV@cN+_}y(YJq(zlnE5W)TCge+JJ=`dF}UNEL1+!m)Jwnoh3L%Z-MaDhd19< z>s3TY8lwxmZ}a0KeKQFw+YN~(d{tI<`*Lt4T!Ql!8iuVJ6e!MDc5AhQf zOZDT1FlU&E{k6(E2eE{f#<>Cc5%;|6XLcoJRApCBVls|W8Z3?E3KW$ZS{?A2 zUyqDz8|AmY_v+@CuX$$Hn0#Kz|bc_GI4FTVio=Y!3#J@`YnH{D14MuyUi6)^mZLBd-!m2HIz+V^-U0S5zXlhfqucUVLQK@uVaJz`0k?iReLt;a ziZA3mxaqyJ5E*EU(?ZnSbuFUj{zZ+?wum`}#ZEBBa5c6X_xs&A1`jD!Urr;|zRBPr zN{_D_bZUFd{Y1>;=QWXqy!jm^@vbA4^|zu}=%YjdGrK3v>(8y*#~1BY@iUjcbd;Mu zDQXIQIvBq0n`de~vh_tR=XOtkuxD3%trb74`Fx?4$LF!SQNU6%GD|wsW)=L+1J@f} z^6mSTweyR&m5g6snfDIJGIa4&wA!*^rWprwa>JCd6t$kPRF_A(qa&3M6Rd2ggY@5CpOJzp24sIs7TGh*{ed~ zAB@Tq@*52f@#6}D=G<#kMz-DEL%duWnSC3Nw6ygE9BtEmhc76+sS9aPsk{nNso#p2 zpznORsQtD=t$|FC>|*vy0L&Bh9LMea;51tw8}KeTp?#yW-r{E{0JH{T4{Vc*c|c2S z)2E!-BXEYn@JEaimwwuQOWS)a`rLdHhG-c5a0N#WMg&1`rcJ$*%E@-goaw4W$WvM@ z-!c&e8(mL_{(|F^T+G$yWs1L{YT{JGlh9#?TH*np`xlWO2j;8aoH_Bu0sFYlO}qRD zloRr1E(bObKlDMps%cRd93tDs3mi+$OyY97=MI)^Sxjrt&A*0wl^^9~t_;9&c*NT9 z9nn9w!3gr24EGjro>Us~r)3hY?@`ir4lLP^`5(UYG5Yg*(O~&gZqRve=GCC$W8A>- z+VdSK{7ZmYxylpmiylJnYU*{mGb{QP_d%IlUqXq-4t~X7euZh&FXnxbiOwSYlwtfI ze)?R|)2O`CkHnee6I#5aN9(i-4SE;B19A7w&<}pp!I1HkFP2On`TRavA65_MQav@6 z7c~}S#I~tUm2F+j={@Q6w!YGWUPrv#hSVRbdQPg>((i71lN(XB4h^b^I?ae2QAn(T zaLwhFSZ%x~f66AEEi6pW8%)l_>x8$zXSe72~3dK3qu%eRS8MX^U~Es_kDu;anoOj&op_#wk1fr=}eFkfgXu`hpge$c}}a>N%; zsWt7KS#=+|Xbwav_kmwp@;o3y6`!|C;2##P9mW`=3|rsHs~*2pm9v4!uDl#Ltr z)5=E``8HkTCZO^mu1wr;%aPb?P5~G-u1%=99Axf-sTA7nX@?t<|jC zT)A7;McI8tw;P>GH3(6j3EZ!hH`^D%-jLLWy(O`Tr9RP@Xfbo6ekcRJp2aP{@;0nQ$DxSi+^tXAN^V&rm|Qwyd23#}%XJKX$ND2cn$RKSG^zJ=>!fdEs{} zM!uP?@OXQPB~aZ!UQt?BYU*-71X$qRDT4l~L&!z(0Za*z-2H?QxGcTPQhCQN=Nm|| zR$3R+aN7shFn4{F= zxyqM3-nj*A$Kmu%JA_d){hXb_?Y*ajz=`uy2#+$Zg!5W+qYe*8skAQL#DIWGs>5Id zclTL%Sn<@0mmw&HS`SV%`1f=fU7{XCWsKz!sDS@73a6p4Wdu*!Ugca9f#`V7;Apmy z6fEV60n;bNocLn8+b?IB@pw3=?&QD zUFhakYE)~E$*C__g-rRHU58GxcOapNSSaQAioY0)tJDZCSiQF=y9qNChgZM+s@PXQ zK6rCoR+JMhzzSy*pBQ>VbiadI<&TCTnSDi$c@a{sYil@v%Z97wUHp>5Ld9+45{;|l z9G^MOsw)aNs+3O7PX$*>mRLvUIiMJV@)jYX8APW^-euN3M-aA5onLP9bCpQ-+c`yu zpno1@*#f_FzbCsqnNW4s8#9ELU@rgCK&4T&KNoHLQ3(#zS4C_b0(hOE%1H;avVxxk z2Rz!q?6ADy{E6OPlqk+bacw)`UMj($p$^(ymhr8U6l*39RB@oG3qB z_IctPW@07gAE+oCg=QTiDu^K)cw88;@9f6Ym<=V8G`%OXgO))J;G91MM|mT zlIjlR1Oy z{t^b>v$R#WH;jneTF2~h@#K!jtb|X-5IiQ6N4s7eYF;88L)qxl*)K0J zn{80e;re0NLZ38yJ*Q%ZxCgw9kp7U%?;?fMr#XVEb81WC#6)b%{%3J>>>fTBsXi2L zmRS3;)E_6G%3d$4G<71Vkd@1?#DpEQ*K!dnJkQSpL;oyM^WCfE8(8trB5zeVCEtk4 zx|)pQm|&!?BDs!suR(*S^q`)>9lz?L+3V~T*gqXN9o@I4MpIsw5J&X`_M82PG${@<#=K;DZMxpCIqVm{|PdUGW`TSRMed zV><~#8u2U|KkN~Rv81=w8@tYaTHn8#Xt)fC-tN7%;BuqdTQeTMAe>Kj_`!Oz?z94L zeY^ehdBf26E1AtpyxA(qV-!{X^mgXC2FBdaHE7WF&8NetCow=SU}pWWfOb6c^?5`y zn>CG`v7d!Tyu{AJLw=0Tcd$5#mLCod?giHdTT*dBSakLWyxV25L0RLNtd&}ojVW29 zH!pGmc2W-7I@2*KUQAh8Z=8tdt2_R*4M1bV2Css`fJNuOY&P=iskWb zV%;RfDruC~*K0R{gktUp1$rElqk`fEe#WM{8?y=|PzpXsAd_tt-6NlsZC^go|6taE zbJ7~$c}&q)-BojxwX3vwo5H=6gZ{Fv=p&AXG5f^?MB=5LQ?2kTnR_twbM$lYRS%4g z-;eK))^OR&i0c{Ibw%pKPLAOL@XKfKE4P1#0ljw%IjFOnHa+1s}16`SjD{wB=KkZ+X|?w=LEXy1R{wfAMY`qlYQ;PnrYC^FrV1kmOAuw3Quo6l-YWmEf12H~sR*d)g2)J(Y6W5*n+GdvG_}#z zJ6oLHo2&DmU;1Dp?2jAD@*>KUjmB57gM%xcUfl0gx*iv@INVRc?Bl-rJQj9#a+V*; z`3(M&=X*i#nqI*2igeLh0!!caXbyCDqmW`o3bt|lXm_f!U8%dZt7FmYI{N9LC+01l z2X_|HCKFibM@EhV5hPE03)v5y-f(w7p^Q3ufa2%+P9ke~xj1)^L2`MqKld+Gg;oo#!@ zozds`+=_77Mr|@5K0gb692BF7i^t1E7}%3cxV3Y?(|Ds?*U0muQ@>d>zA#dm$vEih z!K^&ge^(?0Bl}Td@c6XLaqi>I+=Yq$>A;3Mm2X@4O8~(QuHDY$;ZQcD`+7uHwZ-0Z zvPFB2I~m*YjgN~z&Bu2rL)zu%lyd3j&u_>pJOL2+vh5&kKKkoSU%ta!>oLbnr2C+|8t`_EtfK5JG-oZFnlo zgo~?|d+=hTN&o6r7fTyK#_S^)B~ETk<+h+RGx{vF(qYE3HklnAmcgO;;Du*r;AZ$` zFg^Hg_jr`+5QWdb+B1NvxJW#0x5&8)({-tdcrhonnrha>y9l>CzQcq_ zIDHo!ANgUGBeuA1AghNh|Bw&Qo8A1YMJrF^by}4$do7fCi+@pa)OCgY@qk};%GNTu zeQ@{VR}7Cjap(b^u7|oRIp%(q1mFILPc-wSq;~MJ2_@Xqv)cR z#&1{z>?AxgL>R`3BW3ssdqhMuE}-E;TEq2UyE z*Z#Yz)B!j=Uw8|~>p6+UAgNO9fJXMO?! zoe34Ha1@^!1r&$p32E&cTs1{$H# zf9C8he&R5~ipE>45?~kr7VG`+y#sqnn*oF_cLsX=`s!0@=?{;7NTeP<-zz4|HP4S& zU$K!S^w?!*JbI=_eJJ$HlMOki)IokWayS%jPKz&7+YX4nNa)%A_!%j>92RvcSwcIr?*IlCD(5%7-p-bDD_FX_cHfc0A&|MI0rgX}b3V}01_dy>o# z#RwG|A!FqIp;-w zgR{eYWl%H~)Iq2N)mz^5UDi+ie0ym?80~hc`rtCl;ho7L6EI!4WLG3D34cMicmbKU zFhQeU# z{6zS$*UHTz`kquAW|v!}+9SrqQ8 z_t~ua`*5LCCeq~WX+a~WaI%hS|P%mq!l!S6wr&%WRlP5KJd46yU+baRngC30y${Uo?O-l^Jnm0 z(aT;HBpmpKC_dZiD*Sc5hYr#d;mWg*IJ#Q7OFD3y!ZT>L0VP6yQY4bt-a_ zydAbJb7sZ4y1vr=-sP~FcK>#qJpKKO$Ln5^6MW&Sql^BV?m%d{Kh=%%7sQ&UmP*$) zToUfVaD}$VM*5j!n2_xFyf%nN2*=ioI=wV9iP@`$HI2IPh&XHWS!&wId^}p1(a3#K zz7pmtw8xBp4*wTPXC2q%`~Clo5h4u=3P=koDczu;h)9TULb^jr>E2Kz47zKGN=bJQ zrE8-@Vn~f1FkrBa?YGbG```WF{kZS*xUO@qb6&6KCCVQ#jIaWlr(dFq>3%9bQa{Y~ z?M*ON=k?fDaQV z+xM`FCUbuKTVNXTZOFk+s{%UspF;Q+8&g>Q~*9rD7h0BpmsZ99lhuj2U-iGhNx^PAPS94tHlMMsF^v; zZhj_*R)C)2rAYSdD^@kviMrIcrh(YNosnlIm7DVD8y|;eyQDU+;%*CgEg2+uOCa0lM}D+V6Q4vye&w7E{(L>pY~fV$ zV?_G|dgXS=L+A0UFll+v{()x^UBJ=Z;7Cw=*DWL(`B~ABjmj9PKaI%9WJ0&}FBtOr zifv`rRJ=tJ#HyFpy`{VE$&XK;!ckkb{NM1W>QLdKD`qt@-EGcl)5mX|f6eK*Hcd>i zbrV+U0r{1SN>{BHN22EC|;6lIRcB z4Q97QFW$rz^D+fRN zTxgo)j7l@>)PGqD>E{(@G3)D8_ihRD(@oZvzNUqAYl8RjvU*~wiZ^X=d!O|m91j&f z(|jlZgxIx5kk!Krx5^KSXYIc|4D6lnY{Twn;2KRcir&3$JDkh)HDKYc(mUI7sM7eY zn*K>N(Z=TPUI5YbPZ!A&8`Mfe9C1E9&?vWU@!rC5;Ab**SFR$8J0QeX!S~<1g0|Ff zK@p$g~?)zY#F3~ugFxb7CBEj#2H+ZBr%Q|o2S;jOXYW*FV1cWb> zYa^Ai=V%2qyxcR2@;i*d>YC`gVTo5gC2!qj(T5M|q7?Hz_vqipl3bIda9%@ua;c8Q zg+xfQS`F$}LN+tl^F8r*LHyLzhn>$|BMW$x%f|;KYLDn%*VGrbBK&xs?RVpf)YX2L zQ54-h7^4|s&hzkE8V*0r4^d9eNuuglZLW@-bLH=;DG*csLtW2{iQaXd@2N*}Kf{4` zk#hC0r$lHwL(@cMu*mmX1VDiv<=AOY;qL?Teuc31_B~M6wQ>+5H%>i^>>ttJaio_CFy!sS}9+pLW^aP)fk*!Q0?MIZds} z^nA~RK4mU9a4u|XEvNrp0YetRBGqP#hX=o0dN;IdHq@VI;G6vh*_YeP;|y6J&JBXE zrgP?OqDt|_aYa&lWwuE5kf9N@7~39fZ;uLchei9#Pit~#Oj1wwb%@;#5vXKv-5iYd zD_z1Id9jsmNX1&JoKCS(UeRchv{LSB#x0+#XO6K4x9XQFT^;F!>WWtT6ZeCb8TMbr zn`>HO1LD-@5CJyuXPUwhjjd@UL9)T`+J)^jAelEf`1)0HkgeY(dAJWn zmQWiC{|zmq4KP-^Q|(n?nvcN~$WMWf7KQza2uh`ij3%F$8rVdn6-|VKA3RCaiPgkh^*MCE@O8PE8`F#AvSo2!k zyr&KL`3-8DHCMAtEr}pfdga_f|9WexeYF+X>pN26MTzMRuYswb6OR+YxQ`tXQwWrx z>gPuc6{WHHr1rK_zR{f%VD^R)d+Y&;)HEF~>XBohz{oo5Xvr*6^RMzw16+DuHE5ONLz? zAo%Tu&er-aKbnErZ^v%B!Cnuxzm};1KP-C&`50t;Y~JA@{4%f<0lh`cg@qmNWiVMj zD(9ER9ud1!en0&b>Okqa!UhX|u*vi?P1AHIh$>Vvz z7#Al|QX^7bFa7x@>+{d1Ra=$2_W%i#$GM6QA?Mt0Rf!SqHoAb|i2JtOfG#t+9-CMB z<4hE$R2%x6M_5^;9f*xUnLX) zw;Q0K8j$Ky?eQkizFME$uLdM@Gir4!TlhA?9A_ma7LupCXdXZ2VBZ0CF6s!B>%sWS zBEe}p(mK2dqEBLOXqlVh90+&NF!xUbEy zjQ5SW%d$-5byj`JZ4YuO@F@1qu{b;3w+m$Ku3@O}{c?3qTXY+@vd^8h(_(Xo-lHtz znB~x6sM!DKdbRItc-m@1H$lN1k{@KFjM9cDNFB2c5ef1oxcE8+k!lvic5{i~`y)~y z!ozEvsqnx-in6yj27VGc%<+AXu%&<6X7k}=K@SYSKOdHP|0E|+O$jragvYf`yL-i& z+?n4s#HUe0N^^`;T>(7H0gN3_YxxKws!z&Hm~;i~QS;frp_KRsvmQ*tx6#bT>pzY5?kU8!_s8 zgrx^u7a7J|KirOf9g9?m-&5=~?g9)xoLMm(z@K5Tb_1>)E7UP`c2opA%d&n0Ao&GL z0G)sW1$`5?eCj0gFAik$q_NCu;foxvrWcv5r)id=-v@@roG70QCL?|ay0mI97<{k) zGI7t$|K@cyF%K^t9nZ0wdRnvT)=H$VMi4xeZM*Jv!rNv82m?S?|AYB-AMzQrgrZG5 z&>8TMe*3RM)P4bLTx~7q;513;7-nUlX_IMYbMU1Du~#=_9*t$)dvLf5&(tDW^@iKM zL&dZcm0Tm)EqvEfZgoU$@~?zLydfOTq~%v243fZDT+N)Bb(LRmk9)w#=S~fetXifS z@Fv}=$!6C@fW{JDR-46XSK$=S*69W9$Rk}Qnwy$B)F*vYl9A26{n8(5DADMd63klL zaabGQKNcBYu`8$d?ec2SaN}(R)ScE^@aIbFe{mxmDcP8>lx?&Z8Uyis`2a1*I|Att@GpHD^{mK>fK%R@gkhNB3OdrjM}8?up>S z=b*HTn(DB*Yj2MG@M9Yi8_bh|GaMh^{#T8$q>?(c>RL(?i&)d-#9%|z0gjY4W+@i( zUp27TY^saXgTeC z%QcMHwD)CV74A%3MfvGFI0T}Ed_Q3mPXI0=(1qIEY)mV+`~(ZhNxB+J2wcd9G3bq* z%#&pO#C79>#2)P*hTS-)sq0fbMXiQ5{O4-#Oqh}M`@{Hdw2ZMF!a}L)u+plsPd}Q= zhkTJ3>2$uMp&v+`-EgE$n+el(CzK9W*VS3P-wY;}@ zBV&a4&d#$6C=JU`N4;l2Xi(%7vki?t>JmSi>>BuQQ#K%iC#5o?8|hp^fAUS^Ap^av z9i1DCpQ?QK%eq=#KY-Oejj$g8K!JjDE5f1k(3tETSt)S4aBa5SzcTAsu0ZyizUm{Cq+{OwBvWoebBRX% z^DQt9Vwe?ug?!qK@MGZPnnx#s?wvh%{_f@?*<9<@r5SUdHA}=Gk@GXX=DPdHvkujn zjMb)>uc%HhN*_5uIR;6E&S}RIwn=R0FATye{wW;7Ve{AVy*KkX9XbuF|1;Z*rKfb`_T#kS z`Z;sKv1@~Q0Oot^W;yIQPCEBgXeV(1rF^sUu!;8L&yG?M~BWtY=7P-Gkg@$Yz;z7|KugF@a@q%xt!!Xw^lH2cFOtnaK! zQo)UT!&^fY1CD+BgFZ3Ymo&=Eag?=*Ms8X1MOH-^Yv_y5dLv%1)N*P%pb{U&P01a5 z(JSki3+(YdA-tKkKTc4064oRoBue5!{Jk-LcYn&7k9MGv#t{Z{&@W_hI_L z@jv5gdI6#G&(0S@O=}_<)Nu(SR#aFt35u|I7-{EshG#n=_MCU+KC)poIP4_(NJCv{ zBqxoNYX1`ZQR{+ghb1NAxW5_n$KT_djrFQ_p0F%5M5btJlB_56YJanip)UynT_$}+ zpUwz4uPM#9il7Zf(bWIcnwso&@AI~pX(Rtc66Re}`2obl$5U_Me}8Pk0_F%=MCl-K zyG-zh*eA!G9hGKl!CpdkCnt=DG!Fm;#gts?E{$A{RF-jJli$pC8Mf}O?Z-+2LjRLI z?<>1PZ9+j$g9nPPDC(n)?u!}i+jC6TBWGeAC?h?MLo_3usT7Vr>* zvJ_I~$1VzW)!Z1{+20o(4UhQm25O;A)jci*#;6Bkfd<(aq-rO*!z}VQ=*MAN<=i&4 z!jE6>lXNcwLpxSUQXj5NwQg|3iE3K+s>xlEH}jQ$zvxe9+y%6BaS49t)afH8_5SBP zW8HGaDIAuWnZ*SWL;dw2kb(#UZ8|+QVMt0{9Qe^MiMQ~ohy?$iORnu~&(4c~?Msy3 zRbD`@U?o>V412s6-&=b^It_Q{ey%;>WvNjY5V)1TvQXcZ?|>Z`zG|-fdQZ`4)z8sl z;6&-N+WnE!h8pCGgt{Fn-)+vl;-U;4WKCs^8-q?d{19!T#*Re_BP@LKo$U@CnU%i&5Icu z!*@28DQW*BI^9b}N7%1_R0W@T2H^gp7{y1^a1(PHd(|F)+6X4~#~MSv-H`%N~P4(52d z$zz4ZtA{^6@?Lo-i42U}{tK5g-kR9t^k2$g0v5g3siYhVa9f2%VYJ3U_u({puh4|K zhpzbEak(#~R@kHAk@V6_jXO>GJpu5%Y|_7dpTOAY@bH7K=OGV&aYN1`TWt{jSd<>z zb8+adpbGwpX>YM_HGp}_(6dewgR4{bdVPfQtJXE%S7O-MmyjF`(fH`xD|OtZAmw(B zdNFX9afw9fzr~c8uK$EZsa@gr{*^n#a57)F9(eEO8bi>7tT00oJc1!o{*&u*D{XpO znLp^V9?e0~E|m9q<+#u7Gui?H^agQXTqRePhezB10w;{CUYIF24x2b5X?uO^q4siQ&&QH&#mKgSMrH96?%uK@tK==%>6s zGbr1C(e(3TSd+UA)`3i7x~H_StJpy=J=?W1UNf7R>q&Iaa6`c)aAxoIV*Fi-C0Y%^ zV!1oH7=m9UUE%93Ju3U~GsZi9^ttpg8xwbF-y>VL`n`T!80SWB>%(nFUcfz9yK^$p zH<)5#vQErC3tjg*me2QH8TCo~0eT-SSq&FLz3S=3uRMtppBWbC`lV-~lcchR=-hNp ztflS%Q^&6w+D`y}k27c56`)Z(-K`2T`-3N>MgLQK9jre9A@Uxh{lYwG3)+rLq1hfC zOh86X2amKVJaH1r4*3*AuoVcUUmHL#pB`QYM4(*Ec_uz#eNYsztdI9!rWVLH^Wbea?Ro8csSfjh)Ge=aKZM=cVD=4p=RGuBcg2hakj2fUCaYnY=ry(<_B+gK5I|3^o-W+}OTBJD z^I}-ys1ywSHXQDZ*|Oq( zcl|$8_Q7nVj96a~R~F1YREyc4Rh)y%MmVd0nv?@bRi?((oX_DAu^tN^C0HGa14j8bM$6>}c7u?5@ud2Ry>3)XG- zzo#jQk463nvo^hym)2uXWbufnp!H&$dHVrf_x=|(4Ep`}{r8_PiI}fv6p~aX9+`@> z{-!|ZAB|)GG>w5>#nGVZVwkrs%;~iCj};qZ1Up7%&zvfJ@h1Ub48!Q0X!PFG0PAt8 ztjl%i)?Y}lQFOnf0bI;d)qomMTTVp@o62P^+Ct>28~7W_|tsH8+f)-(_(|%Y7|$A2Te%ibK<;Ix!*y&CywXiK6Dy z*mo&NK;9MWA=jvlEwMo+r9ulc{?p|nCkdn5t44Kg|IDny%~O1&Xy$$3o80Zyx5RWb zN(6~7eLnGKg=tN)m#QEa0z6kesr|V+R8T**wG1OdI0tBiOWFc`!e1Cc0RyQK!)Y95 z3t)tHrUt7oMe&DA@ZVA;!f(OiMBrpB)vm7ZrvdDQLIVXiYFq&^(I2~Tij-DSSKy_S z{c-{Msy4av1M+j9qu(~9>9rUipIHll1JLEt%hfgM!(Yidaye$iK?Zm=Z1SuFf8V?S zR@~Bo9q+YdLVsNyi+DyLu0Z<(PhA;|K|vKAE!EBw-Ax#j`pJ@mhPD_;$v zR~G8>c8f|g`Jp`X(*Q`~*vkIny>)6_(IQY&i(dw)HH-YdC1uRx4Zwib_4PcBZ33pW zQQ_aOMvc|@*AsmyR^2}Z`S7Jb(ZQk>uxJ@OjPqh-+?~@7qG|YEDt-8t0W$I8mF4C@ z^+kze9b-XU9K*{KUpD1XEiJ5MRi@9zMvv`J=gThFugz~YV4%&Js*ruLYDrrA?+|;% zr0TPixINYHN=a*s%u3mq%cH6Dun-tn!b0iX7 zU8IApYvQRJn}|WrnpI?Edo@lyL#L?&xspHF2YF5mY!Oqd1w(kdJqpjd&{^3#hg$tI z0N_y2iz>CccZ_ZleOq&%{}4q(&hyv^*aV$Rz`T@{Coz+^)DA?)wEAwdX(+YrQ7Zq9 z7QKxh28=?UoRAAGKT~`D-yNYIW05cKcYtmb{vY3xl8x(swbB3aEy>s}Kq`B}A6e2N zX`FOGrcolvPqSpM%=$?YU z(BAK=lzCNa?&7pG1)GE=We(bbC|*8+i22AXFKuEzghw#M6`BkcpC02{Jpux9!3-U8 zkZLk<;b(D6gI~O!0~sEsX%4XaL->^E_)7aS&}Lz>=XBJ`z> zh4!dO{^-l1WbYZdXC|L^@wTF!*(q6orN`Gk^#=sqF=p*{%^8FOjK zo-=n7U*4a;&9o3g)#(J*7{(KsoVGRd+H4r$v|9deD1qHQ6?yP=-iWJ5fL@ifEbZ*A2Cku z5kYqtO!sRv#wgGUn(|X&T6t`~H2ySz@T@wZB5$Fw9~l-G2Tvy_bVaKeNPixGqHi&V zb@?@u*kTWFzHp!4f}t_5MB9Q5L$8&KbeT8)#JYhk8g8Y~w0N5^hTb=9Pm0n^!K>xs zjNl_pQ9}@SZ5jaIl^>Nkf`;rEcTKS8~2CWh+R(Lf$en02+cduq|~mF_OZ2%f38;p+;GuY@!A{ zy_-y273O!5f^@aWup9`8Z)iT1kL7a!DPni*c%l>uAL?)23eN*wz!2Prod>6f?v0$r zQEWA~fdF4I9?v1I)AnkZ4co~AKOic4jJn?_n8xO5?}x+&vs2S+)?>_py<8z_D0wDH z-~b(2;57kOe*u20ufRoP-?C{zz(q0$;M+99_p2$IIBUL1&1ybOED+@_EL%?hGA%^z zZgL7FP}ihP@;@Lrxf5UC`qNe2UjCSB1o85;smA+)XSTDvK~*ORg{~YPUi@C)G!+RxC_9Q_c#y$-VyFZ}F#-{ST3k;H7FZ`TfIh`vv{Q@tgpbV_S;#D4cj zK*7%)JR+rfd8K~ai!qw_VuDi7%knl9W2cf@JT!jeoJU=yR!?Zj7B5%A6yB846X`b; z{7SPU{Ud|WH!xI1O}Y2;t_Ab<==`Js|CI5z6K-GKl_*jy%o(-TdZ z=FVPka5TJZO=F@wul9Jbu62qRmY#L}9n(^G7m@(dAyo8%eCEg1Umb z)zyWXVOJ}JoUxJ3fmD3qWR^f>N~vIX$<9Bfpkeahba7pSN-V@UnKJS#;rayC%F}7k z+ye2Q>CakcbJ59vh_Z-3d^M^=M{I!OGqjoWgl!Px=R8LrZ(OhV%AQgU0IM5um#m~m zZ85y)1c?FU5)N9uQIn@@(&Wb2dqd55IjhUYymR@@nmD-W;d%V-8Uxg~G9(-8ern6s zBgZk*2-+DkTAS^^S25=0)L-6vzSW@Z9&3_os&AsDG^S1}^=Qp5T||Y9 zvgyleEBnXvSLK#ax7}7*?$TkWW+O53F=_BZZT?&j=<1AdKVM4x*w;#VK9>zIt-iAQ zthDb?g$BZ7k;>5QikEGseMNJAx#2U$Jkp08bvHBQGLAmxj}fD`IiBzR;jiwjpsOG- z)VF5Vs-h-{nJz9cCASn0#UiQ9MCcd@UCK556B%%2-Ky8NOpvn|ieL<>0UM z^UN?UG(J=AJZ)LntD+4oA~V8==iJ|M6`@ih9AZawh5bGkjDn^0B>icA=f-SRHbI1% zH&K~J4N=j|pi^5vR(MSVRoYZ_Q=VdtjSlnj2< z>BJjgr+gW7ddQMr;Ei;GndsI~)KQeHCKe81r68SImHX218V6ml)rsJHOf`*%ekB33Sx= z+EZ{8FukbEwWft3ynh_R!XvJv9&KT{D6ZMa=6!{wY*%2WppKoGKmVo1aGFood{Gu~ zjJ6@YFv^ItCocM??rKBX;-V=uBgN@UwPGeSJ>N&C53s3W^GCcgv?$7?=JtpIqY<9x zH8(TIy*djz@D@nu7>8iygLjVWyN}zFOpn1(+Tfcu!%H8E?Lm|5(QB^iSKken)fSt>D zktOvj3`5#^TVVZzO-q}>ohW`8KXn-##)Ro7V1T@5?9qxwYD%=q5l#J_RwbPn<__%=?VW%ccd3zu4GQsf_h7p3upR)4`X8*YoPq^5Epx;$vB&RE2;7X8y42=^`EyESYM^Kx->?Qbm+ zwGRU=+p+9p?L$99YOahs!q~j2IF3a$3f^$l2ZfxXY)bwK`8BO|0uX{4cpr8&lM8n3 z2(&%WDRTQfF9k6}EO9@lWCikoDKrN!r5qnU?_d@PpRQOG%pIY!6Fgjd$@jAdmM{QZ zLX~ZK-wwveXc&I2lT*PLnr7$hoyqh9jZ8lIEubdJ^+itpS0+h-1(>IZOVaCd!Gc0> zCC#=(XG$h2)$A}tQ=L6_IeqBpoeU2nQ+$<0Sw9 zBW<(KtPUcwJ+`^tSH>|fWG@A%y*Nt}^m%Tr9z^Knq$15Gcu#Y~6h(QSSC;Wjl71>Z zU#ac^%vZniDgg4a@P>0}JPvSCYFyV8=B)2q&f5K+y?$2lnMRJ{uEpLh#@*#aD$PRc ztlu{Velb+nm6W+eR5A(Z{(f>2*pGhfbSd$pt4aBA&(9g+@x7Bcv*_`gEP-h$$oAU` z?Svh4<>HTs&HDz3ODoi=-?m8{Z`O+Y+R7DUk#M|I{ev>`1Gg*~2d8HGCZ-ASr$YS1 z_gQ}(#x-F8FwuBkpoZqosNiAFzWTbqmlEq-y+oP(4%w#`S&r-bDeuD6x|9;nP4KN@%j3~M;C^k?wv|t z@uy!~^An7)6jWiNkk5c!^$Nr~7c+v4ipc3SZu1DFdy83*5=xq492|BjWU&6=w3Ryi z)Zm>Xvg*KjOvVqY&j`!cHJi;lp`;oZsO-8)_P@Yq$G*^9=zF|kH@Y4ChUeaw*r@uO zL;&+16kAPl3b~_hB~=>a13Z4t`4-eX_v0aeQ&Y{5t9#mnzin4BQ_&!v5}+xR$!r8N z)BDt9d7VZ=y;@$NxNr7(qt{K6G^rrktvDRPM0i55uTvfSbf=l=v%X*6mcyscvy-VD zx*LYmBbfsgqm+Po2XST3Scvau?ln5&@9%K;TW>IH28hjB#34UE-^dHBY}<0?x0wUW z$MQ)v_;29pF>0Z1>R3ZhB#-p ze|(JH_8QI3t9Zz;OR7#!r%01IA+0faA<`-S$F%S zxrkvW_&1TrKl<=jIPOvb0rz`{sNGAPlpoG|BhRHqbJjDE{h`vgKQ$?z60oJBH&b8ozq$wBy+?`jk{M=A=|Sp)qmPWQ z;ksu2c>=y@fTebqFPtn3*F!|KL_1!e+Ut)+Pl%{u+o(hT=SAi5$m_@%_JjXq`yW?@ zLV{npao4`zzsYLh%i(oZ5z>59rs<0@p0hvE94_hv?&u~j5AH9fJ0-05)sc)iU||}n{~~X&41Wk`7`uuvu>Dl1`{`a5GE&d~B}PZq^? zP{x}nWK+{MJ7=J`I=fuQ4X}a3Y$V;I9rmz$JO6y<{2Sjs*r*zb`6aib7T#8$3#~fT zuBzz&Uf@O=O+6X{o&CC%!+EX98vbhR8Q^^RQ*}yqQ1sXy@d3@aVn}1N3g4UD+`=?QQI3 zd(lmO=_`U3PRA=o;2Q4eM_f>a$u$4xFrt48xDSEg#<9y8PTtYLYGdno+eov56w;q2 zBX{i){6*D4>r5j^8*E}y#Jv@vT)H@Q@!|PAq|fNC-C7eG9SPoR*w-UgIJ)pv(U7y} zT(s9*wvrmls1!YlCU-dTSTw_;U=t^dw1$t+-a6|L0j`1T-dpM$hSl&Cf_v%f)qMn0 zuW$T@Wr{77>0Y;j06(5?61(m{sqO;N(+4Yl#HYe==QmEpaX^UlMjLXifd1PvAE` zch;brwosR4esh?^)`I6QEn+xt4gzY{IX>Q70VEjK`SA>u0&L`O%<8PkB`(k@UYT^o z2;jwb<(W*n7k0O3oQ74cD$=1UEF5;$3oDj??o!|4Yw4l+4w?c?dqBQYgq{G)*BgIv z*46$083pG}P@09s-qRi=U#*d4=&nx|YvyiapYCwkazfQdaQI(fFgt@z9tbYsFZ zpM$EazH}*JVP;bci6-n!z&dPQHGryY`$%eOAHMg0Ju%u&(*%VCxzxyzA_YlUNne4Q z`&??sW!i%B#u;(Rk93H=0CTOz#=QPMon$%guwHmjg!lc1Myn1h{Mx=YGLMM&fe%n0lwTrNz|{gzIT$&AwdBTJnFTRs+rn@ z3_CZ~L#pny(%3ZmWiNyi)RR?;bg7?6^|c~&JjVar;q9EcaT0o#mt6p)Di8|M%z@7i zEbTE#KUl0W%vtaC2_2kb%AW#{eARZzj()fI{B^wa8eQq^@ps@0UXXLmAqD&&#fseY z2P|iMSOeXEJ+sWN&|`#K+HZC^^^Xcm@STnC+?wa`W7qeV3=gnQVR8WBZiU8X;ERJd zzME~z|DvhLtCf#harS7Ud#ARs=V9dK_WskqUrpqQxX8|M-?rJ2fjWdqF3Km(X8=cZ z3+}uP>3bf4Vn6OwXEZwiQHu^pqzM_kp@K{!!q)D4-o9Po#xfRul_&P``5>flaMiCX zY1GOpr8kH6{n}dMUZNN$pNn97=ZpQkrY0HsUv&Nn9zFlgaDO8u*x2?53-&plr#$Kn zU>nl5%04^`%NwyDVBB&HdyFvGzL5#&d3lxG_rf-|-ZAZlM%)cpuAXjW9rk|eYCebH z(3YyTB-crGlb8^0#`oUXp$KwSO{J|SD6Z$bIGt%O;%0?lD+RK$GBq6GEUTvEj=deh z>|>;nS=Tsmo|b=@CZwBkO!q`sv)Kh)p^_egq@3!>Ez92WP03gBt7>cX4N)`F04QD> zT$A$MZjKr8wv$(Tt^R%Cp5$|_Kw;`YrcB^(+PDro*LnwaWuJ|iPcYb0xVeHQ#Vt3% zmE{uO+=;}$)sPSa`X=pG!dAMqVY>Ut4Xlz7K!}dxCSXO*>9Se+n)w z?n$qvihNo6DSqSk{Sq4<`^$%n6$~@pHd}}iDs3k*Xou4*bVLN^n>8phXCt|Xu-CFt zcsMt?m^}X9(ck9z33#gZ)AM}n(!+OP8`}${D||sLtaL*SlZSF;gn?TQ`CQLl?cqjz z7+#X`Y_BMXz)Gh(`J^r#%uKi(~}g4w+N zC)}K0Z)><`ast@Sl~SY0=q%^fuKC;Su6*RHQ3elPlwH=$zF^^)iz>}F6!3zgwA#6R zVlY25jw!1Ef=mKHj*{@SLgX+`XJ<}+V^bIUb6)#r)@Wm;5$1A-=~~zu6D6+h=inVl zLL?i$mQfSaud0BTgHXMZSR7&mV5n<=7l38>*mAxQT!1_y1e89KW<4)Iai3@BYF;?c z4Xg6O7J<#cU2bfc)Op4dTlv(`uGnB##4TC*e-xgZ-a7^A**T zR_=p6q*m#%x1TORL4qO}QZrw`rK%a80T(HcS2jEk+i}gJ_w{-2cNN6iG5b#8fdfy( zS|qr&Ii{csB&82Wsox9#1_|vcQ6Sxt$SNqPrAI`Tldp})dq>kHe?6;pBi#CZPl2%S zVFWW}Z|;f~H**z>zo!7nUN1a%-`x(I?-eFP!pYFS-~!QuX3>);qxh-u7`vY3-qWzP zfRk};`O4|f)?j9-+KJ#=F!um!F76&1?^}DZ_&)=$O~w%QI;|;D&)#wC<_+R+C8UTJ zR8ncH!*av(j54VK3M@kLoYUN9JrQ-VD%osf&((-w`fSecozJ?M6#U?E@j?-9M4_rT)TUHFRH3vN$9UJCvxRJ4< zx}Jy4_24CwG;^?+K;p)R{OZoAw5Sre>fk{&>01eHp28ZE=2E#m4w?lX7-NGK?8U`# zb;{zkdU3h?aGZO52Pts+MNkg=Jt^kG#Wq1{95I5|e&QvD+Jko4JAAOiWuU_&9w~0l z_0GUv#xbN(>;fpc^(f1uxId=ru*+&(1YYZxZ#$%FDqVNsk2xxbhwxDI84x`^40p~v zM%+qHze(E@(cR3+tRygdvnp-zmv zAkY_uu)W?thei+HosLrAU7kn+(DII}ZntLhyoh|7#WvospWDGTPr5J2Kd-yAa3BBc zb?OJd1dgVp_)ei~rA?rt(7Gr33uY8e`y!lc6jIqtlWbcLSOMyCH+ohYJH7K|-@CB9 z(n<|U2qv$^|9HCqt34BSycoRPQ!V6~3zv`EDfVk|6q(sTH6PxsR0vd6^Lb!4ZE%-% zOF*YTtc_c#~f!x#qm=ZowDuQKJgibbT_^Ky{jq`>m|EfO+85*g=E~>#!^CKO8tOmj3d?q^{8r4`A!s)jqxa=>|Sk=BgLmMX^IH zRpIzC-TL%JhnU8!p6fm|?YYEjMS{S6facwa3Or;a9>4-$z-@c)CTm(M3siLd~6-5_DY98k%P&Y{3VxHZxTS4*9oam z5mjmo5@}j_yXvpcjy z6Jit7PpVyP0IA54!$*Cu08=h)+V@e({(lhVZeh-JI)01!JCR!mUhvf8k&+^@+{H-G z-p?^|5ob*~M&JLo)ys>O|AI$58PRq7w{J_>PEHq6Jq9|Zw#7ZFFWX$`WJ6{)9uOmH z2mf$k7JXhjJTYSpWz*^v+3w0Tl(#?Y=rYK2%LSieJVfk-Z{sk^j*`B%QN2+k$W@?X1aruc zdVPUsnGGf!JFq2}60O$Kky-S5ymd-V4x{lRK4@t4-!Sb1`uNkmNrBWS+_kz{1%>T+ zeK=$bR_tzjQ?y2uBT+`9pk6rHa*wG! zdU-BPCBi<8eQG)wofz%IFp-@j%Lf*;LW407fiwQs|U-Z+d~ zTDK4(LMNy3s2S1nh|=G0X6?^=pJJzSJo`ffWf>qNn2^JXLZCdPHUiSB^TujTpl_6( zyzHhe%gkPWUv1WMQXY3OlG*Z@64Z6ZsY(7t;y|iBZyNlrS3e?QX2V*OeX8Maw&qvf z!)RI*8h*jXn5ASOw{lQ>fVxK|mVmY{-`AOpfbQsQ0~=TnGuw%*C9*&D;~A>$rt;V~ zTdCu8FCbgc{BbC{)qo@ks!L6U@q9qrcUQ5Uw4p&mWX(cbh6E2DK2lU|ld-)$f?=@Z z{qRLN2s-9YloP{xUVbs3BVRh#yIuXpW58p=!_sHs2~s?xF*7|RK64prU1VHvDFBa# zCc?}&-|}3?LDFA@UW}CCY1LnSF2U68aHE~0BHdg%$3iX7`^1~YDbFY?DT)kLx^J?$ zRe(E<&T0_;j}hS?)Q3;6MBg0YDaB;NG&Y(~TB5AbR7%h-3#2da+YYM^3&paW6DmkQ zR$5&U;;Ux(m*%Fa-UFk_;U7A00R)@$UL9Sbj~wc>?>d6V2=WN5vhLOO7f@MU5$=kaFRQmX{C-mOEm`u->-NEiHjDl9G@R@IK2QYi z_gIpI3?!KMs?#c*GL7#{X`b>aYdB_n3fB?QO1mM+NU;zWdZ3KV}XwnF!!(gBZ>Tr z4Pdrg?z6L{WW8!FSAqB{D=yg>*%|y>beaLs)bbIi&&)){y2w6_r=fwuJmrkywjdw}#37+UC#9harXk+s6F`0OMGUzFLm~d+EX_67zU&p#Nza zFf{|F9RZtJ0hZw zz5EA4ORbCHZzPaCI~$Vsbpy=ra!)=#kAC}yBzvDeI{!4}(h&UO2bm$!)6-0o|I>oP zI4*#o1bSv@c(>)f(`cS)-3sSLzFhDYU{l~b>*+XY+qY}U2RFzlUzLKAJn@?k(XD?p zTq343`wdKgX}-U)zob=D`*xH4gdHe0wjMTbJrHO;L?aAXtarZuLm6PV&uE?s5jhQcNtg_qQMDpTr?#&)+BUm1P`Sk)BNVovrHJthP zS)VcpS|qjtx`w;;eEO&Q@e3ou9E2|bg!{(W`$Vt>R)6fqy9qGPfL;r*-GEI2rfC64 zIY&=i;v|Ux0001lG6X6K=5#R~Sk~6KkCxUQ19F9Get*b9$w6N-&mW9ZR~}xwjIAU1 zKU@2bPr*b#+oRB`o9o@GqbB=zH3I;tB=l$Q)NBC&R>OYA7Jw=M)N(uOi6wxHYrIa{ zv1rEkgoH(aAd`yg@BaM!8VS6!NiM*`Z2)v5TFob!T+d~}a{B#=jO(kqe#NUbl6d_3 z>{H+Jz0JGJmaUU~@o(>IpXdgnmrj6FiE))JK<&fv0lt_>F3Hs0JHO55^#|p@wSCdK z0X`WK0N7mYH#;t{HO1WAj-MXyP~iCRVP`a=m{}$@YallLpoJ$2{?5-8@ZxDe zYb4jR$rCw1TwnR7Ydq?M42oC}ch)h-2lC1!=4jzC-TF+Yi0ehD zE`2v`9^!twYkqipw`RX_Ijlbu#k$FbB8E*=6e}IC#t7i_EWf$rq@KI3836y&j2O_0 z>_Bjjkfxtr{QTruN`P}v3P}JlPMh!9uX34V+hf38&anQ6t(3f_V(w|TobAGF9$|Mq zyex@tW$N3;x$E|BppLObE=d4LE`So#vIO}L?9KVTDN7qxCr@`yflb0VN6bl>jtuw? zpaOn1+|uhW{c1(A?L*gaCmsUfj;e2qVTbzL0Y4y+ek|hCt97y`#CQbvI5+0&2TV}_ z=f+V0H0II8B2f&PBnbcj000~S0HsXm`!%hf6%F1kx~cu4g3xyP!oha8MQx?uzBr}f z?0?%HYlBeR&2GMbx0u=79})n5MH25%0{z_Q<@N0+^eck33(aAveC^TVdP#S)AQ1W6 z-EMZh-}#Xp$JNCWh3^PhF%)<{1Z{mHhI?G1z%=_E`^&DL!Ln3H+1ExxaON{B3Dg$= zY;hET1`o@xxLXy+NI+))9bf|h;65S20X6^t02Xj9V1r#q0P9PnQ{hr6(uGq&0bl{1 zHN4YPa=hvaoo%nLd_mRlBDWIzr#{Q4X*>193An0j=@0l{H2a;OZ1zh{gYj_z&D8YB z1QGxM0000000000sFJsGT7r)rUOV#AvFKZvPj{VLc;BS=$&hM0rK4p09r}HlcnC|y z7gJ&!%pLp3zLySYUE*`k_vft!V9xatta;^$1Ce9_O#Ftkdm56WR9=5iWbV%tkvDle zlr{>L<6VB(<+>Kn2&;1I+gru~QvkKhM#ha!lCS`&0Dd)m=tE+E#4=x=j)%_*SHlmv zNO&Ky?1$|nIG+t9aG;q1j3G4;dNHG8a00000000000001hTzA2JILaigM%E6MgFnkBd#UICY5bu6 z+Gn28&FJc?UgsOxe7VWy?@vntO98d?;sXbKNs|=-0Dd+6@2Fo8zy_{{{~h%U0@#26 T00000000000002szk!tj=@K!p literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/assets/gentle.ogg b/blender/arm/lightmapper/assets/gentle.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e36d106defb89ff4bb0bb014e87f868d83d5e5c5 GIT binary patch literal 6615 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�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

W4s2c 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/panels/__init__.py b/blender/arm/lightmapper/panels/__init__.py deleted file mode 100644 index 85678ea8..00000000 --- a/blender/arm/lightmapper/panels/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -import bpy, os -from bpy.utils import register_class, unregister_class -from . import scene, object, light, world - -classes = [ - scene.TLM_PT_Panel, - scene.TLM_PT_Settings, - scene.TLM_PT_Denoise, - scene.TLM_PT_Filtering, - scene.TLM_PT_Encoding, - scene.TLM_PT_Selection, - scene.TLM_PT_Additional, - object.TLM_PT_ObjectMenu, - light.TLM_PT_LightMenu, - world.TLM_PT_WorldMenu -] - -def register(): - for cls in classes: - register_class(cls) - -def unregister(): - for cls in classes: - unregister_class(cls) \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/light.py b/blender/arm/lightmapper/panels/light.py deleted file mode 100644 index fd576af1..00000000 --- a/blender/arm/lightmapper/panels/light.py +++ /dev/null @@ -1,17 +0,0 @@ -import bpy -from bpy.props import * -from bpy.types import Menu, Panel - -class TLM_PT_LightMenu(bpy.types.Panel): - bl_label = "The Lightmapper" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "light" - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - scene = context.scene - obj = bpy.context.object - layout.use_property_split = True - layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/object.py b/blender/arm/lightmapper/panels/object.py deleted file mode 100644 index 13d15188..00000000 --- a/blender/arm/lightmapper/panels/object.py +++ /dev/null @@ -1,59 +0,0 @@ -import bpy -from bpy.props import * -from bpy.types import Menu, Panel - -class TLM_PT_ObjectMenu(bpy.types.Panel): - bl_label = "The Lightmapper" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "object" - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - scene = context.scene - obj = bpy.context.object - layout.use_property_split = True - layout.use_property_decorate = False - - if obj.type == "MESH": - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") - - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - - row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") - row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") - row = layout.row() - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": - pass - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") - row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") - row = layout.row() - if obj.TLM_ObjectProperties.tlm_mesh_filter_override: - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") - row = layout.row(align=True) - if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") - elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") - elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") - else: - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) - row = layout.row(align=True) - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/scene.py b/blender/arm/lightmapper/panels/scene.py deleted file mode 100644 index 846d357a..00000000 --- a/blender/arm/lightmapper/panels/scene.py +++ /dev/null @@ -1,322 +0,0 @@ -import bpy, importlib -from bpy.props import * -from bpy.types import Menu, Panel -from .. utility import icon -from .. properties.denoiser import oidn, optix - -class TLM_PT_Panel(bpy.types.Panel): - bl_label = "The Lightmapper" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - -class TLM_PT_Settings(bpy.types.Panel): - bl_label = "Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - - row = layout.row(align=True) - - #We list LuxCoreRender as available, by default we assume Cycles exists - row.prop(sceneProperties, "tlm_lightmap_engine") - - if sceneProperties.tlm_lightmap_engine == "Cycles": - - #CYCLES SETTINGS HERE - engineProperties = scene.TLM_EngineProperties - - row = layout.row(align=True) - row.label(text="General Settings") - row = layout.row(align=True) - row.operator("tlm.build_lightmaps") - row = layout.row(align=True) - row.operator("tlm.clean_lightmaps") - row = layout.row(align=True) - row.operator("tlm.explore_lightmaps") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_apply_on_unwrap") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_headless") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_alert_on_finish") - - row = layout.row(align=True) - row.label(text="Cycles Settings") - - row = layout.row(align=True) - row.prop(engineProperties, "tlm_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_quality") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_resolution_scale") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_bake_mode") - - if scene.TLM_EngineProperties.tlm_bake_mode == "Background": - row = layout.row(align=True) - row.label(text="Warning! Background mode is currently unstable", icon_value=2) - row = layout.row(align=True) - row.prop(engineProperties, "tlm_caching_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_directional_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_lightmap_savedir") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_dilation_margin") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_exposure_multiplier") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_setting_supersample") - - elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - - #LUXCORE SETTINGS HERE - luxcore_available = False - - #Look for Luxcorerender in the renderengine classes - for engine in bpy.types.RenderEngine.__subclasses__(): - if engine.bl_idname == "LUXCORE": - luxcore_available = True - break - - row = layout.row(align=True) - if not luxcore_available: - row.label(text="Please install BlendLuxCore.") - else: - row.label(text="LuxCoreRender not yet available.") - - elif sceneProperties.tlm_lightmap_engine == "OctaneRender": - - #LUXCORE SETTINGS HERE - octane_available = False - - row = layout.row(align=True) - row.label(text="Octane Render not yet available.") - -class TLM_PT_Denoise(bpy.types.Panel): - bl_label = "Denoise" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw_header(self, context): - scene = context.scene - sceneProperties = scene.TLM_SceneProperties - self.layout.prop(sceneProperties, "tlm_denoise_use", text="") - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - layout.active = sceneProperties.tlm_denoise_use - - row = layout.row(align=True) - - #row.prop(sceneProperties, "tlm_denoiser", expand=True) - #row = layout.row(align=True) - row.prop(sceneProperties, "tlm_denoise_engine", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_denoise_engine == "Integrated": - row.label(text="No options for Integrated.") - elif sceneProperties.tlm_denoise_engine == "OIDN": - denoiseProperties = scene.TLM_OIDNEngineProperties - row.prop(denoiseProperties, "tlm_oidn_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_threads") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_maxmem") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_affinity") - # row = layout.row(align=True) - # row.prop(denoiseProperties, "tlm_denoise_ao") - elif sceneProperties.tlm_denoise_engine == "Optix": - denoiseProperties = scene.TLM_OptixEngineProperties - row.prop(denoiseProperties, "tlm_optix_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_maxmem") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_denoise_ao") - -class TLM_PT_Filtering(bpy.types.Panel): - bl_label = "Filtering" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw_header(self, context): - scene = context.scene - sceneProperties = scene.TLM_SceneProperties - self.layout.prop(sceneProperties, "tlm_filtering_use", text="") - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - layout.active = sceneProperties.tlm_filtering_use - #row = layout.row(align=True) - #row.label(text="TODO MAKE CHECK") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_filtering_engine", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_filtering_engine == "OpenCV": - - cv2 = importlib.util.find_spec("cv2") - - if cv2 is None: - row = layout.row(align=True) - row.label(text="OpenCV is not installed. Install it through preferences.") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") - row = layout.row(align=True) - if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") - - -class TLM_PT_Encoding(bpy.types.Panel): - bl_label = "Encoding" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw_header(self, context): - scene = context.scene - sceneProperties = scene.TLM_SceneProperties - self.layout.prop(sceneProperties, "tlm_encoding_use", text="") - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - layout.active = sceneProperties.tlm_encoding_use - - sceneProperties = scene.TLM_SceneProperties - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_mode", expand=True) - if sceneProperties.tlm_encoding_mode == "RGBM" or sceneProperties.tlm_encoding_mode == "RGBD": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_range") - if sceneProperties.tlm_encoding_mode == "LogLuv": - pass - if sceneProperties.tlm_encoding_mode == "HDR": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_format") - -class TLM_PT_Selection(bpy.types.Panel): - bl_label = "Selection" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.use_property_split = True - layout.use_property_decorate = False - sceneProperties = scene.TLM_SceneProperties - - row = layout.row(align=True) - row.operator("tlm.enable_selection") - row = layout.row(align=True) - row.operator("tlm.disable_selection") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_object_settings") - - if sceneProperties.tlm_override_object_settings: - - row = layout.row(align=True) - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") - row = layout.row() - - if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroup": - - if scene.TLM_AtlasList_index >= 0 and len(scene.TLM_AtlasList) > 0: - row = layout.row() - item = scene.TLM_AtlasList[scene.TLM_AtlasList_index] - row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') - else: - row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") - - else: - - row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_unwrap_margin") - - row = layout.row(align=True) - row.operator("tlm.remove_uv_selection") - row = layout.row(align=True) - -class TLM_PT_Additional(bpy.types.Panel): - bl_label = "Additional" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = {'DEFAULT_CLOSED'} - bl_parent_id = "TLM_PT_Panel" - - def draw(self, context): - layout = self.layout - scene = context.scene - sceneProperties = scene.TLM_SceneProperties \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/world.py b/blender/arm/lightmapper/panels/world.py deleted file mode 100644 index b3c5d294..00000000 --- a/blender/arm/lightmapper/panels/world.py +++ /dev/null @@ -1,17 +0,0 @@ -import bpy -from bpy.props import * -from bpy.types import Menu, Panel - -class TLM_PT_WorldMenu(bpy.types.Panel): - bl_label = "The Lightmapper" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "world" - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - scene = context.scene - obj = bpy.context.object - layout.use_property_split = True - layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/scene.py b/blender/arm/lightmapper/properties/scene.py index e1cddb62..b7da0bc4 100644 --- a/blender/arm/lightmapper/properties/scene.py +++ b/blender/arm/lightmapper/properties/scene.py @@ -108,10 +108,10 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_filtering_engine : EnumProperty( items = [('OpenCV', 'OpenCV', 'Make use of OpenCV based image filtering (Requires it to be installed first in the preferences panel)'), - ('Numpy', 'Numpy', 'Make use of Numpy based image filtering (Integrated)')], + ('Shader', 'Shader', 'Make use of GPU offscreen shader to filter')], name = "Filtering library", description="Select which filtering library to use.", - default='Numpy') + default='OpenCV') #Numpy Filtering options tlm_numpy_filtering_mode : EnumProperty( @@ -261,4 +261,13 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_headless : BoolProperty( name="Don't apply materials", description="Headless; Do not apply baked materials on finish.", - default=False) \ No newline at end of file + default=False) + + tlm_alert_sound : EnumProperty( + items = [('dash', 'Dash', 'Dash alert'), + ('noot', 'Noot', 'Noot alert'), + ('gentle', 'Gentle', 'Gentle alert'), + ('pingping', 'Ping', 'Ping alert')], + name = "Alert sound", + description="Alert sound when lightmap building finished.", + default="gentle") \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/build.py b/blender/arm/lightmapper/utility/build.py index 33632daa..c4b05a7e 100644 --- a/blender/arm/lightmapper/utility/build.py +++ b/blender/arm/lightmapper/utility/build.py @@ -1,7 +1,7 @@ import bpy, os, importlib, subprocess, sys, threading, platform, aud from . import encoding from . cycles import lightmap, prepare, nodes, cache -from . denoiser import integrated, oidn +from . denoiser import integrated, oidn, optix from . filtering import opencv from os import listdir from os.path import isfile, join @@ -56,6 +56,8 @@ def prepare_build(self=0, background_mode=False): self.report({'INFO'}, "Error:Filtering - OpenCV not installed") return{'FINISHED'} + setMode() + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) if not os.path.isdir(dirpath): os.mkdir(dirpath) @@ -234,7 +236,24 @@ def begin_build(): del denoiser else: - pass + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + optixProperties = scene.TLM_OptixEngineProperties + + denoiser = optix.TLM_Optix_Denoise(optixProperties, baked_image_array, dirpath) + + denoiser.denoise() + + denoiser.clean() + + del denoiser #Filtering if sceneProperties.tlm_filtering_use: @@ -444,13 +463,23 @@ def manage_build(background_pass=False): if scene.TLM_SceneProperties.tlm_alert_on_finish: + alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + + if alertSelect == "dash": + soundfile = "dash.ogg" + elif alertSelect == "pingping": + soundfile = "pingping.ogg" + elif alertSelect == "gentle": + soundfile = "gentle.ogg" + else: + soundfile = "noot.ogg" + scriptDir = os.path.dirname(os.path.realpath(__file__)) - sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/sound.ogg')) + sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) device = aud.Device() sound = aud.Sound.file(sound_path) device.play(sound) - print("ALERT!") def reset_settings(prev_settings): scene = bpy.context.scene @@ -468,6 +497,8 @@ def reset_settings(prev_settings): cycles.device = prev_settings[9] scene.render.engine = prev_settings[10] bpy.context.view_layer.objects.active = prev_settings[11] + scene.render.resolution_x = prev_settings[13][0] + scene.render.resolution_y = prev_settings[13][1] #for obj in prev_settings[12]: # obj.select_set(True) @@ -550,15 +581,6 @@ def check_denoiser(): else: return 0 - # if scene.TLM_SceneProperties.tlm_denoise_use: - # if scene.TLM_SceneProperties.tlm_oidn_path == "": - # print("NO DENOISE PATH") - # return False - # else: - # return True - # else: - # return True - def check_materials(): for obj in bpy.data.objects: if obj.type == "MESH": @@ -582,4 +604,9 @@ def sec_to_hours(seconds): b=str((seconds%3600)//60) c=str((seconds%3600)%60) d=["{} hours {} mins {} seconds".format(a, b, c)] - return d \ No newline at end of file + return d + +def setMode(): + bpy.ops.object.mode_set(mode='OBJECT') + + #TODO Make some checks that returns to previous selection \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/cache.py b/blender/arm/lightmapper/utility/cycles/cache.py index a4269204..4c168e66 100644 --- a/blender/arm/lightmapper/utility/cycles/cache.py +++ b/blender/arm/lightmapper/utility/cycles/cache.py @@ -19,11 +19,13 @@ def backup_material_rename(obj): if "TLM_PrevMatArray" in obj: print("Has PrevMat B") for slot in obj.material_slots: - if slot.material.name.endswith("_Original"): - newname = slot.material.name[1:-9] - if newname in bpy.data.materials: - bpy.data.materials.remove(bpy.data.materials[newname]) - slot.material.name = newname + + if slot.material is not None: + if slot.material.name.endswith("_Original"): + newname = slot.material.name[1:-9] + if newname in bpy.data.materials: + bpy.data.materials.remove(bpy.data.materials[newname]) + slot.material.name = newname del obj["TLM_PrevMatArray"] @@ -45,30 +47,9 @@ def backup_material_restore(obj): except IndexError: originalMaterial = "" - slot.material.user_clear() + if slot.material is not None: + slot.material.user_clear() - if "." + originalMaterial + "_Original" in bpy.data.materials: - slot.material = bpy.data.materials["." + originalMaterial + "_Original"] - slot.material.use_fake_user = False - - - #slot.material = - - #Remove material after changin - #We only rename after every change is complete - - #if "." + material.name + "_Original" in bpy.data.materials: - - # material = slot.material - # if "." + material.name + "_Original" in bpy.data.materials: - # original = bpy.data.materials["." + material.name + "_Original"] - # slot.material = original - # material.name = material.name - # original.name = original.name[1:-9] - # original.use_fake_user = False - # material.user_clear() - # bpy.data.materials.remove(material) - # #Reset number - # else: - # pass - #Check if material has nodes with lightmap prefix \ No newline at end of file + if "." + originalMaterial + "_Original" in bpy.data.materials: + slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + slot.material.use_fake_user = False \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/nodes.py b/blender/arm/lightmapper/utility/cycles/nodes.py index 174a1867..1494b0fb 100644 --- a/blender/arm/lightmapper/utility/cycles/nodes.py +++ b/blender/arm/lightmapper/utility/cycles/nodes.py @@ -56,8 +56,10 @@ def apply_materials(): load_library("Exposure") #Apply materials + print(obj.name) for slot in obj.material_slots: mat = slot.material + print(slot.material) node_tree = mat.node_tree nodes = mat.node_tree.nodes diff --git a/blender/arm/lightmapper/utility/cycles/prepare.py b/blender/arm/lightmapper/utility/cycles/prepare.py index 32ae7127..413ffc16 100644 --- a/blender/arm/lightmapper/utility/cycles/prepare.py +++ b/blender/arm/lightmapper/utility/cycles/prepare.py @@ -76,6 +76,13 @@ def configure_meshes(self): if obj.type == "MESH": if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + objWasHidden = False + + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) + currentIterNum = currentIterNum + 1 #Configure selection @@ -244,6 +251,14 @@ def preprocess_material(obj, scene): obj["TLM_PrevMatArray"] = matArray + #We check and safeguard against NoneType + for slot in obj.material_slots: + if slot.material is None: + matName = obj.name + ".00" + str(0) + bpy.data.materials.new(name=matName) + slot.material = bpy.data.materials[matName] + slot.material.use_nodes = True + for slot in obj.material_slots: cache.backup_material_copy(slot) @@ -253,54 +268,6 @@ def preprocess_material(obj, scene): copymat = mat.copy() slot.material = copymat - # for slot in obj.material_slots: - # matname = slot.material.name - # originalName = "." + matname + "_Original" - # hasOriginal = False - # if originalName in bpy.data.materials: - # hasOriginal = True - # else: - # hasOriginal = False - - # if hasOriginal: - # cache.backup_material_restore(slot) - - # cache.backup_material_copy(slot) - - ############################ - - #Make a material backup and restore original if exists - # if scene.TLM_SceneProperties.tlm_caching_mode == "Copy": - # for slot in obj.material_slots: - # matname = slot.material.name - # originalName = "." + matname + "_Original" - # hasOriginal = False - # if originalName in bpy.data.materials: - # hasOriginal = True - # else: - # hasOriginal = False - - # if hasOriginal: - # matcache.backup_material_restore(slot) - - # matcache.backup_material_copy(slot) - - # else: #Cache blend - # #TEST CACHE - # filepath = bpy.data.filepath - # dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_SceneProperties.tlm_lightmap_savedir) - # path = dirpath + "/cache.blend" - # bpy.ops.wm.save_as_mainfile(filepath=path, copy=True) - #print("Warning: Cache blend not supported") - - # for mat in bpy.data.materials: - # if mat.name.endswith('_baked'): - # bpy.data.materials.remove(mat, do_unlink=True) - # for img in bpy.data.images: - # if img.name == obj.name + "_baked": - # bpy.data.images.remove(img, do_unlink=True) - - #SOME ATLAS EXCLUSION HERE? ob = obj for slot in ob.material_slots: @@ -448,5 +415,6 @@ def store_existing(prev_container): cycles.device, scene.render.engine, bpy.context.view_layer.objects.active, - selected + selected, + [scene.render.resolution_x, scene.render.resolution_y] ] \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/denoiser/optix.py b/blender/arm/lightmapper/utility/denoiser/optix.py index eae92b27..aff36919 100644 --- a/blender/arm/lightmapper/utility/denoiser/optix.py +++ b/blender/arm/lightmapper/utility/denoiser/optix.py @@ -1,7 +1,89 @@ -import bpy, os +import bpy, os, platform, subprocess -class TLM_OIDN_Denoise: +class TLM_Optix_Denoise: - def __init__(self): + image_array = [] - pass \ No newline at end of file + image_output_destination = "" + + denoised_array = [] + + def __init__(self, optixProperties, img_array, dirpath): + + self.optixProperties = optixProperties + + self.image_array = img_array + + self.image_output_destination = dirpath + + self.check_binary() + + def check_binary(self): + + optixPath = self.optixProperties.tlm_optix_path + + if optixPath != "": + + file = os.path.basename(os.path.realpath(optixPath)) + filename, file_extension = os.path.splitext(file) + + if(file_extension == ".exe"): + + #if file exists optixDenoise or denoise + + pass + + else: + + #if file exists optixDenoise or denoise + + self.optixProperties.tlm_optix_path = os.path.join(self.optixProperties.tlm_optix_path,"Denoiser.exe") + + else: + + print("Please provide Optix path") + + def denoise(self): + + print("Optix: Denoising") + for image in self.image_array: + + if image not in self.denoised_array: + + image_path = os.path.join(self.image_output_destination, image) + + denoise_output_destination = image_path[:-10] + "_denoised.hdr" + + if platform.system() == 'Windows': + optixPath = bpy.path.abspath(self.optixProperties.tlm_optix_path) + pipePath = [optixPath, '-i', image_path, '-o', denoise_output_destination] + elif platform.system() == 'Darwin': + print("Mac for Optix is still unsupported") + else: + print("Linux for Optix is still unsupported") + + if self.optixProperties.tlm_optix_verbose: + denoisePipe = subprocess.Popen(pipePath, shell=True) + else: + denoisePipe = subprocess.Popen(pipePath, stdout=subprocess.PIPE, stderr=None, shell=True) + + denoisePipe.communicate()[0] + + image = bpy.data.images.load(image_path, check_existing=False) + bpy.data.images[image.name].filepath_raw = bpy.data.images[image.name].filepath_raw[:-4] + "_denoised.hdr" + bpy.data.images[image.name].reload() + + def clean(self): + + self.denoised_array.clear() + self.image_array.clear() + + for file in self.image_output_destination: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + #self.image_output_destination + + #Clean temporary files here.. + #...pfm + #...denoised.hdr \ No newline at end of file diff --git a/blender/arm/props.py b/blender/arm/props.py index f87f7319..8004efbd 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -8,7 +8,7 @@ import arm.proxy import arm.utils # Armory version -arm_version = '2020.7' +arm_version = '2020.8' arm_commit = '$Id$' def init_properties(): diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py index f2554461..93d69647 100644 --- a/blender/arm/props_bake.py +++ b/blender/arm/props_bake.py @@ -3,7 +3,7 @@ import arm.assets import bpy from bpy.types import Menu, Panel, UIList from bpy.props import * -from arm.lightmapper import operators, panels, properties, preferences, utility, keymap +from arm.lightmapper import operators, properties, preferences, utility, keymap class ArmBakeListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") @@ -362,7 +362,6 @@ def register(): operators.register() properties.register() preferences.register() - panels.register() keymap.register() def unregister(): @@ -384,5 +383,4 @@ def unregister(): operators.unregister() properties.unregister() preferences.unregister() - panels.unregister() keymap.unregister() \ No newline at end of file diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index c580e1f0..40db59fc 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1863,4 +1863,4 @@ def unregister(): bpy.utils.unregister_class(ArmProxyApplyAllButton) bpy.utils.unregister_class(ArmSyncProxyButton) bpy.utils.unregister_class(ArmPrintTraitsButton) - bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) + bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) \ No newline at end of file