Refactor CollisionShape generating in physics

1. Remove triangulating in exporting any convex CollisionShape
2. Add utils to reuse mesh exporting related functions in CollisionShape
exporting
This commit is contained in:
Jason0214
2019-08-04 13:32:49 -06:00
parent de3815fa51
commit bae7431678
6 changed files with 249 additions and 195 deletions

View File

@@ -1,15 +1,15 @@
"""Exports a normal triangle mesh"""
import logging
import bpy
import bmesh
import mathutils
from .material import export_material
from ..structures import (
Array, NodeTemplate, InternalResource, Map, gamma_correct)
from . import physics
from . import armature
from . import animation
from .utils import MeshConverter, MeshResourceKey
from .physics import has_physics, export_physics_properties
from .armature import generate_bones_mapping
from .animation import export_animation_data
MAX_BONE_PER_VERTEX = 4
@@ -21,8 +21,8 @@ def export_mesh_node(escn_file, export_settings, obj, parent_gd_node):
# If this mesh object has physics properties, we need to export them first
# because they need to be higher in the scene-tree
if physics.has_physics(obj):
parent_gd_node = physics.export_physics_properties(
if has_physics(obj):
parent_gd_node = export_physics_properties(
escn_file, export_settings, obj, parent_gd_node
)
# skip wire mesh which is used as collision mesh
@@ -59,7 +59,7 @@ def export_mesh_node(escn_file, export_settings, obj, parent_gd_node):
# Transform of rigid mesh is moved up to its collision
# shapes.
if physics.has_physics(obj):
if has_physics(obj):
mesh_node['transform'] = mathutils.Matrix.Identity(4)
else:
mesh_node['transform'] = obj.matrix_local
@@ -69,25 +69,13 @@ def export_mesh_node(escn_file, export_settings, obj, parent_gd_node):
# export shape key animation
if (export_settings['use_export_shape_key'] and
has_shape_keys(obj.data)):
animation.export_animation_data(
export_animation_data(
escn_file, export_settings, mesh_node,
obj.data.shape_keys, 'shapekey')
return mesh_node
def triangulate_mesh(mesh):
"""Triangulate a mesh"""
tri_mesh = bmesh.new()
tri_mesh.from_mesh(mesh)
bmesh.ops.triangulate(
tri_mesh, faces=tri_mesh.faces, quad_method="ALTERNATE")
tri_mesh.to_mesh(mesh)
tri_mesh.free()
mesh.update(calc_loop_triangles=True)
def fix_vertex(vtx):
"""Changes a single position vector from y-up to z-up"""
return mathutils.Vector((vtx.x, vtx.z, -vtx.y))
@@ -119,85 +107,12 @@ def export_object_link_material(escn_file, export_settings, mesh_object,
)
def get_applicable_modifiers(obj, export_settings):
"""Returns a list of all the modifiers that'll be applied to the final
godot mesh"""
ignore_modifiers = []
if not export_settings['use_mesh_modifiers']:
return []
if "ARMATURE" in export_settings['object_types']:
ignore_modifiers.append(bpy.types.ArmatureModifier)
ignore_modifiers = tuple(ignore_modifiers)
return [m for m in obj.modifiers if not isinstance(m, ignore_modifiers)
and m.show_viewport]
def record_modifier_config(obj):
"""Returns modifiers viewport visibility config"""
modifier_config_cache = []
for mod in obj.modifiers:
modifier_config_cache.append(mod.show_viewport)
return modifier_config_cache
def restore_modifier_config(obj, modifier_config_cache):
"""Applies modifiers viewport visibility config"""
for i, mod in enumerate(obj.modifiers):
mod.show_viewport = modifier_config_cache[i]
def has_shape_keys(object_data):
"""Determine if object data has shape keys"""
return (hasattr(object_data, "shape_keys") and
object_data.shape_keys is not None)
class MeshResourceKey:
"""Produces a key based on an mesh object's data, every different
Mesh Resource would have a unique key"""
def __init__(self, rsc_type, obj, export_settings):
mesh_data = obj.data
# Resource type included because same blender mesh may be used as
# MeshResource or CollisionShape, but they are different resource
gd_rsc_type = rsc_type
# Here collect info of all the modifiers applied on the mesh.
# Modifiers along with the original mesh data would determine
# the evaluated mesh.
mod_info_list = list()
for modifier in get_applicable_modifiers(obj, export_settings):
# Modifier name indicates its type, its an identifier
mod_info_list.append(modifier.name)
# First property is always 'rna_type', skip it
for prop in modifier.bl_rna.properties.keys()[1:]:
# Note that Property may be `BoolProperty`,
# `CollectionProperty`, `EnumProperty`, `FloatProperty`,
# `IntProperty`, `PointerProperty`, `StringProperty`"
# Most of them are primary type when accessed with `getattr`,
# so they are fine to be hashed.
# For `PointerProperty`, it is mostly an bpy.types.ID, hash it
# would get its python object identifier, which is also good.
# For `CollectionProperty`, it would make more sense to
# traversal it, however, we cut down it here to allow
# some of mesh resource not be shared because of simplicity
mod_info_list.append(getattr(modifier, prop))
self._data = tuple([mesh_data, gd_rsc_type] + mod_info_list)
# Precalculate the hash now for better efficiency later
self._hash = hash(self._data)
def __hash__(self):
return self._hash
def __eq__(self, other):
# pylint: disable=protected-access
return (self.__class__ == other.__class__ and
self._data == other._data)
class ArrayMeshResource(InternalResource):
"""Godot ArrayMesh resource, containing surfaces"""
@@ -232,8 +147,7 @@ class ArrayMeshResourceExporter:
if armature_obj is None:
return
bones_mapping = armature.generate_bones_mapping(export_settings,
armature_obj)
bones_mapping = generate_bones_mapping(export_settings, armature_obj)
for bl_bone_name, gd_bone_id in bones_mapping.items():
group = self.object.vertex_groups.get(bl_bone_name)
if group is not None:
@@ -310,7 +224,7 @@ class ArrayMeshResourceExporter:
)
mesh_converter = MeshConverter(self.object, export_settings)
shape_key_mesh = mesh_converter.to_mesh(index)
shape_key_mesh = mesh_converter.to_mesh(shape_key_index=index)
surfaces_morph_data = self.intialize_surfaces_morph_data(surfaces)
@@ -675,68 +589,3 @@ class Vertex:
self.uv = []
self.bones = []
self.weights = []
class MeshConverter:
"""MeshConverter evaulates and converts objects to meshes, triangulates
and calculates tangents"""
def __init__(self, obj, export_settings):
self.object = obj
self.eval_object = None
self.use_mesh_modifiers = export_settings["use_mesh_modifiers"]
self.use_export_shape_key = export_settings['use_export_shape_key']
self.has_tangents = False
def to_mesh(self, shape_key_index=0, calculate_tangents=True):
"""Evaluates object & converts to final mesh, ready for export.
The mesh is only temporary, call to_mesh_clear() afterwards."""
# set shape key to basis key which would have index 0
orig_shape_key_index = self.object.active_shape_key_index
self.object.show_only_shape_key = True
self.object.active_shape_key_index = shape_key_index
self.eval_object = self.object
modifier_config_cache = None
if not self.use_mesh_modifiers:
modifier_config_cache = record_modifier_config(self.object)
for mod in self.object.modifiers:
mod.show_viewport = False
depsgraph = bpy.context.view_layer.depsgraph
depsgraph.update()
self.eval_object = self.object.evaluated_get(depsgraph)
# These parameters are required for preserving vertex groups.
mesh = self.eval_object.to_mesh(
preserve_all_data_layers=True, depsgraph=depsgraph)
if not self.use_mesh_modifiers:
restore_modifier_config(self.object, modifier_config_cache)
self.has_tangents = False
# mesh result can be none if the source geometry has no faces, so we
# need to consider this if we want a robust exporter.
if mesh is not None:
triangulate_mesh(mesh)
self.has_tangents = mesh.uv_layers and mesh.polygons
if calculate_tangents:
if self.has_tangents:
mesh.calc_tangents()
else:
mesh.calc_normals_split()
self.object.show_only_shape_key = False
self.object.active_shape_key_index = orig_shape_key_index
return mesh
def to_mesh_clear(self):
"""Clears the temporary generated mesh from memory"""
if self.object is None:
return
self.eval_object.to_mesh_clear()
self.object = self.eval_object = None

View File

@@ -7,6 +7,7 @@ physics owns the object.
import logging
import mathutils
from ..structures import NodeTemplate, InternalResource, Array, _AXIS_CORRECT
from .utils import MeshConverter, MeshResourceKey
PHYSICS_TYPES = {'KinematicBody', 'RigidBody', 'StaticBody'}
@@ -72,10 +73,17 @@ def export_collision_shape(escn_file, export_settings, node, parent_gd_node,
col_shape = None
if rbd.collision_shape in ("CONVEX_HULL", "MESH"):
is_convex = rbd.collision_shape == "CONVEX_HULL"
shape_id = generate_shape_mesh(
escn_file, export_settings,
node, is_convex
)
if rbd.collision_shape == "CONVEX_HULL":
shape_id = generate_convex_shape(
escn_file, export_settings, node
)
else: # "MESH"
shape_id = generate_concave_shape(
escn_file, export_settings, node
)
if shape_id is not None:
col_node['shape'] = "SubResource({})".format(shape_id)
else:
bounds = get_extents(node)
if rbd.collision_shape == "BOX":
@@ -96,56 +104,95 @@ def export_collision_shape(escn_file, export_settings, node, parent_gd_node,
else:
logging.warning("Unable to export physics shape for %s", node.name)
col_node['shape'] = "SubResource({})".format(shape_id)
if col_shape is not None and rbd.use_margin:
col_shape['margin'] = rbd.collision_margin
if shape_id is not None:
col_node['shape'] = "SubResource({})".format(shape_id)
escn_file.add_node(col_node)
return col_node
def generate_shape_mesh(escn_file, export_settings, node, is_convex):
"""Generates godots PolygonShape from a blender mesh object"""
# pylint: disable-msg=cyclic-import
from .mesh import (MeshConverter, MeshResourceKey)
class MeshCollisionShapeKey:
"""Produces a resource key based on an mesh object's data and rigid
propertys"""
def __init__(self, shape_type, bl_object, export_settings):
assert shape_type in ("ConvexPolygonShape", "ConcavePolygonShape")
margin = 0
if node.rigid_body.use_margin:
margin = node.rigid_body.collision_margin
mesh_data_key = MeshResourceKey(shape_type, bl_object, export_settings)
# margin is the property that stores in CollisionShape in Godot
margin = 0
if bl_object.rigid_body.use_margin:
margin = bl_object.rigid_body.collision_margin
# Build the Shape resource hash key with rigid margin and mesh data
if is_convex:
shape_rsc_type = "ConvexPolygonShape"
else:
shape_rsc_type = "ConcavePolygonShape"
mesh_data_key = MeshResourceKey(shape_rsc_type, node, export_settings)
shape_rsc_key = (margin, mesh_data_key)
self._data = tuple((margin, mesh_data_key))
def __hash__(self):
return hash(self._data)
def __eq__(self, other):
# pylint: disable=protected-access
return (self.__class__ == other.__class__ and
self._data == other._data)
def generate_convex_shape(escn_file, export_settings, bl_object):
"""Generates godots ConvexCollisionShape from a blender mesh object"""
shape_rsc_key = MeshCollisionShapeKey(
"ConvexPolygonShape", bl_object, export_settings)
shape_id = escn_file.get_internal_resource(shape_rsc_key)
if shape_id is not None:
return shape_id
# No cached Shape found, build new one
col_shape = None
mesh_converter = MeshConverter(node, export_settings)
mesh = mesh_converter.to_mesh(calculate_tangents=False)
mesh_converter = MeshConverter(bl_object, export_settings)
mesh = mesh_converter.to_mesh(
triangulate=False,
preserve_vertex_groups=False,
calculate_tangents=False
)
if mesh is not None:
vert_array = [vert.co for vert in mesh.vertices]
col_shape = InternalResource("ConvexPolygonShape", mesh.name)
col_shape['points'] = Array("PoolVector3Array(", values=vert_array)
if bl_object.rigid_body.use_margin:
col_shape['margin'] = bl_object.rigid_body.collision_margin
shape_id = escn_file.add_internal_resource(col_shape, shape_rsc_key)
mesh_converter.to_mesh_clear()
return shape_id
def generate_concave_shape(escn_file, export_settings, bl_object):
"""Generates godots ConcaveCollisionShape from a blender mesh object"""
shape_rsc_key = MeshCollisionShapeKey(
"ConcavePolygonShape", bl_object, export_settings)
shape_id = escn_file.get_internal_resource(shape_rsc_key)
if shape_id is not None:
return shape_id
# No cached Shape found, build new one
col_shape = None
mesh_converter = MeshConverter(bl_object, export_settings)
mesh = mesh_converter.to_mesh(
triangulate=True,
preserve_vertex_groups=False,
calculate_tangents=False
)
if mesh is not None and mesh.polygons:
vert_array = list()
for poly in mesh.polygons:
for vert_id in poly.vertices:
vert_array.append(list(mesh.vertices[vert_id].co))
if is_convex:
col_shape = InternalResource("ConvexPolygonShape", mesh.name)
col_shape['points'] = Array("PoolVector3Array(", values=vert_array)
else:
col_shape = InternalResource("ConcavePolygonShape", mesh.name)
col_shape['data'] = Array("PoolVector3Array(", values=vert_array)
col_shape = InternalResource("ConcavePolygonShape", mesh.name)
col_shape['data'] = Array("PoolVector3Array(", values=vert_array)
if node.rigid_body.use_margin:
col_shape['margin'] = node.rigid_body.collision_margin
if bl_object.rigid_body.use_margin:
col_shape['margin'] = bl_object.rigid_body.collision_margin
shape_id = escn_file.add_internal_resource(col_shape, shape_rsc_key)

View File

@@ -0,0 +1,158 @@
"""Util functions and structs shared by multiple resource converters"""
import bpy
import bmesh
def get_applicable_modifiers(obj, export_settings):
"""Returns a list of all the modifiers that'll be applied to the final
godot mesh"""
ignore_modifiers = []
if not export_settings['use_mesh_modifiers']:
return []
if "ARMATURE" in export_settings['object_types']:
ignore_modifiers.append(bpy.types.ArmatureModifier)
ignore_modifiers = tuple(ignore_modifiers)
return [m for m in obj.modifiers if not isinstance(m, ignore_modifiers)
and m.show_viewport]
def record_modifier_config(obj):
"""Returns modifiers viewport visibility config"""
modifier_config_cache = []
for mod in obj.modifiers:
modifier_config_cache.append(mod.show_viewport)
return modifier_config_cache
def restore_modifier_config(obj, modifier_config_cache):
"""Applies modifiers viewport visibility config"""
for i, mod in enumerate(obj.modifiers):
mod.show_viewport = modifier_config_cache[i]
def triangulate_mesh(mesh):
"""Triangulate a mesh"""
tri_mesh = bmesh.new()
tri_mesh.from_mesh(mesh)
bmesh.ops.triangulate(
tri_mesh, faces=tri_mesh.faces, quad_method="ALTERNATE")
tri_mesh.to_mesh(mesh)
tri_mesh.free()
mesh.update(calc_loop_triangles=True)
class MeshResourceKey:
"""Produces a key based on an mesh object's data, every different
Mesh Resource would have a unique key"""
def __init__(self, rsc_type, obj, export_settings):
mesh_data = obj.data
# Resource type included because same blender mesh may be used as
# MeshResource or CollisionShape, but they are different resource
gd_rsc_type = rsc_type
# Here collect info of all the modifiers applied on the mesh.
# Modifiers along with the original mesh data would determine
# the evaluated mesh.
mod_info_list = list()
for modifier in get_applicable_modifiers(obj, export_settings):
# Modifier name indicates its type, its an identifier
mod_info_list.append(modifier.name)
# First property is always 'rna_type', skip it
for prop in modifier.bl_rna.properties.keys()[1:]:
# Note that Property may be `BoolProperty`,
# `CollectionProperty`, `EnumProperty`, `FloatProperty`,
# `IntProperty`, `PointerProperty`, `StringProperty`"
# Most of them are primary type when accessed with `getattr`,
# so they are fine to be hashed.
# For `PointerProperty`, it is mostly an bpy.types.ID, hash it
# would get its python object identifier, which is also good.
# For `CollectionProperty`, it would make more sense to
# traversal it, however, we cut down it here to allow
# some of mesh resource not be shared because of simplicity
mod_info_list.append(getattr(modifier, prop))
self._data = tuple([mesh_data, gd_rsc_type] + mod_info_list)
# Precalculate the hash now for better efficiency later
self._hash = hash(self._data)
def __hash__(self):
return self._hash
def __eq__(self, other):
# pylint: disable=protected-access
return (self.__class__ == other.__class__ and
self._data == other._data)
class MeshConverter:
"""MeshConverter evaulates and converts objects to meshes, triangulates
and calculates tangents"""
def __init__(self, obj, export_settings):
self.object = obj
self.eval_object = None
self.use_mesh_modifiers = export_settings["use_mesh_modifiers"]
self.use_export_shape_key = export_settings['use_export_shape_key']
self.has_tangents = False
def to_mesh(self, triangulate=True, preserve_vertex_groups=True,
calculate_tangents=True, shape_key_index=0):
"""Evaluates object & converts to final mesh, ready for export.
The mesh is only temporary, call to_mesh_clear() afterwards."""
# set shape key to basis key which would have index 0
orig_shape_key_index = self.object.active_shape_key_index
self.object.show_only_shape_key = True
self.object.active_shape_key_index = shape_key_index
self.eval_object = self.object
modifier_config_cache = None
if not self.use_mesh_modifiers:
modifier_config_cache = record_modifier_config(self.object)
for mod in self.object.modifiers:
mod.show_viewport = False
depsgraph = bpy.context.view_layer.depsgraph
depsgraph.update()
self.eval_object = self.object.evaluated_get(depsgraph)
# These parameters are required for preserving vertex groups.
mesh = self.eval_object.to_mesh(
preserve_all_data_layers=preserve_vertex_groups,
depsgraph=depsgraph
)
if not self.use_mesh_modifiers:
restore_modifier_config(self.object, modifier_config_cache)
self.has_tangents = False
# mesh result can be none if the source geometry has no faces, so we
# need to consider this if we want a robust exporter.
if mesh is not None:
if triangulate:
triangulate_mesh(mesh)
self.has_tangents = mesh.uv_layers and mesh.polygons
if calculate_tangents:
if self.has_tangents:
mesh.calc_tangents()
else:
mesh.calc_normals_split()
self.object.show_only_shape_key = False
self.object.active_shape_key_index = orig_shape_key_index
return mesh
def to_mesh_clear(self):
"""Clears the temporary generated mesh from memory"""
if self.object is None:
return
self.eval_object.to_mesh_clear()
self.object = self.eval_object = None

View File

@@ -3,7 +3,7 @@
[sub_resource id=1 type="ConvexPolygonShape"]
resource_name = "Cube"
points = PoolVector3Array(1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, 0.999999, -1.0, 4.09789, 1.0, 0.999999, 4.09789, 1.0, 0.999999, 4.09789, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 0.999999, -1.0, 4.09789, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 4.09789, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, 1.0, 0.999999, 4.09789, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, -1.0, -1.0, 4.09789, 0.999999, -1.0, 4.09789, 1.0, 0.999999, 4.09789, 0.999999, -1.0, 4.09789, 1.0, -1.0, -1.0, 0.999999, -1.0, 4.09789, -1.0, -1.0, 4.09789, -1.0, -1.0, -1.0, -1.0, -1.0, 4.09789, -1.0, 1.0, 4.09789, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 4.09789)
points = PoolVector3Array(1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 0.999999, 4.09789, 0.999999, -1.0, 4.09789, -1.0, -1.0, 4.09789, -1.0, 1.0, 4.09789)
[sub_resource id=2 type="ArrayMesh"]
@@ -27,7 +27,7 @@ surfaces/0 = {
[sub_resource id=3 type="ConvexPolygonShape"]
resource_name = "Cube003"
points = PoolVector3Array(1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, 0.999999, -1.0, 4.09789, 1.0, 0.999999, 4.09789, 1.0, 0.999999, 4.09789, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 0.999999, -1.0, 4.09789, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 4.09789, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, 1.0, 0.999999, 4.09789, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 4.09789, -1.0, -1.0, 4.09789, 0.999999, -1.0, 4.09789, 1.0, 0.999999, 4.09789, 0.999999, -1.0, 4.09789, 1.0, -1.0, -1.0, 0.999999, -1.0, 4.09789, -1.0, -1.0, 4.09789, -1.0, -1.0, -1.0, -1.0, -1.0, 4.09789, -1.0, 1.0, 4.09789, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 4.09789)
points = PoolVector3Array(1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 0.999999, 4.09789, 0.999999, -1.0, 4.09789, -1.0, -1.0, 4.09789, -1.0, 1.0, 4.09789)
[sub_resource id=4 type="Animation"]

View File

@@ -3,7 +3,7 @@
[sub_resource id=1 type="ConvexPolygonShape"]
resource_name = "Cube002"
points = PoolVector3Array(-0.8, -0.8, 1.2, -0.8, 0.8, -1.2, -0.8, -0.8, -1.2, -0.8, 0.8, 1.2, 0.8, 0.8, -1.2, -0.8, 0.8, -1.2, 0.8, 0.8, 1.2, 0.8, -0.8, -1.2, 0.8, 0.8, -1.2, 0.8, -0.8, 1.2, -0.8, -0.8, -1.2, 0.8, -0.8, -1.2, 0.8, 0.8, -1.2, -0.8, -0.8, -1.2, -0.8, 0.8, -1.2, -0.8, 0.8, 1.2, 0.8, -0.8, 1.2, 0.8, 0.8, 1.2, -0.8, -0.8, 1.2, -0.8, 0.8, 1.2, -0.8, 0.8, -1.2, -0.8, 0.8, 1.2, 0.8, 0.8, 1.2, 0.8, 0.8, -1.2, 0.8, 0.8, 1.2, 0.8, -0.8, 1.2, 0.8, -0.8, -1.2, 0.8, -0.8, 1.2, -0.8, -0.8, 1.2, -0.8, -0.8, -1.2, 0.8, 0.8, -1.2, 0.8, -0.8, -1.2, -0.8, -0.8, -1.2, -0.8, 0.8, 1.2, -0.8, -0.8, 1.2, 0.8, -0.8, 1.2)
points = PoolVector3Array(-0.8, -0.8, -1.2, -0.8, -0.8, 1.2, -0.8, 0.8, -1.2, -0.8, 0.8, 1.2, 0.8, -0.8, -1.2, 0.8, -0.8, 1.2, 0.8, 0.8, -1.2, 0.8, 0.8, 1.2)
[sub_resource id=2 type="ArrayMesh"]

View File

@@ -3,7 +3,7 @@
[sub_resource id=1 type="ConvexPolygonShape"]
resource_name = "Cube002"
points = PoolVector3Array(-4.0, 1.33333, 0.3, -4.0, 4.0, -0.3, -4.0, 1.33333, -0.3, 1.33333, 4.0, 0.3, 4.0, 4.0, -0.3, 1.33333, 4.0, -0.3, 4.0, -1.33333, 0.3, 4.0, -4.0, -0.3, 4.0, -1.33333, -0.3, -1.33333, -4.0, 0.3, -4.0, -4.0, -0.3, -1.33333, -4.0, -0.3, 4.0, -1.33333, -0.3, 1.33333, -4.0, -0.3, 1.33333, -1.33333, -0.3, -4.0, -1.33333, 0.3, -1.33333, -4.0, 0.3, -1.33333, -1.33333, 0.3, 1.33333, -1.33333, 0.3, 4.0, -4.0, 0.3, 4.0, -1.33333, 0.3, -1.33333, -1.33333, 0.3, 1.33333, -4.0, 0.3, 1.33333, -1.33333, 0.3, 1.33333, 4.0, 0.3, 4.0, 1.33333, 0.3, 4.0, 4.0, 0.3, 1.33333, 1.33333, 0.3, 4.0, -1.33333, 0.3, 4.0, 1.33333, 0.3, -1.33333, 4.0, 0.3, 1.33333, 1.33333, 0.3, 1.33333, 4.0, 0.3, -1.33333, -1.33333, -0.3, -1.33333, 1.33333, 0.3, -1.33333, -1.33333, 0.3, -4.0, 4.0, 0.3, -1.33333, 1.33333, 0.3, -1.33333, 4.0, 0.3, -4.0, 1.33333, 0.3, -1.33333, -1.33333, 0.3, -1.33333, 1.33333, 0.3, -1.33333, -1.33333, -0.3, -4.0, -4.0, -0.3, -4.0, -1.33333, -0.3, 1.33333, -1.33333, -0.3, -1.33333, -4.0, -0.3, -1.33333, -1.33333, -0.3, -1.33333, 4.0, -0.3, -4.0, 1.33333, -0.3, -4.0, 4.0, -0.3, -1.33333, 1.33333, -0.3, -4.0, -1.33333, -0.3, -4.0, 1.33333, -0.3, 1.33333, 4.0, -0.3, -1.33333, 1.33333, -0.3, -1.33333, 4.0, -0.3, 1.33333, -1.33333, -0.3, -1.33333, -1.33333, 0.3, 1.33333, -1.33333, 0.3, 4.0, 4.0, -0.3, 1.33333, 1.33333, -0.3, 1.33333, 4.0, -0.3, 4.0, 1.33333, -0.3, 1.33333, -1.33333, -0.3, 1.33333, 1.33333, -0.3, 4.0, -4.0, 0.3, 1.33333, -4.0, -0.3, 4.0, -4.0, -0.3, 1.33333, -4.0, 0.3, -1.33333, -4.0, -0.3, 1.33333, -4.0, -0.3, 4.0, 4.0, 0.3, 4.0, 1.33333, -0.3, 4.0, 4.0, -0.3, 4.0, 1.33333, 0.3, 4.0, -1.33333, -0.3, 4.0, 1.33333, -0.3, -4.0, 4.0, 0.3, -1.33333, 4.0, -0.3, -4.0, 4.0, -0.3, -1.33333, 4.0, 0.3, 1.33333, 4.0, -0.3, -1.33333, 4.0, -0.3, -4.0, -4.0, 0.3, -4.0, -1.33333, -0.3, -4.0, -4.0, -0.3, -4.0, -1.33333, 0.3, -4.0, 1.33333, -0.3, -4.0, -1.33333, -0.3, -1.33333, 1.33333, -0.3, 1.33333, 1.33333, 0.3, -1.33333, 1.33333, 0.3, 1.33333, 1.33333, -0.3, 1.33333, -1.33333, 0.3, 1.33333, 1.33333, 0.3, -4.0, 1.33333, 0.3, -4.0, 4.0, 0.3, -4.0, 4.0, -0.3, 1.33333, 4.0, 0.3, 4.0, 4.0, 0.3, 4.0, 4.0, -0.3, 4.0, -1.33333, 0.3, 4.0, -4.0, 0.3, 4.0, -4.0, -0.3, -1.33333, -4.0, 0.3, -4.0, -4.0, 0.3, -4.0, -4.0, -0.3, 4.0, -1.33333, -0.3, 4.0, -4.0, -0.3, 1.33333, -4.0, -0.3, -4.0, -1.33333, 0.3, -4.0, -4.0, 0.3, -1.33333, -4.0, 0.3, 1.33333, -1.33333, 0.3, 1.33333, -4.0, 0.3, 4.0, -4.0, 0.3, -1.33333, -1.33333, 0.3, -1.33333, -4.0, 0.3, 1.33333, -4.0, 0.3, 1.33333, 4.0, 0.3, 1.33333, 1.33333, 0.3, 4.0, 1.33333, 0.3, 1.33333, 1.33333, 0.3, 1.33333, -1.33333, 0.3, 4.0, -1.33333, 0.3, -1.33333, 4.0, 0.3, -1.33333, 1.33333, 0.3, 1.33333, 1.33333, 0.3, -1.33333, -1.33333, -0.3, -1.33333, 1.33333, -0.3, -1.33333, 1.33333, 0.3, -4.0, 4.0, 0.3, -4.0, 1.33333, 0.3, -1.33333, 1.33333, 0.3, -4.0, 1.33333, 0.3, -4.0, -1.33333, 0.3, -1.33333, -1.33333, 0.3, -1.33333, -1.33333, -0.3, -1.33333, -4.0, -0.3, -4.0, -4.0, -0.3, 1.33333, -1.33333, -0.3, 1.33333, -4.0, -0.3, -1.33333, -4.0, -0.3, -1.33333, 4.0, -0.3, -1.33333, 1.33333, -0.3, -4.0, 1.33333, -0.3, -1.33333, 1.33333, -0.3, -1.33333, -1.33333, -0.3, -4.0, -1.33333, -0.3, 1.33333, 4.0, -0.3, 1.33333, 1.33333, -0.3, -1.33333, 1.33333, -0.3, 1.33333, -1.33333, -0.3, -1.33333, -1.33333, -0.3, -1.33333, -1.33333, 0.3, 4.0, 4.0, -0.3, 4.0, 1.33333, -0.3, 1.33333, 1.33333, -0.3, 4.0, 1.33333, -0.3, 4.0, -1.33333, -0.3, 1.33333, -1.33333, -0.3, 4.0, -4.0, 0.3, 1.33333, -4.0, 0.3, 1.33333, -4.0, -0.3, 1.33333, -4.0, 0.3, -1.33333, -4.0, 0.3, -1.33333, -4.0, -0.3, 4.0, 4.0, 0.3, 4.0, 1.33333, 0.3, 4.0, 1.33333, -0.3, 4.0, 1.33333, 0.3, 4.0, -1.33333, 0.3, 4.0, -1.33333, -0.3, -4.0, 4.0, 0.3, -1.33333, 4.0, 0.3, -1.33333, 4.0, -0.3, -1.33333, 4.0, 0.3, 1.33333, 4.0, 0.3, 1.33333, 4.0, -0.3, -4.0, -4.0, 0.3, -4.0, -1.33333, 0.3, -4.0, -1.33333, -0.3, -4.0, -1.33333, 0.3, -4.0, 1.33333, 0.3, -4.0, 1.33333, -0.3, -1.33333, 1.33333, -0.3, 1.33333, 1.33333, -0.3, 1.33333, 1.33333, 0.3, 1.33333, 1.33333, -0.3, 1.33333, -1.33333, -0.3, 1.33333, -1.33333, 0.3)
points = PoolVector3Array(-4.0, -4.0, -0.3, -4.0, -4.0, 0.3, -4.0, 4.0, -0.3, -4.0, 4.0, 0.3, 4.0, -4.0, -0.3, 4.0, -4.0, 0.3, 4.0, 4.0, -0.3, 4.0, 4.0, 0.3, -4.0, 1.33333, -0.3, -4.0, -1.33333, -0.3, -4.0, -1.33333, 0.3, -4.0, 1.33333, 0.3, 1.33333, 4.0, -0.3, -1.33333, 4.0, -0.3, -1.33333, 4.0, 0.3, 1.33333, 4.0, 0.3, 4.0, -1.33333, -0.3, 4.0, 1.33333, -0.3, 4.0, 1.33333, 0.3, 4.0, -1.33333, 0.3, -1.33333, -4.0, -0.3, 1.33333, -4.0, -0.3, 1.33333, -4.0, 0.3, -1.33333, -4.0, 0.3, 1.33333, 1.33333, 0.3, 1.33333, -1.33333, 0.3, -1.33333, 1.33333, 0.3, -1.33333, -1.33333, 0.3, -1.33333, 1.33333, -0.3, -1.33333, -1.33333, -0.3, 1.33333, 1.33333, -0.3, 1.33333, -1.33333, -0.3)
[sub_resource id=2 type="ArrayMesh"]