Updated func_godot

This commit is contained in:
MaddoScientisto 2025-12-28 22:53:18 +01:00
commit 01a852de9b
170 changed files with 1705 additions and 2296 deletions

View file

@ -122,7 +122,7 @@ class GroupData extends RefCounted:
class EntityData extends RefCounted:
## All of the entity's key value pairs from the map file, retrieved during parsing.
## The func_godot_properties dictionary generated at the end of entity assembly is derived from this.
var properties: Dictionary = {}
var properties: Dictionary[String, Variant] = {}
## The entity's brush data collected during the parsing stage. If the entity's FGD resource cannot be found,
## the presence of a single brush determines this entity to be a Solid Entity.
var brushes: Array[BrushData] = []
@ -154,6 +154,12 @@ class EntityData extends RefCounted:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.build_visuals)
func is_gi_enabled() -> bool:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.global_illumination_mode
)
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Convex.
func is_collision_convex() -> bool:
@ -172,12 +178,12 @@ class EntityData extends RefCounted:
## Determines if the entity's mesh should be processed for normal smoothing.
## The smoothing property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_property].
func is_smooth_shaded(smoothing_property: String = "_phong") -> bool:
return properties.get(smoothing_property, "0").to_int()
return properties.get(smoothing_property, 0)
## Retrieves the entity's smoothing angle to determine if the face should be smoothed.
## The smoothing angle property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_angle_property].
func get_smoothing_angle(smoothing_angle_property: String = "_phong_angle") -> float:
return properties.get(smoothing_angle_property, "89.0").to_float()
return properties.get(smoothing_angle_property, 89.0)
class VertexGroupData:
## Faces this vertex appears in.

View file

@ -51,14 +51,14 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
if script is Script:
node = script.new()
else:
node = Node3D.new()
node.name = node_name
node_name = node_name.trim_suffix(definition.classname).trim_suffix("_")
var properties: Dictionary = data.properties
var properties: Dictionary[String, Variant] = data.properties
# Mesh Instance generation
if data.mesh:
@ -95,9 +95,17 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
node.add_child(occluder_instance)
data.occluder_instance = occluder_instance
# NOTE: Currently occuring in EntityAssembler until the appropriate method in GeometryGenerator is resolved
# For now, smooth entire mesh, then unwrap for lightmap if needed
if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING) and data.is_smooth_shaded(map_settings.entity_smoothing_property):
mesh_instance.mesh = FuncGodotUtil.smooth_mesh_by_angle(data.mesh, data.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
if data.is_gi_enabled() and (build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2):
mesh_instance.mesh.lightmap_unwrap(
Transform3D.IDENTITY,
map_settings.uv_unwrap_texel_size * map_settings.scale_factor
)
# Collision generation
if data.shapes.size() and node is CollisionObject3D:
node.collision_layer = definition.collision_layer
@ -131,7 +139,9 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
if "position" in node:
if node.position is Vector3:
node.position = FuncGodotUtil.id_to_opengl(data.origin) * map_settings.scale_factor
node.position = FuncGodotUtil.id_to_opengl(data.origin)
elif node.position is Vector2:
node.position = Vector2(data.origin.z, -data.origin.y) * map_settings.inverse_scale_factor
if not data.mesh_metadata.is_empty():
node.set_meta("func_godot_mesh_data", data.mesh_metadata)
@ -152,9 +162,9 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
else:
if script is Script:
node = script.new()
if not node:
node = Node3D.new()
node.name = node_name
@ -164,22 +174,28 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
if "angles" in properties or "mangle" in properties:
var key := "angles" if "angles" in properties else "mangle"
var angles_raw = properties[key]
if not angles_raw is Vector3:
if angles_raw is String:
angles_raw = angles_raw.split_floats(' ')
if angles_raw.size() > 2:
if angles_raw.size() < 3:
push_error("Invalid vector format for \"" + key + "\" in entity \"" + classname + "\"")
angles_raw = null
if angles_raw:
angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2])
if key == "mangle":
if definition.classname.begins_with("light"):
angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
elif definition.classname == "info_intermission":
angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2])
else:
push_error("Invalid vector format for \"" + key + "\" in entity \"" + classname + "\"")
elif "angle" in properties:
var angle = properties["angle"]
if not angle is float:
angle = float(angle)
angles.y += angle
if is_equal_approx(angle, -1):
angles.x = 90
elif is_equal_approx(angle, -2):
angles.x = -90
else:
angles.y += angle
angles.y += 180
node.rotation_degrees = angles
@ -203,11 +219,18 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
if "origin" in properties:
var origin_vec: Vector3 = Vector3.ZERO
var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
var origin_prop = properties['origin']
if origin_prop is Vector3:
origin_vec = Vector3(origin_prop.y, origin_prop.z, origin_prop.x)
elif origin_prop is String:
var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
else:
push_error("Invalid vector format for \"origin\" in " + node_name)
else:
push_error("Invalid vector format for \"origin\" in " + node_name)
if "position" in node:
if node.position is Vector3:
node.position = origin_vec * map_settings.scale_factor
@ -220,127 +243,15 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
## based upon the [FuncGodotFGDEntity]'s class properties, then attempts to send those properties to a [code]func_godot_properties[/code] [Dictionary]
## and an [code]_func_godot_apply_properties(properties: Dictionary)[/code] method on the node. A deferred call to [code]_func_godot_build_complete()[/code] is also made.
func apply_entity_properties(node: Node, data: _EntityData) -> void:
var properties: Dictionary = data.properties
var properties: Dictionary[String, Variant] = data.properties
if data.definition:
var def := data.definition
for property in properties:
var prop_string = properties[property]
if property in def.class_properties:
var prop_default: Variant = def.class_properties[property]
match typeof(prop_default):
TYPE_INT:
properties[property] = prop_string.to_int()
TYPE_FLOAT:
properties[property] = prop_string.to_float()
TYPE_BOOL:
properties[property] = bool(prop_string.to_int())
TYPE_VECTOR3:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 2:
properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
else:
push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR3I:
var prop_vec: Vector3i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
for i in 3:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_COLOR:
var prop_color: Color = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
prop_color.r8 = prop_comps[0].to_int()
prop_color.g8 = prop_comps[1].to_int()
prop_color.b8 = prop_comps[2].to_int()
prop_color.a = 1.0
else:
push_error("Invalid Color format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = def.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
properties[property] = prop_string.to_int()
TYPE_ARRAY:
properties[property] = prop_string.to_int()
TYPE_VECTOR2:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 1:
properties[property] = Vector2(prop_comps[0], prop_comps[1])
else:
push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR2I:
var prop_vec: Vector2i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 1:
for i in 2:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_VECTOR4:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 3:
properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
else:
push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR4I:
var prop_vec: Vector4i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 3:
for i in 4:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_STRING_NAME:
properties[property] = StringName(prop_string)
TYPE_NODE_PATH:
properties[property] = prop_string
TYPE_OBJECT:
properties[property] = prop_string
# Assign properties not defined with defaults from the entity definition
for property in def.class_properties:
if not property in properties:
var prop_default: Variant = def.class_properties[property]
# Flags
if prop_default is Array:
var prop_flags_sum := 0
for prop_flag in prop_default:
if prop_flag is Array and prop_flag.size() > 2:
if prop_flag[2] and prop_flag[1] is int:
prop_flags_sum += prop_flag[1]
properties[property] = prop_flags_sum
# Choices
elif prop_default is Dictionary:
var prop_desc = def.class_property_descriptions.get(property, "")
if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
properties[property] = prop_desc[1]
elif prop_default.size():
properties[property] = prop_default[prop_default.keys().front()]
else:
properties[property] = 0
# Materials, Shaders, and Sounds
elif prop_default is Resource:
properties[property] = prop_default.resource_path
# Target Destination and Target Source
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
if def.auto_apply_to_matching_node_properties:
for property in properties:
if property == 'scale' and def is FuncGodotFGDPointClass and def.apply_scale_on_map_build:
# scale has already been applied
continue
if property in node:
if typeof(node.get(property)) == typeof(properties[property]):
node.set(property, properties[property])
@ -361,49 +272,44 @@ func apply_entity_properties(node: Node, data: _EntityData) -> void:
func generate_entity_node(entity_data: _EntityData, entity_index: int) -> Node:
var node: Node = null
var node_name: String = "entity_%s" % entity_index
var properties: Dictionary = entity_data.properties
var properties: Dictionary[String, Variant] = entity_data.properties
var entity_def: FuncGodotFGDEntityClass = entity_data.definition
if "classname" in entity_data.properties:
var classname: String = properties["classname"]
node_name += "_" + properties["classname"]
var default_point_def := FuncGodotFGDPointClass.new()
var default_solid_def := FuncGodotFGDSolidClass.new()
default_solid_def.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
if entity_def:
var name_prop: String
if entity_def.name_property in properties:
name_prop = str(properties[entity_def.name_property])
elif map_settings.entity_name_property in properties:
name_prop = str(properties[map_settings.entity_name_property])
if not name_prop.is_empty():
node_name = "entity_" + name_prop
if entity_def is FuncGodotFGDSolidClass:
node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
elif entity_def is FuncGodotFGDPointClass:
node = generate_point_entity_node(node, node_name, properties, entity_def)
else:
push_error("Invalid entity definition for \"" + node_name + "\". Entity definition must be Solid Class or Point Class.")
node = generate_point_entity_node(node, node_name, properties, default_point_def)
if node and entity_def.script_class:
var name_prop: String
if entity_def.name_property in properties:
name_prop = str(properties[entity_def.name_property])
elif map_settings.entity_name_property in properties:
name_prop = str(properties[map_settings.entity_name_property])
if not name_prop.is_empty():
node_name = "entity_" + name_prop
if entity_def is FuncGodotFGDSolidClass:
node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
elif entity_def is FuncGodotFGDPointClass:
node = generate_point_entity_node(node, node_name, properties, entity_def)
if node:
if entity_def.script_class:
node.set_script(entity_def.script_class)
else:
push_error("No entity definition found for \"" + node_name + "\"")
if entity_data.brushes.size():
node = generate_solid_entity_node(node, node_name, entity_data, default_solid_def)
else:
node = generate_point_entity_node(node, node_name, properties, default_point_def)
var node_groups: Array[String] = map_settings.entity_node_groups.duplicate()
node_groups.append_array(entity_def.node_groups)
for node_group in node_groups:
if node_group.is_empty():
continue
node.add_to_group(node_group, true)
return node
## Main entity assembly process called by [FuncGodotMap]. Generates and sorts group nodes in the [SceneTree] first,
## then generates and assembles [Node]s based upon the provided [FuncGodotData.EntityData] and adds them to the [SceneTree].
func build(map_node: FuncGodotMap, entities: Array[_EntityData], groups: Array[_GroupData]) -> void:
var scene_root := map_node.get_tree().edited_scene_root if map_node.get_tree() else map_node
var scene_root := map_node.get_tree().edited_scene_root if map_node.is_inside_tree() else map_node
build_flags = map_node.build_flags
if map_settings.use_groups_hierarchy:

View file

@ -1,136 +0,0 @@
class_name FuncGodot extends RefCounted
var map_data:= FuncGodotMapData.new()
var map_parser:= FuncGodotMapParser.new(map_data)
var geo_generator = preload("res://addons/func_godot/src/core/func_godot_geo_generator.gd").new(map_data)
var map_settings: FuncGodotMapSettings = null:
set(new):
if not new or new == map_settings: return
surface_gatherer.map_settings = new
map_settings = new
var surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data, map_settings)
func load_map(filename: String, keep_tb_groups: bool) -> void:
map_parser.load_map(filename, keep_tb_groups)
func get_texture_list() -> PackedStringArray:
var g_textures: PackedStringArray
var tex_count: int = map_data.textures.size()
g_textures.resize(tex_count)
for i in range(tex_count):
g_textures.set(i, map_data.textures[i].name)
return g_textures
func set_entity_definitions(entity_defs: Dictionary) -> void:
for i in range(entity_defs.size()):
var classname: String = entity_defs.keys()[i]
var spawn_type: int = entity_defs.values()[i].get("spawn_type", FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY)
var origin_type: int = entity_defs.values()[i].get("origin_type", FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER)
var metadata_inclusion_flags: int = entity_defs.values()[i].get("metadata_inclusion_flags", FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.NONE)
map_data.set_entity_types_by_classname(classname, spawn_type, origin_type, metadata_inclusion_flags)
func get_texture_info(texture_name: String) -> FuncGodotMapData.FuncGodotTextureType:
if texture_name == map_settings.origin_texture:
return FuncGodotMapData.FuncGodotTextureType.ORIGIN
return FuncGodotMapData.FuncGodotTextureType.NORMAL
func generate_geometry(texture_dict: Dictionary) -> void:
var keys: Array = texture_dict.keys()
for key in keys:
var val: Vector2 = texture_dict[key]
map_data.set_texture_info(key, val.x, val.y, get_texture_info(key))
geo_generator.run()
func get_entity_dicts() -> Array:
var ent_dicts: Array
for entity in map_data.entities:
var dict: Dictionary
dict["brush_count"] = entity.brushes.size()
# TODO: This is a horrible remnant of the worldspawn layer system, remove it.
var brush_indices: PackedInt64Array
brush_indices.resize(entity.brushes.size())
for b in range(entity.brushes.size()):
brush_indices[b] = b
dict["brush_indices"] = brush_indices
dict["center"] = Vector3(entity.center.y, entity.center.z, entity.center.x)
dict["properties"] = entity.properties
ent_dicts.append(dict)
return ent_dicts
func gather_texture_surfaces(texture_name: String) -> Dictionary:
var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data, map_settings)
sg.reset_params()
sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
sg.metadata_skip_flags = MFlags.TEXTURES | MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
sg.set_texture_filter(texture_name)
sg.set_clip_filter_texture(map_settings.clip_texture)
sg.set_skip_filter_texture(map_settings.skip_texture)
sg.set_origin_filter_texture(map_settings.origin_texture)
sg.run()
return {
surfaces = fetch_surfaces(sg),
metadata = sg.out_metadata,
}
func gather_entity_convex_collision_surfaces(entity_idx: int) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.BRUSH
surface_gatherer.entity_filter_idx = entity_idx
surface_gatherer.set_origin_filter_texture(map_settings.origin_texture)
surface_gatherer.run()
func gather_entity_concave_collision_surfaces(entity_idx: int) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE
surface_gatherer.entity_filter_idx = entity_idx
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
surface_gatherer.metadata_skip_flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
surface_gatherer.set_skip_filter_texture(map_settings.skip_texture)
surface_gatherer.set_origin_filter_texture(map_settings.origin_texture)
surface_gatherer.run()
func fetch_surfaces(sg: FuncGodotSurfaceGatherer) -> Array:
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = sg.out_surfaces
var surf_array: Array
for surf in surfs:
if surf == null or surf.vertices.size() == 0:
surf_array.append(null)
continue
var vertices: PackedVector3Array
var normals: PackedVector3Array
var tangents: PackedFloat64Array
var uvs: PackedVector2Array
for v in surf.vertices:
vertices.append(Vector3(v.vertex.y, v.vertex.z, v.vertex.x) * map_settings.scale_factor)
normals.append(Vector3(v.normal.y, v.normal.z, v.normal.x))
tangents.append(v.tangent.y)
tangents.append(v.tangent.z)
tangents.append(v.tangent.x)
tangents.append(v.tangent.w)
uvs.append(Vector2(v.uv.x, v.uv.y))
var indices: PackedInt32Array
if surf.indicies.size() > 0:
indices.append_array(surf.indicies)
var brush_array: Array
brush_array.resize(Mesh.ARRAY_MAX)
brush_array[Mesh.ARRAY_VERTEX] = vertices
brush_array[Mesh.ARRAY_NORMAL] = normals
brush_array[Mesh.ARRAY_TANGENT] = tangents
brush_array[Mesh.ARRAY_TEX_UV] = uvs
brush_array[Mesh.ARRAY_INDEX] = indices
surf_array.append(brush_array)
return surf_array

View file

@ -1 +0,0 @@
uid://bvstd30rkrap

View file

@ -1,381 +0,0 @@
extends RefCounted
# Min distance between two verts in a brush before they're merged. Higher values fix angled brushes near extents.
const CMP_EPSILON:= 0.008
const UP_VECTOR:= Vector3(0.0, 0.0, 1.0)
const RIGHT_VECTOR:= Vector3(0.0, 1.0, 0.0)
const FORWARD_VECTOR:= Vector3(1.0, 0.0, 0.0)
var map_data: FuncGodotMapData
var wind_entity_idx: int = 0
var wind_brush_idx: int = 0
var wind_face_idx: int = 0
var wind_face_center: Vector3
var wind_face_basis: Vector3
var wind_face_normal: Vector3
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func sort_vertices_by_winding(a: FuncGodotMapData.FuncGodotFaceVertex, b: FuncGodotMapData.FuncGodotFaceVertex) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var u: Vector3 = wind_face_basis.normalized()
var v: Vector3 = u.cross(wind_face_normal).normalized()
var loc_a: Vector3 = a.vertex - wind_face_center
var a_pu: float = loc_a.dot(u)
var a_pv: float = loc_a.dot(v)
var loc_b: Vector3 = b.vertex - wind_face_center
var b_pu: float = loc_b.dot(u)
var b_pv: float = loc_b.dot(v)
var a_angle: float = atan2(a_pv, a_pu)
var b_angle: float = atan2(b_pv, b_pu)
return a_angle < b_angle
# returns null if no intersection, else intersection vertex.
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace) -> Variant:
var n0:= f0.plane_normal
var n1:= f1.plane_normal
var n2:= f2.plane_normal
var denom: float = n0.cross(n1).dot(n2)
if denom > 0.0:
return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom
return null
func vertex_in_hull(faces: Array[FuncGodotMapData.FuncGodotFace], vertex: Vector3) -> bool:
for face in faces:
var proj: float = face.plane_normal.dot(vertex)
if proj > face.plane_dist and absf(face.plane_dist - proj) > CMP_EPSILON:
return false
return true
func get_standard_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var du:= absf(face.plane_normal.dot(UP_VECTOR))
var dr:= absf(face.plane_normal.dot(RIGHT_VECTOR))
var df:= absf(face.plane_normal.dot(FORWARD_VECTOR))
if du >= dr and du >= df:
uv_out = Vector2(vertex.x, -vertex.y)
elif dr >= du and dr >= df:
uv_out = Vector2(vertex.x, -vertex.z)
elif df >= du and df >= dr:
uv_out = Vector2(vertex.y, -vertex.z)
var angle: float = deg_to_rad(face.uv_extra.rot)
uv_out = Vector2(
uv_out.x * cos(angle) - uv_out.y * sin(angle),
uv_out.x * sin(angle) + uv_out.y * cos(angle))
uv_out.x /= texture_width
uv_out.y /= texture_height
uv_out.x /= face.uv_extra.scale_x
uv_out.y /= face.uv_extra.scale_y
uv_out.x += face.uv_standard.x / texture_width
uv_out.y += face.uv_standard.y / texture_height
return uv_out
func get_valve_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var u_axis:= face.uv_valve.u.axis
var v_axis:= face.uv_valve.v.axis
var u_shift:= face.uv_valve.u.offset
var v_shift:= face.uv_valve.v.offset
uv_out.x = u_axis.dot(vertex);
uv_out.y = v_axis.dot(vertex);
uv_out.x /= texture_width;
uv_out.y /= texture_height;
uv_out.x /= face.uv_extra.scale_x;
uv_out.y /= face.uv_extra.scale_y;
uv_out.x += u_shift / texture_width;
uv_out.y += v_shift / texture_height;
return uv_out
func get_standard_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var du:= face.plane_normal.dot(UP_VECTOR)
var dr:= face.plane_normal.dot(RIGHT_VECTOR)
var df:= face.plane_normal.dot(FORWARD_VECTOR)
var dua:= absf(du)
var dra:= absf(dr)
var dfa:= absf(df)
var u_axis: Vector3
var v_sign: float = 0.0
if dua >= dra and dua >= dfa:
u_axis = FORWARD_VECTOR
v_sign = signf(du)
elif dra >= dua and dra >= dfa:
u_axis = FORWARD_VECTOR
v_sign = -signf(dr)
elif dfa >= dua and dfa >= dra:
u_axis = RIGHT_VECTOR
v_sign = signf(df)
v_sign *= signf(face.uv_extra.scale_y);
u_axis = u_axis.rotated(face.plane_normal, deg_to_rad(-face.uv_extra.rot) * v_sign)
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func get_valve_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var u_axis:= face.uv_valve.u.axis.normalized()
var v_axis:= face.uv_valve.v.axis.normalized()
var v_sign = -signf(face.plane_normal.cross(u_axis).dot(v_axis))
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[entity_idx]
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[brush_idx]
var face_count: int = brush.faces.size()
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[entity_idx]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[brush_idx]
var phong: bool = entity.properties.get("_phong", "0") == "1"
var phong_angle_str: String = entity.properties.get("_phong_angle", "89")
var phong_angle: float = float(phong_angle_str) if phong_angle_str.is_valid_float() else 89.0
for f0 in range(face_count):
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f0]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f0]
var texture: FuncGodotMapData.FuncGodotTextureData = map_data.textures[face.texture_idx]
for f1 in range(face_count):
for f2 in range(face_count):
var vertex = intersect_face(brush.faces[f0], brush.faces[f1], brush.faces[f2])
if not vertex is Vector3:
continue
if not vertex_in_hull(brush.faces, vertex):
continue
var merged: bool = false
for f3 in range(f0):
var other_face_geo : FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f3]
for i in range(len(other_face_geo.vertices)):
if other_face_geo.vertices[i].vertex.distance_to(vertex) < CMP_EPSILON:
vertex = other_face_geo.vertices[i].vertex
merged = true;
break
if merged:
break
var normal: Vector3 = face.plane_normal
if phong:
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
if face.plane_normal.dot(brush.faces[f1].plane_normal) > threshold:
normal += brush.faces[f1].plane_normal
if face.plane_normal.dot(brush.faces[f2].plane_normal) > threshold:
normal += brush.faces[f2].plane_normal
normal = normal.normalized()
var uv: Vector2
var tangent: Vector4
if face.is_valve_uv:
uv = get_valve_uv(vertex, face, texture.width, texture.height)
tangent = get_valve_tangent(face)
else:
uv = get_standard_uv(vertex, face, texture.width, texture.height)
tangent = get_standard_tangent(face)
# Check for a duplicate vertex in the current face.
var duplicate_idx: int = -1
for i in range(face_geo.vertices.size()):
if face_geo.vertices[i].vertex == vertex:
duplicate_idx = i
break
if duplicate_idx < 0:
var new_face_vert:= FuncGodotMapData.FuncGodotFaceVertex.new()
new_face_vert.vertex = vertex
new_face_vert.normal = normal
new_face_vert.tangent = tangent
new_face_vert.uv = uv
face_geo.vertices.append(new_face_vert)
elif phong:
face_geo.vertices[duplicate_idx].normal += normal
# maybe optimisable?
for face_geo in brush_geo.faces:
for i in range(face_geo.vertices.size()):
face_geo.vertices[i].normal = face_geo.vertices[i].normal.normalized()
func run() -> void:
map_data.entity_geo.resize(map_data.entities.size())
for i in range(map_data.entity_geo.size()):
map_data.entity_geo[i] = FuncGodotMapData.FuncGodotEntityGeometry.new()
for e in range(map_data.entities.size()):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
entity_geo.brushes.resize(entity.brushes.size())
for i in range(entity_geo.brushes.size()):
entity_geo.brushes[i] = FuncGodotMapData.FuncGodotBrushGeometry.new()
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
brush_geo.faces.resize(brush.faces.size())
for i in range(brush_geo.faces.size()):
brush_geo.faces[i] = FuncGodotMapData.FuncGodotFaceGeometry.new()
var generate_vertices_task = func(e):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
var entity_mins: Vector3 = Vector3.INF
var entity_maxs: Vector3 = Vector3.INF
var origin_mins: Vector3 = Vector3.INF
var origin_maxs: Vector3 = -Vector3.INF
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
brush.center = Vector3.ZERO
var vert_count: int = 0
# Check if this is a special brush (eg: origin)
var brush_texture_type: FuncGodotMapData.FuncGodotTextureType = FuncGodotMapData.FuncGodotTextureType.NORMAL
if brush.faces.size() > 0:
brush_texture_type = map_data.textures[brush.faces[0].texture_idx].type
# Check that all the faces match the same type
for face_idx in range(1,brush.faces.size()):
if map_data.textures[brush.faces[face_idx].texture_idx].type != brush_texture_type:
brush_texture_type = FuncGodotMapData.FuncGodotTextureType.NORMAL # Reset face type if it doesn't match
break
generate_brush_vertices(e, b)
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = map_data.entity_geo[e].brushes[b]
for face in brush_geo.faces:
for vert in face.vertices:
if entity_mins != Vector3.INF:
entity_mins = entity_mins.min(vert.vertex)
else:
entity_mins = vert.vertex
if entity_maxs != Vector3.INF:
entity_maxs = entity_maxs.max(vert.vertex)
else:
entity_maxs = vert.vertex
if brush_texture_type == FuncGodotMapData.FuncGodotTextureType.ORIGIN:
if origin_mins != Vector3.INF:
origin_mins = origin_mins.min(vert.vertex)
else:
origin_mins = vert.vertex
if origin_maxs != Vector3.INF:
origin_maxs = origin_maxs.max(vert.vertex)
else:
origin_maxs = vert.vertex
brush.center += vert.vertex
vert_count += 1
if vert_count > 0:
brush.center /= float(vert_count)
# Default origin type is BOUNDS_CENTER
if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
entity.center = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
match entity.origin_type:
FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE, FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE:
if 'origin' in entity.properties:
var origin_comps: PackedFloat64Array = entity.properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
if entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE:
entity.center = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
else: # OriginType.RELATIVE
entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
FuncGodotMapData.FuncGodotEntityOriginType.BRUSH:
if origin_mins != Vector3.INF:
entity.center = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MINS:
entity.center = entity_mins
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MAXS:
entity.center = entity_maxs
FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED:
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
entity.center += entity.brushes[b].center
entity.center /= float(entity.brushes.size())
var generate_vertices_task_id:= WorkerThreadPool.add_group_task(generate_vertices_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(generate_vertices_task_id)
# wind face vertices
for e in range(map_data.entities.size()):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush.faces.size()):
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
wind_entity_idx = e
wind_brush_idx = b
wind_face_idx = f
wind_face_basis = face_geo.vertices[1].vertex - face_geo.vertices[0].vertex
wind_face_center = Vector3.ZERO
wind_face_normal = face.plane_normal
for v in face_geo.vertices:
wind_face_center += v.vertex
wind_face_center /= face_geo.vertices.size()
face_geo.vertices.sort_custom(sort_vertices_by_winding)
wind_entity_idx = 0
# index face vertices
var index_faces_task:= func(e):
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity_geo.brushes.size()):
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush_geo.faces.size()):
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
var i_count: int = 0
face_geo.indicies.resize((face_geo.vertices.size() - 2) * 3)
for i in range(face_geo.vertices.size() - 2):
face_geo.indicies[i_count] = 0
face_geo.indicies[i_count + 1] = i + 1
face_geo.indicies[i_count + 2] = i + 2
i_count += 3
var index_faces_task_id:= WorkerThreadPool.add_group_task(index_faces_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(index_faces_task_id)

View file

@ -1 +0,0 @@
uid://cb0c2fn35hqov

View file

@ -1,158 +0,0 @@
class_name FuncGodotMapData extends RefCounted
var entities: Array[FuncGodotMapData.FuncGodotEntity]
var entity_geo: Array[FuncGodotMapData.FuncGodotEntityGeometry]
var textures: Array[FuncGodotMapData.FuncGodotTextureData]
func register_texture(name: String) -> int:
for i in range(textures.size()):
if textures[i].name == name:
return i
textures.append(FuncGodotTextureData.new(name))
return textures.size() - 1
func set_texture_info(name: String, width: int, height: int, type: FuncGodotTextureType) -> void:
for i in range(textures.size()):
if textures[i].name == name:
textures[i].width = width
textures[i].height = height
textures[i].type = type
return
func find_texture(texture_name: String) -> int:
for i in range(textures.size()):
if textures[i].name == texture_name:
return i
return -1
func set_entity_types_by_classname(classname: String, spawn_type: int, origin_type: int, meta_flags: int) -> void:
for entity in entities:
if entity.properties.has("classname") and entity.properties["classname"] == classname:
entity.metadata_inclusion_flags = meta_flags as FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
entity.spawn_type = spawn_type as FuncGodotMapData.FuncGodotEntitySpawnType
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
entity.origin_type = origin_type as FuncGodotMapData.FuncGodotEntityOriginType
else:
entity.origin_type = FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED
func clear() -> void:
entities.clear()
entity_geo.clear()
textures.clear()
# --------------------------------------------------------------------------------------------------
# Nested Types
# --------------------------------------------------------------------------------------------------
enum FuncGodotEntitySpawnType {
WORLDSPAWN = 0,
MERGE_WORLDSPAWN = 1,
ENTITY = 2
}
enum FuncGodotEntityOriginType {
AVERAGED = 0,
ABSOLUTE = 1,
RELATIVE = 2,
BRUSH = 3,
BOUNDS_CENTER = 4,
BOUNDS_MINS = 5,
BOUNDS_MAXS = 6,
}
enum FuncGodotEntityMetadataInclusionFlags {
NONE = 0,
ENTITY_INDEX_RANGES = 1,
TEXTURES = 2,
VERTEX = 4,
FACE_POSITION = 8,
FACE_NORMAL = 16,
COLLISION_SHAPE_TO_FACE_RANGE_MAP = 32,
}
enum FuncGodotTextureType {
NORMAL = 0,
ORIGIN = 1
}
class FuncGodotFacePoints:
var v0: Vector3
var v1: Vector3
var v2: Vector3
class FuncGodotValveTextureAxis:
var axis: Vector3
var offset: float
class FuncGodotValveUV:
var u: FuncGodotValveTextureAxis
var v: FuncGodotValveTextureAxis
func _init() -> void:
u = FuncGodotValveTextureAxis.new()
v = FuncGodotValveTextureAxis.new()
class FuncGodotFaceUVExtra:
var rot: float
var scale_x: float
var scale_y: float
class FuncGodotFace:
var plane_points: FuncGodotFacePoints
var plane_normal: Vector3
var plane_dist: float
var texture_idx: int
var is_valve_uv: bool
var uv_standard: Vector2
var uv_valve: FuncGodotValveUV
var uv_extra: FuncGodotFaceUVExtra
func _init() -> void:
plane_points = FuncGodotFacePoints.new()
uv_valve = FuncGodotValveUV.new()
uv_extra = FuncGodotFaceUVExtra.new()
class FuncGodotBrush:
var faces: Array[FuncGodotFace]
var center: Vector3
class FuncGodotEntity:
var properties: Dictionary
var brushes: Array[FuncGodotBrush]
var center: Vector3
var spawn_type: FuncGodotEntitySpawnType
var origin_type: FuncGodotEntityOriginType
var metadata_inclusion_flags: FuncGodotEntityMetadataInclusionFlags
class FuncGodotFaceVertex:
var vertex: Vector3
var normal: Vector3
var uv: Vector2
var tangent: Vector4
func duplicate() -> FuncGodotFaceVertex:
var new_vert := FuncGodotFaceVertex.new()
new_vert.vertex = vertex
new_vert.normal = normal
new_vert.uv = uv
new_vert.tangent = tangent
return new_vert
class FuncGodotFaceGeometry:
var vertices: Array[FuncGodotFaceVertex]
var indicies: Array[int]
class FuncGodotBrushGeometry:
var faces: Array[FuncGodotFaceGeometry]
class FuncGodotEntityGeometry:
var brushes: Array[FuncGodotBrushGeometry]
class FuncGodotTextureData:
var name: String
var width: int
var height: int
var type: FuncGodotTextureType
func _init(in_name: String):
name = in_name

View file

@ -1 +0,0 @@
uid://ct3rx5npjd00s

View file

@ -1,326 +0,0 @@
class_name FuncGodotMapParser extends RefCounted
var scope:= FuncGodotMapParser.ParseScope.FILE
var comment: bool = false
var entity_idx: int = -1
var brush_idx: int = -1
var face_idx: int = -1
var component_idx: int = 0
var prop_key: String = ""
var current_property: String = ""
var valve_uvs: bool = false
var current_face: FuncGodotMapData.FuncGodotFace
var current_brush: FuncGodotMapData.FuncGodotBrush
var current_entity: FuncGodotMapData.FuncGodotEntity
var map_data: FuncGodotMapData
var _keep_tb_groups: bool = false
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func load_map(map_file: String, keep_tb_groups: bool) -> bool:
current_face = FuncGodotMapData.FuncGodotFace.new()
current_brush = FuncGodotMapData.FuncGodotBrush.new()
current_entity = FuncGodotMapData.FuncGodotEntity.new()
scope = FuncGodotMapParser.ParseScope.FILE
comment = false
entity_idx = -1
brush_idx = -1
face_idx = -1
component_idx = 0
valve_uvs = false
_keep_tb_groups = keep_tb_groups
var lines: PackedStringArray = []
var map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
if map == null:
printerr("Error: Failed to open map file (" + map_file + ")")
return false
if map_file.ends_with(".import"):
while not map.eof_reached():
var line: String = map.get_line()
if line.begins_with("path"):
map.close()
line = line.replace("path=", "");
line = line.replace('"', '')
var map_data: String = (load(line) as QuakeMapFile).map_data
if map_data.is_empty():
printerr("Error: Failed to open map file (" + line + ")")
return false
lines = map_data.split("\n")
break
else:
while not map.eof_reached():
var line: String = map.get_line()
lines.append(line)
for line in lines:
if comment:
comment = false
var tokens := split_string(line, [" ", "\t"], true)
for s in tokens:
token(s)
return true
func split_string(s: String, delimeters: Array[String], allow_empty: bool = true) -> Array[String]:
var parts: Array[String] = []
var start := 0
var i := 0
while i < s.length():
if s[i] in delimeters:
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
start = i + 1
i += 1
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
return parts
func set_scope(new_scope: FuncGodotMapParser.ParseScope) -> void:
"""
match new_scope:
ParseScope.FILE:
print("Switching to file scope.")
ParseScope.ENTITY:
print("Switching to entity " + str(entity_idx) + "scope")
ParseScope.PROPERTY_VALUE:
print("Switching to property value scope")
ParseScope.BRUSH:
print("Switching to brush " + str(brush_idx) + " scope")
ParseScope.PLANE_0:
print("Switching to face " + str(face_idx) + " plane 0 scope")
ParseScope.PLANE_1:
print("Switching to face " + str(face_idx) + " plane 1 scope")
ParseScope.PLANE_2:
print("Switching to face " + str(face_idx) + " plane 2 scope")
ParseScope.TEXTURE:
print("Switching to texture scope")
ParseScope.U:
print("Switching to U scope")
ParseScope.V:
print("Switching to V scope")
ParseScope.VALVE_U:
print("Switching to Valve U scope")
ParseScope.VALVE_V:
print("Switching to Valve V scope")
ParseScope.ROT:
print("Switching to rotation scope")
ParseScope.U_SCALE:
print("Switching to U scale scope")
ParseScope.V_SCALE:
print("Switching to V scale scope")
"""
scope = new_scope
func token(buf_str: String) -> void:
if comment:
return
elif buf_str == "//":
comment = true
return
match scope:
FuncGodotMapParser.ParseScope.FILE:
if buf_str == "{":
entity_idx += 1
brush_idx = -1
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.ENTITY:
if buf_str.begins_with('"'):
prop_key = buf_str.substr(1)
if prop_key.ends_with('"'):
prop_key = prop_key.left(-1)
set_scope(FuncGodotMapParser.ParseScope.PROPERTY_VALUE)
elif buf_str == "{":
brush_idx += 1
face_idx = -1
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
elif buf_str == "}":
commit_entity()
set_scope(FuncGodotMapParser.ParseScope.FILE)
FuncGodotMapParser.ParseScope.PROPERTY_VALUE:
var is_first = buf_str[0] == '"'
var is_last = buf_str.right(1) == '"'
if is_first:
if current_property != "":
current_property = ""
if not is_last:
current_property += buf_str + " "
else:
current_property += buf_str
if is_last:
current_entity.properties[prop_key] = current_property.substr(1, len(current_property) - 2)
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.BRUSH:
if buf_str == "(":
face_idx += 1
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_0)
elif buf_str == "}":
commit_brush()
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.PLANE_0:
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_1)
else:
match component_idx:
0:
current_face.plane_points.v0.x = float(buf_str)
1:
current_face.plane_points.v0.y = float(buf_str)
2:
current_face.plane_points.v0.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_1:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_2)
else:
match component_idx:
0:
current_face.plane_points.v1.x = float(buf_str)
1:
current_face.plane_points.v1.y = float(buf_str)
2:
current_face.plane_points.v1.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_2:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.TEXTURE)
else:
match component_idx:
0:
current_face.plane_points.v2.x = float(buf_str)
1:
current_face.plane_points.v2.y = float(buf_str)
2:
current_face.plane_points.v2.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.TEXTURE:
current_face.texture_idx = map_data.register_texture(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U)
FuncGodotMapParser.ParseScope.U:
if buf_str == "[":
valve_uvs = true
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_U)
else:
valve_uvs = false
current_face.uv_standard.x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V)
FuncGodotMapParser.ParseScope.V:
current_face.uv_standard.y = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.ROT)
FuncGodotMapParser.ParseScope.VALVE_U:
if buf_str == "]":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_V)
else:
match component_idx:
0:
current_face.uv_valve.u.axis.x = float(buf_str)
1:
current_face.uv_valve.u.axis.y = float(buf_str)
2:
current_face.uv_valve.u.axis.z = float(buf_str)
3:
current_face.uv_valve.u.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.VALVE_V:
if buf_str != "[":
if buf_str == "]":
set_scope(FuncGodotMapParser.ParseScope.ROT)
else:
match component_idx:
0:
current_face.uv_valve.v.axis.x = float(buf_str)
1:
current_face.uv_valve.v.axis.y = float(buf_str)
2:
current_face.uv_valve.v.axis.z = float(buf_str)
3:
current_face.uv_valve.v.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.ROT:
current_face.uv_extra.rot = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U_SCALE)
FuncGodotMapParser.ParseScope.U_SCALE:
current_face.uv_extra.scale_x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V_SCALE)
FuncGodotMapParser.ParseScope.V_SCALE:
current_face.uv_extra.scale_y = float(buf_str)
commit_face()
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
func commit_entity() -> void:
if current_entity.properties.has('_tb_type') and map_data.entities.size() > 0:
map_data.entities[0].brushes.append_array(current_entity.brushes)
current_entity.brushes.clear()
if !_keep_tb_groups:
current_entity = FuncGodotMapData.FuncGodotEntity.new()
return
var new_entity:= FuncGodotMapData.FuncGodotEntity.new()
new_entity.spawn_type = FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY
new_entity.properties = current_entity.properties
new_entity.brushes = current_entity.brushes
map_data.entities.append(new_entity)
current_entity = FuncGodotMapData.FuncGodotEntity.new()
func commit_brush() -> void:
current_entity.brushes.append(current_brush)
current_brush = FuncGodotMapData.FuncGodotBrush.new()
func commit_face() -> void:
var v0v1: Vector3 = current_face.plane_points.v1 - current_face.plane_points.v0
var v1v2: Vector3 = current_face.plane_points.v2 - current_face.plane_points.v1
current_face.plane_normal = v1v2.cross(v0v1).normalized()
current_face.plane_dist = current_face.plane_normal.dot(current_face.plane_points.v0)
current_face.is_valve_uv = valve_uvs
current_brush.faces.append(current_face)
current_face = FuncGodotMapData.FuncGodotFace.new()
# Nested
enum ParseScope{
FILE,
COMMENT,
ENTITY,
PROPERTY_VALUE,
BRUSH,
PLANE_0,
PLANE_1,
PLANE_2,
TEXTURE,
U,
V,
VALVE_U,
VALVE_V,
ROT,
U_SCALE,
V_SCALE
}

View file

@ -1 +0,0 @@
uid://cg2iiom3svtw0

View file

@ -1,217 +0,0 @@
class_name FuncGodotSurfaceGatherer extends RefCounted
var map_data: FuncGodotMapData
var map_settings: FuncGodotMapSettings
var split_type: SurfaceSplitType = SurfaceSplitType.NONE
var entity_filter_idx: int = -1
var texture_filter_idx: int = -1
var clip_filter_texture_idx: int
var skip_filter_texture_idx: int
var origin_filter_texture_idx: int
var metadata_skip_flags: int
var out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry]
var out_metadata: Dictionary
func _init(in_map_data: FuncGodotMapData, in_map_settings: FuncGodotMapSettings) -> void:
map_data = in_map_data
map_settings = in_map_settings
func set_texture_filter(texture_name: String) -> void:
texture_filter_idx = map_data.find_texture(texture_name)
func set_clip_filter_texture(texture_name: String) -> void:
clip_filter_texture_idx = map_data.find_texture(texture_name)
func set_skip_filter_texture(texture_name: String) -> void:
skip_filter_texture_idx = map_data.find_texture(texture_name)
func set_origin_filter_texture(texture_name: String) -> void:
origin_filter_texture_idx = map_data.find_texture(texture_name)
func filter_entity(entity_idx: int) -> bool:
if entity_filter_idx != -1 and entity_idx != entity_filter_idx:
return true
return false
func filter_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx]
if face_geo.vertices.size() < 3:
return true
# Omit faces textured with Clip
if clip_filter_texture_idx != -1 and face.texture_idx == clip_filter_texture_idx:
return true
# Omit faces textured with Skip
if skip_filter_texture_idx != -1 and face.texture_idx == skip_filter_texture_idx:
return true
# Omit faces textured with Origin
if origin_filter_texture_idx != -1 and face.texture_idx == origin_filter_texture_idx:
return true
# Omit filtered texture indices
if texture_filter_idx != -1 and face.texture_idx != texture_filter_idx:
return true
return false
func run() -> void:
out_surfaces.clear()
var texture_names: Array[StringName] = []
var textures: PackedInt32Array = []
var vertices: PackedVector3Array = []
var positions: PackedVector3Array = []
var normals: PackedVector3Array = []
var shape_index_ranges: Array[Vector2i] = []
var entity_index_ranges: Array[Vector2i] = []
var index_offset: int = 0
var entity_face_range: Vector2i = Vector2i.ZERO
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
var build_entity_index_ranges: bool = not metadata_skip_flags & MFlags.ENTITY_INDEX_RANGES
var surf: FuncGodotMapData.FuncGodotFaceGeometry
if split_type == SurfaceSplitType.NONE:
surf = add_surface()
index_offset = len(out_surfaces) - 1
for e in range(map_data.entities.size()):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
var shape_face_range := Vector2i.ZERO
var total_entity_tris := 0
var include_normals_metadata: bool = not metadata_skip_flags & MFlags.FACE_NORMAL and entity.metadata_inclusion_flags & MFlags.FACE_NORMAL
var include_vertices_metadata: bool = not metadata_skip_flags & MFlags.VERTEX and entity.metadata_inclusion_flags & MFlags.VERTEX
var include_textures_metadata: bool = not metadata_skip_flags & MFlags.TEXTURES and entity.metadata_inclusion_flags & MFlags.TEXTURES
var include_positions_metadata: bool = not metadata_skip_flags & MFlags.FACE_POSITION and entity.metadata_inclusion_flags & MFlags.FACE_POSITION
var include_shape_range_metadata: bool = not metadata_skip_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP and entity.metadata_inclusion_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
if filter_entity(e):
continue
if split_type == SurfaceSplitType.ENTITY:
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.MERGE_WORLDSPAWN:
add_surface()
surf = out_surfaces[0]
index_offset = surf.vertices.size()
else:
surf = add_surface()
index_offset = surf.vertices.size()
for b in range(entity.brushes.size()):
var brush:= entity.brushes[b]
var brush_geo:= entity_geo.brushes[b]
var total_brush_tris:= 0
if split_type == SurfaceSplitType.BRUSH:
index_offset = 0
surf = add_surface()
for f in range(brush.faces.size()):
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var num_tris = face_geo.vertices.size() - 2
if filter_face(e, b, f):
continue
for v in range(face_geo.vertices.size()):
var vert: FuncGodotMapData.FuncGodotFaceVertex = face_geo.vertices[v].duplicate()
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
vert.vertex -= entity.center
surf.vertices.append(vert)
if include_normals_metadata:
var normal := Vector3(face.plane_normal.y, face.plane_normal.z, face.plane_normal.x)
for i in num_tris:
normals.append(normal)
if include_shape_range_metadata or build_entity_index_ranges:
total_brush_tris += num_tris
if include_textures_metadata:
var texname := StringName(map_data.textures[face.texture_idx].name)
var index: int
if texture_names.is_empty():
texture_names.append(texname)
index = 0
elif texture_names.back() == texname:
# Common case, faces with textures are next to each other
index = texture_names.size() - 1
else:
var texture_name_index: int = texture_names.find(texname)
if texture_name_index == -1:
index = texture_names.size()
texture_names.append(texname)
else:
index = texture_name_index
# Metadata addresses triangles, so we have to duplicate the info for each tri
for i in num_tris:
textures.append(index)
var avg_vertex_pos := Vector3.ZERO
var avg_vertex_pos_ct: int = 0
for i in range(num_tris * 3):
surf.indicies.append(face_geo.indicies[i] + index_offset)
var vertex: Vector3 = surf.vertices[surf.indicies.back()].vertex
vertex = Vector3(vertex.y, vertex.z, vertex.x) * map_settings.scale_factor
if include_vertices_metadata:
vertices.append(vertex)
if include_positions_metadata:
avg_vertex_pos_ct += 1
avg_vertex_pos += vertex
if avg_vertex_pos_ct == 3:
avg_vertex_pos /= 3
positions.append(avg_vertex_pos)
avg_vertex_pos = Vector3.ZERO
avg_vertex_pos_ct = 0
index_offset += face_geo.vertices.size()
if include_shape_range_metadata:
shape_face_range.x = shape_face_range.y
shape_face_range.y = shape_face_range.x + total_brush_tris
shape_index_ranges.append(shape_face_range)
if build_entity_index_ranges:
total_entity_tris += total_brush_tris
if build_entity_index_ranges:
entity_face_range.x = entity_face_range.y
entity_face_range.y = entity_face_range.x + total_entity_tris
entity_index_ranges.append(entity_face_range)
out_metadata = {
textures = textures,
texture_names = texture_names,
normals = normals,
vertices = vertices,
positions = positions,
shape_index_ranges = shape_index_ranges,
}
if build_entity_index_ranges:
out_metadata["entity_index_ranges"] = entity_index_ranges
func add_surface() -> FuncGodotMapData.FuncGodotFaceGeometry:
var surf:= FuncGodotMapData.FuncGodotFaceGeometry.new()
out_surfaces.append(surf)
return surf
func reset_params() -> void:
split_type = SurfaceSplitType.NONE
entity_filter_idx = -1
texture_filter_idx = -1
clip_filter_texture_idx = -1
skip_filter_texture_idx = -1
metadata_skip_flags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.ENTITY_INDEX_RANGES
# nested
enum SurfaceSplitType{
NONE,
ENTITY,
BRUSH
}

View file

@ -1 +0,0 @@
uid://df8y3hiimomt5

View file

@ -8,8 +8,6 @@ const _SIGNATURE: String = "[GEO]"
const _VERTEX_EPSILON := FuncGodotUtil._VERTEX_EPSILON
const _VERTEX_EPSILON2 := _VERTEX_EPSILON * _VERTEX_EPSILON
const _HYPERPLANE_SIZE := 65355.0
const _OriginType := FuncGodotFGDSolidClass.OriginType
const _GroupData := FuncGodotData.GroupData
@ -21,6 +19,7 @@ const _VertexGroupData := FuncGodotData.VertexGroupData
# Class members
var map_settings: FuncGodotMapSettings = null
var hyperplane_size: float = 512.0
var entity_data: Array[_EntityData]
var texture_materials: Dictionary[String, Material]
var texture_sizes: Dictionary[String, Vector2]
@ -30,8 +29,9 @@ var texture_sizes: Dictionary[String, Vector2]
## Emitted when beginning a new step of the generation process.
signal declare_step(step: String)
func _init(settings: FuncGodotMapSettings = null) -> void:
func _init(settings: FuncGodotMapSettings = null, hplane_size: float = 512.0) -> void:
map_settings = settings
hyperplane_size = hplane_size
#region TOOLS
func is_skip(face: _FaceData) -> bool:
@ -99,10 +99,11 @@ func generate_base_winding(plane: Plane) -> PackedVector3Array:
# construct oversized square on the plane to clip against
var winding := PackedVector3Array()
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
var h: float = hyperplane_size
winding.append(centroid + (right * h) + (forward * h))
winding.append(centroid + (right * -h) + (forward * h))
winding.append(centroid + (right * -h) + (forward * -h))
winding.append(centroid + (right * h) + (forward * -h))
return winding
func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_distance: float = 0.0) -> PackedVector3Array:
@ -121,9 +122,17 @@ func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_dis
if winding.is_empty():
break
# Reduce seams between vertices
for i in winding.size():
winding.set(i, winding.get(i).snappedf(vertex_merge_distance))
# Perform rounding and merge adjacent vertices that are equivalent
if vertex_merge_distance > 0:
var merged_winding : PackedVector3Array = PackedVector3Array()
var prev_vtx : Vector3 = winding[0].snappedf(vertex_merge_distance)
merged_winding.append(prev_vtx)
for i in range(1, winding.size()):
var cur_vtx : Vector3 = winding[i].snappedf(vertex_merge_distance)
if prev_vtx != cur_vtx:
merged_winding.append(cur_vtx)
prev_vtx = cur_vtx
winding = merged_winding
return winding
@ -206,9 +215,9 @@ func determine_entity_origins(entity_index: int) -> void:
var origin_comps: PackedFloat64Array = entity.properties["origin"].split_floats(" ")
if origin_comps.size() > 2:
if entity.origin_type == _OriginType.ABSOLUTE:
entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
else: # _OriginType.RELATIVE
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
_OriginType.BRUSH:
if origin_mins != Vector3.INF:
@ -312,7 +321,7 @@ func generate_entity_surfaces(entity_index: int) -> void:
def = entity.definition
var op_entity_ogl_xf: Callable = func(v: Vector3) -> Vector3:
return (FuncGodotUtil.id_to_opengl(v - entity.origin) * map_settings.scale_factor)
return (FuncGodotUtil.id_to_opengl(v - entity.origin))
# Surface groupings <texture_name, Array[Face]>
var surfaces: Dictionary[String, Array] = {}
@ -372,13 +381,70 @@ func generate_entity_surfaces(entity_index: int) -> void:
# Begin fresh index offset for this subarray
var index_offset: int = 0
for face in faces:
for face: _FaceData in faces:
# FACE SCOPE BEGIN
# Reject invalid faces
if face.vertices.size() < 3 or is_skip(face) or is_origin(face):
continue
#region Reject interior faces only if desired
if entity.properties.get(map_settings.cull_interior_faces_property, false):
var remove_face := false
for face2: _FaceData in faces:
if face == face2:
continue
# Are the planes aligned?
if !face2.plane.has_point(face.plane.get_center()):
continue
# Opposite planes
if !(face.plane.normal*-1.0).is_equal_approx(face2.plane.normal):
continue;
# Check for faces that share all their vertices.
var all_verts_in_face := true
for vert in face.vertices:
if !face2.vertices.has(vert):
all_verts_in_face = false
break;
if all_verts_in_face:
remove_face = true
break
# Check if all vertices of Face1 intersect with any triangle of face 2
# If they do, then Face 1 is entirely overlapped on Face 2 and we can remove Face 1
var all_verts_in_face2 := true
for vert in face.vertices:
var vert_in_any_tri := false
var from := vert - face2.plane.normal*0.001
var to := face2.plane.normal*0.001
# Loop over all triangles in face 2 and see if the vert intersects any of them
for i in ((face2.indices.size()/3)):
var intersect = Geometry3D.ray_intersects_triangle(
from,
to,
face2.vertices[face2.indices[i*3]],
face2.vertices[face2.indices[i*3 + 1]],
face2.vertices[face2.indices[i*3 + 2]]
)
if !intersect:
continue
if intersect:
vert_in_any_tri = true
break;
# This vert didn't show up any triangle, can't remove this face
if !vert_in_any_tri:
all_verts_in_face2 = false
break
# All verts of face 1 are in face 2, so we can safely remove that face
if all_verts_in_face2:
remove_face = true
break;
if remove_face:
continue;
#endregion
# Create trimesh points regardless of texture
if build_concave:
var tris: PackedVector3Array
@ -519,9 +585,11 @@ func generate_entity_surfaces(entity_index: int) -> void:
func unwrap_uv2s(entity_index: int, texel_size: float) -> void:
var entity: _EntityData = entity_data[entity_index]
if entity.mesh:
if (entity.definition as FuncGodotFGDSolidClass).global_illumination_mode:
entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
# NOTE: This skips smoothed meshes as they need to be unwrapped after smoothing.
# Ideally smoothing will be performed here in GeoGen before this process.
# For now, since it occurs in EntityAssembler, skip it.
if entity.mesh and entity.is_gi_enabled() and not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
# Main build process
func build(build_flags: int, entities: Array[_EntityData]) -> Error:

View file

@ -81,8 +81,22 @@ func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _Pa
var entities_data: Array[_EntityData] = parse_data.entities
var entity_defs: Dictionary[String, FuncGodotFGDEntityClass] = map_settings.entity_fgd.get_entity_definitions()
var missing_defs: PackedStringArray = []
declare_step.emit("Checking entity omission and definition status")
var default_point_class := FuncGodotFGDPointClass.new()
default_point_class.node_class = "Marker3D"
var default_solid_class := FuncGodotFGDSolidClass.new()
default_solid_class.spawn_type = FuncGodotFGDSolidClass.SpawnType.ENTITY
default_solid_class.build_occlusion = false
default_solid_class.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
default_solid_class.origin_type = FuncGodotFGDSolidClass.OriginType.BRUSH
declare_step.emit("Checking entity omission, definition status, and property types")
# Cache retrieved class property defaults. Format is Dictionary[Classname, Properties].
var prop_defaults_cache: Dictionary[String, Dictionary] = {}
var prop_descriptions_cache: Dictionary[String, Dictionary] = {}
for i in range(entities_data.size() - 1, -1, -1):
var entity: _EntityData = entities_data[i]
@ -98,6 +112,145 @@ func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _Pa
var classname: String = entity.properties["classname"]
if classname in entity_defs:
entity.definition = entity_defs[classname]
if not entity.definition is FuncGodotFGDSolidClass and not entity.definition is FuncGodotFGDPointClass:
if missing_defs.find(classname) < 0:
push_error("Invalid entity definition for \"" + classname + "\". Entity definition must be Solid Class or Point Class.")
missing_defs.append(classname)
entity.definition = null
elif missing_defs.find(classname) < 0:
push_error("No entity definition found for \"" + classname + "\"")
missing_defs.append(classname)
# Make sure we have a default definition to build entities from
# This will make sure nothing goes wrong in the build processes
if not entity.definition:
if entity.brushes.is_empty():
entity.definition = default_point_class
else:
entity.definition = default_solid_class
# Convert the string values of the entity's properties Dictionary to various
# Variant formats based on the entity definition's class property defaults.
var def := entity.definition
var properties: Dictionary = entity.properties
for property in properties:
var prop_string = entity.properties[property]
if property in def.class_properties:
var prop_default: Variant = def.class_properties[property]
match typeof(prop_default):
TYPE_INT:
properties[property] = prop_string.to_int()
TYPE_FLOAT:
properties[property] = prop_string.to_float()
TYPE_BOOL:
properties[property] = bool(prop_string.to_int())
TYPE_VECTOR3:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 2:
properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
else:
push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR3I:
var prop_vec: Vector3i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
for v in 3:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_COLOR:
var prop_color: Color = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
prop_color.r8 = prop_comps[0].to_int()
prop_color.g8 = prop_comps[1].to_int()
prop_color.b8 = prop_comps[2].to_int()
prop_color.a = 1.0
else:
push_error("Invalid Color format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = def.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
properties[property] = prop_string.to_int()
TYPE_ARRAY:
properties[property] = prop_string.to_int()
TYPE_VECTOR2:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 1:
properties[property] = Vector2(prop_comps[0], prop_comps[1])
else:
push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR2I:
var prop_vec: Vector2i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 1:
for v in 2:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_VECTOR4:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 3:
properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
else:
push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR4I:
var prop_vec: Vector4i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 3:
for v in 4:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_STRING_NAME:
properties[property] = StringName(prop_string)
TYPE_NODE_PATH:
properties[property] = prop_string
TYPE_OBJECT:
properties[property] = prop_string
# Retrieve default properties.
var def_properties: Dictionary[String, Variant] = prop_defaults_cache.get(def.classname, def.retrieve_all_class_properties())
var def_descriptions: Dictionary[String, Variant] = prop_descriptions_cache.get(def.classname, def.retrieve_all_class_property_descriptions())
# Assign properties not defined with defaults from the entity definition
for property in def_properties:
if not property in properties:
var prop_default: Variant = def_properties[property]
# Flags
if prop_default is Array:
var prop_flags_sum := 0
for prop_flag in prop_default:
if prop_flag is Array and prop_flag.size() > 2:
if prop_flag[2] and prop_flag[1] is int:
prop_flags_sum += prop_flag[1]
properties[property] = prop_flags_sum
# Choices
elif prop_default is Dictionary:
var prop_desc = def_descriptions.get(property, "")
if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
properties[property] = prop_desc[1]
elif prop_default.size():
properties[property] = prop_default[prop_default.keys().front()]
else:
properties[property] = 0
# Materials, Shaders, and Sounds
elif prop_default is Resource:
properties[property] = prop_default.resource_path
# Target Destination and Target Source
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
# Delete omitted groups
declare_step.emit("Removing omitted layers and groups")
@ -203,7 +356,7 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point := Vector3(pts[0], pts[1], pts[2])
var point := Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
points[i] = point
var plane := Plane(points[0], points[1], points[2])
@ -244,8 +397,8 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
coords = tokens[2].split_floats(" ", false)
# UV scale factor stored in basis
face.uv.x = Vector2(coords[1], 0.0)
face.uv.y = Vector2(0.0, coords[2])
face.uv.x = Vector2(coords[1], 0.0) * map_settings.scale_factor
face.uv.y = Vector2(0.0, coords[2]) * map_settings.scale_factor
# Quake Standard: texname offsetX offsetY rotation scaleX scaleY
else:
@ -253,8 +406,8 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
face.uv.origin = Vector2(coords[0], coords[1])
var r: float = deg_to_rad(coords[2])
face.uv.x = Vector2(cos(r), -sin(r)) * coords[3]
face.uv.y = Vector2(sin(r), cos(r)) * coords[4]
face.uv.x = Vector2(cos(r), -sin(r)) * coords[3] * map_settings.scale_factor
face.uv.y = Vector2(sin(r), cos(r)) * coords[4] * map_settings.scale_factor
brush.faces.append(face)
continue
@ -374,7 +527,7 @@ func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings,
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point: Vector3 = Vector3(pts[0], pts[1], pts[2])
var point: Vector3 = Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
points[i] = point
brush.planes.append(Plane(points[0], points[1], points[2]))
brush.faces.append(_FaceData.new())
@ -399,10 +552,10 @@ func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings,
face.uv_axes.append(Vector3(vals[0], vals[1], vals[2]))
if key.begins_with("u"):
face.uv.origin.x = vals[3] # Offset
face.uv.x *= vals[4] # Scale
face.uv.x *= vals[4] * map_settings.scale_factor # Scale
else:
face.uv.origin.y = vals[3] # Offset
face.uv.y *= vals[4] # Scale
face.uv.y *= vals[4] * map_settings.scale_factor # Scale
continue
"rotation":
# Rotation isn't used in Valve 220 mapping and VMFs are 220 exclusive

View file

@ -1,5 +1,5 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotFGDEntityClass extends Resource
@abstract class_name FuncGodotFGDEntityClass extends Resource
## Entity definition template. WARNING! Not to be used directly! Use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
##
## Entity definition template. It holds all of the common entity class properties shared between [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass].
@ -28,24 +28,24 @@ var prefix: String = ""
## Key value pair properties that will appear in the map editor. After building the [FuncGodotMap] in Godot, these properties will be added to a [Dictionary]
## that gets applied to the generated node, as long as that node is a tool script with an exported `func_godot_properties` Dictionary.
@export var class_properties : Dictionary = {}
@export var class_properties : Dictionary[String, Variant] = {}
## Map editor descriptions for previously defined key value pair properties. Optional but recommended.
@export var class_property_descriptions : Dictionary = {}
@export var class_property_descriptions : Dictionary[String, Variant] = {}
## Automatically applies entity class properties to matching properties in the generated node.
## When using this feature, class properties need to be the correct type or you may run into errors on map build.
@export var auto_apply_to_matching_node_properties : bool = false
## Appearance properties for the map editor. See the Valve Developer Wiki and TrenchBroom documentation for more information.
@export var meta_properties : Dictionary = {
@export var meta_properties : Dictionary[String, Variant] = {
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
"color": Color(0.8, 0.8, 0.8)
}
@export_group("Node Generation")
## Node to generate on map build. This can be a built-in Godot class, a GDScript class, or a GDExtension class.
## Node to generate on map build. This can be a built-in Godot class, a Script class, or a GDExtension class.
## For Point Class entities that use Scene File instantiation leave this blank.
@export var node_class := ""
@ -54,6 +54,9 @@ var prefix: String = ""
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var name_property := ""
## Optional array of node groups to add the generated node to.
@export var node_groups : Array[String] = []
## Parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
# Class prefix
@ -231,3 +234,17 @@ func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors =
res += "]" + FuncGodotUtil.newline()
return res
func retrieve_all_class_properties(properties: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
for key in class_properties.keys():
properties[key] = class_properties[key]
for b in base_classes:
properties = b.retrieve_all_class_properties(properties)
return properties
func retrieve_all_class_property_descriptions(descriptions: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
for key in class_property_descriptions.keys():
descriptions[key] = class_property_descriptions[key]
for b in base_classes:
descriptions = b.retrieve_all_class_property_descriptions(descriptions)
return descriptions

View file

@ -23,28 +23,25 @@ func export_button() -> void:
do_export_file(target_map_editor)
func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void:
if not Engine.is_editor_hint():
return
if fgd_output_folder.is_empty():
fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String
if fgd_output_folder.is_empty():
print("Skipping export: No game config folder")
printerr("Skipping export: No game config folder")
return
if fgd_name == "":
print("Skipping export: Empty FGD name")
printerr("Skipping export: Empty FGD name")
if not DirAccess.dir_exists_absolute(fgd_output_folder):
if DirAccess.make_dir_recursive_absolute(fgd_output_folder) != OK:
print("Skipping export: Failed to create directory")
printerr("Skipping export: Failed to create directory")
return
var fgd_file = fgd_output_folder.path_join(fgd_name + ".fgd")
var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
if not file_obj:
print("Failed to open file for writing: ", fgd_file)
printerr("Failed to open file for writing: ", fgd_file)
return
print("Exporting FGD to ", fgd_file)
@ -76,6 +73,9 @@ func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMa
## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap].
@export var entity_definitions: Array[Resource] = []
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
var res : String = ""
@ -91,6 +91,8 @@ func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTarget
continue
if ent.func_godot_internal:
continue
if ent is FuncGodotFGDModelPointClass:
ent._model_generation_enabled = generate_model_point_class_models
var ent_text = ent.build_def_text(target_editor)
res += ent_text
@ -127,9 +129,9 @@ func get_entity_definitions() -> Dictionary[String, FuncGodotFGDEntityClass]:
if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass:
var entity_def = ent.duplicate()
var meta_properties := {}
var class_properties := {}
var class_property_descriptions := {}
var meta_properties: Dictionary[String, Variant] = {}
var class_properties: Dictionary[String, Variant] = {}
var class_property_descriptions: Dictionary[String, Variant] = {}
for base_class in _generate_base_class_list(entity_def):
for meta_property in base_class.meta_properties:

View file

@ -29,6 +29,8 @@ enum TargetMapEditor {
## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
@export_tool_button("Generate GD Ignore File", "FileAccess") var generate_gd_ignore_file : Callable = _generate_gd_ignore_file
var _model_generation_enabled: bool = false
func _generate_gd_ignore_file() -> void:
if Engine.is_editor_hint():
var path: String = _get_game_path().path_join(_get_model_folder())
@ -45,7 +47,9 @@ func _generate_gd_ignore_file() -> void:
## Builds and saves the display model into the specified destination, then parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
_generate_model()
if _model_generation_enabled:
_generate_model()
_model_generation_enabled = false
return super()
func _generate_model() -> void:
@ -65,7 +69,10 @@ func _generate_model() -> void:
if target_map_editor == TargetMapEditor.TRENCHBROOM:
const model_key: String = "model"
if scale_expression.is_empty():
meta_properties[model_key] = '"%s"' % _get_local_path()
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
_get_local_path(),
ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
]
else:
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
_get_local_path(),

View file

@ -3,7 +3,7 @@
class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
## FGD PointClass entity definition.
##
## A resource used to define an FGD PointClass entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
## A resource used to define an FGD Point Class entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
## or the [member scene_file] property to tell [FuncGodotMap] what to generate on map build.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
@ -17,15 +17,12 @@ class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
func _init() -> void:
prefix = "@PointClass"
@export_group ("Scene")
## An optional [PackedScene] file to instantiate on map build. Overrides [member FuncGodotFGDEntityClass.node_class] and [member script_class].
@export var scene_file: PackedScene
@export_group ("Scripting")
## An optional [Script] resource to attach to the node generated on map build. Ignored if [member scene_file] is specified.
@export var script_class: Script
@export_group("Build")
## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order.
## Set to [code]false[/code] if you would like to define how the generated node is rotated yourself.
@export var apply_rotation_on_map_build : bool = true
@ -33,3 +30,91 @@ func _init() -> void:
## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node.
## The property can be a [float], [Vector3], or [Vector2]. Set to [code]false[/code] if you would like to define how the generated node is scaled yourself.
@export var apply_scale_on_map_build: bool = true
## An optional [Array] of [FuncGodotFGDPointClassDisplayDescriptor] that describes how this Point Entity should appear in the map editor.
## When using multiple display descriptors, only the first element found without [member FuncGodotFGDPointClassDisplayDescriptor.conditional]
## will be used as the default display asset. If no descriptor is found without a condition, the last descriptor will become the default.[br][br]
## Conditional display descriptors will be written to the FGD in the order set in the array.[br][br]
## [color=orange]WARNING:[/color] Multiple descriptors are only supported by TrenchBroom! They will be omitted on export when
## [member FuncGodotFGDFile.target_map_editor] is not set to [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
@export var display_descriptors: Array[FuncGodotFGDPointClassDisplayDescriptor] = []
func _build_model_branch_text(descriptor: FuncGodotFGDPointClassDisplayDescriptor) -> String:
if not descriptor:
return ''
var model_string: String = ''
var uses_options: bool = false
if not descriptor.scale.is_empty() or not descriptor.skin.is_empty() or not descriptor.frame.is_empty():
uses_options = true
if not uses_options:
return descriptor.display_asset_path
model_string = '{ \"path\": %s' % descriptor.display_asset_path
if not descriptor.skin.is_empty():
model_string += ', \"skin\": %s' % descriptor.skin
if not descriptor.frame.is_empty():
model_string += ', \"frame\": %s' % descriptor.frame
if not descriptor.scale.is_empty():
model_string += ', \"scale\": %s' % descriptor.scale
model_string += " }"
return model_string
func _build_model_text() -> String:
var model_string: String = ''
if display_descriptors.is_empty():
return model_string
if display_descriptors.size() == 1:
return _build_model_branch_text(display_descriptors[0])
model_string = '{{'
var default_display: FuncGodotFGDPointClassDisplayDescriptor
for i in display_descriptors.size():
var d: FuncGodotFGDPointClassDisplayDescriptor = display_descriptors[i]
# Only set the first discovered descriptor without a condition to the default, which must be the last option in a list.
# If a conditional is not set, skip it.
if d.conditional.is_empty():
if not default_display:
default_display = d
else:
printerr(classname + " has a Point Class Display Descriptor without required conditionals set. Must have only 1 conditionless Display Descriptor!")
continue
model_string += '%s -> %s, ' % [d.conditional, _build_model_branch_text(d)]
if default_display:
model_string += '%s }}' % _build_model_branch_text(default_display)
else:
model_string = model_string.trim_suffix(', ')
model_string += ' }}'
return model_string
func _build_studio_text() -> String:
var display_string = ""
for d in display_descriptors:
if d.display_asset_path.find('\"') != -1:
display_string = d.display_asset_path
else:
printerr(classname + " attempting to set an invalid value to @studio format during FGD export. Only relative file paths encapsulated by quotations are valid.")
return display_string
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
if not display_descriptors.is_empty():
if target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
var display_string: String = _build_model_text()
if not display_string.is_empty():
meta_properties["model"] = display_string
else:
var display_string: String = _build_studio_text()
if not display_string.is_empty():
meta_properties["studio"] = display_string
return super(target_editor)

View file

@ -0,0 +1,48 @@
@tool
@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
class_name FuncGodotFGDPointClassDisplayDescriptor extends Resource
## Resource that describes how to display an FGD Point Class entity.
##
## A resource for [FuncGodotFGDPointClass] that describes how to display a point entity in a map editor.
## Values entered into the different options are taken literally: paths should be enclosed within quotation marks,
## while class property keys and integer values should omit them.[br][br]
##
## Most editors only support the [member display_asset] option. Exporting an FGD compatible with these editors will
## automatically omit the unsupported options introduced by TrenchBroom when exporting from their respective game configuration resources
## or setting [member FuncGodotFGDFile.target_map_editor] away from [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
##
## The extra options are considered advanced features and are unable to be evaluated by FuncGodot to ensure they were input correctly.
## Exercise caution, care, and patience when attempting to use these, especially the [member conditional] option.
##
## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
## @tutorial(TrenchBroom Manual: Expression Language): https://trenchbroom.github.io/manual/latest/#expression_language
## Either a file path to the asset that will be displayed for this point entity, relative to the map editor's game path,
## or a class property key that can contain the path.[br][br]
## For paths, you must surround the path with quotes, e.g: [code]"models/marsfrog.glb"[/code].
## For properties, you must omit the quotes, e.g: [code]display_model_path[/code].[br][br]
## Different editors support different file types: common ones include MDL, GLB, SPR, and PNG.
@export var display_asset_path: String = ""
@export_group("TrenchBroom Options")
## Optional string that determines the scale of the display asset. This can be a number, a class property key, or
## a scale expression in accordance with TrenchBroom's Expression Language. Leave blank to use the game configuration's default scale expression.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var scale: String = ""
## Optional string that determines which skin the display asset should use. This can be either a number or a class property key.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var skin: String = ""
## Optional string that determines the appearance of a display asset based on its file type. This can be either a number or a class property key.[br][br]
## Traditional Quake MDL files will set the display to that frame of its animations (all animations in a Quake MDL are compiled into a single animation).
## GLBs meanwhile seem to set themselves to the animation assigned to an index that matches the [code]frame[/code] value.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var frame: String = ""
## Optional evaluation string that, when true, will force the Point Class to display the asset defined by [member display_asset_path].
## Format should be [code]property == value[/code] or some other valid expression in accordance with TrenchBroom's Expression Language.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var conditional: String = ""

View file

@ -0,0 +1 @@
uid://d1nwwgcrner8b

View file

@ -38,6 +38,6 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
map_resource.revision += 1
else:
map_resource = QuakeMapFile.new()
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text(true)
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text()
return ResourceSaver.save(map_resource, save_path_str)

View file

@ -34,7 +34,7 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.lmp file: ', err])
printerr(['Error opening super.lmp file: ', err])
return err
var colors := PackedColorArray()

View file

@ -10,5 +10,5 @@ class_name QuakeWadFile extends Resource
## Collection of [ImageTexture] imported from the WAD file.
@export var textures: Dictionary[String, ImageTexture]
func _init(textures: Dictionary = Dictionary()):
func _init(textures: Dictionary[String, ImageTexture] = {}):
self.textures = textures

View file

@ -70,7 +70,7 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.wad file: ', err])
printerr(['Error opening super.wad file: ', err])
return err
# Read WAD header
@ -81,13 +81,13 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
if magic_string == 'WAD3':
wad_format = WadFormat.HalfLife
elif magic_string != 'WAD2':
print('Error: Invalid WAD magic')
printerr('Error: Invalid WAD magic')
return ERR_INVALID_DATA
var palette_path : String = options['palette_file']
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
if wad_format == WadFormat.Quake and not palette_file:
print('Error: Invalid Quake palette file')
printerr('Error: Invalid Quake palette file')
file.close()
return ERR_CANT_ACQUIRE_RESOURCE

View file

@ -43,6 +43,12 @@ var _map_file_internal: String = ""
## [enum BuildFlags] that can affect certain aspects of the build process.
@export_flags("Unwrap UV2:1", "Show Profiling Info:2", "Disable Smooth Shading:4") var build_flags: int = 0
## The hyperplane is an initial plane that all geometry faces are cut from, like a large sheet of marble before a sculptor begins chiseling.
## The hyperplane size would need to be able to cover your map's potential total area.
## Smaller values can minimize floating point errors, reducing the effect of gaps between polygon seams.
## Measured in Godot units, not Quake units.
@export_range(256.0, 2048.0, 128.0) var hyperplane_size: float = 512.0
## Map build failure handler. Displays error message and emits [signal build_failed] signal.
func fail_build(reason: String, notify: bool = false) -> void:
push_error(_SIGNATURE, " ", reason)
@ -117,7 +123,7 @@ func build() -> void:
parser = null
# Retrieve geometry
var generator := FuncGodotGeometryGenerator.new(map_settings)
var generator := FuncGodotGeometryGenerator.new(map_settings, hyperplane_size)
if build_flags & BuildFlags.SHOW_PROFILE_INFO:
print("\nGEOMETRY GENERATOR")
generator.declare_step.connect(FuncGodotUtil.print_profile_info.bind(generator._SIGNATURE))

View file

@ -4,7 +4,8 @@ class_name FuncGodotMapSettings extends Resource
## Reusable map settings configuration for [FuncGodotMap] nodes.
#region BUILD
@export_category("Build Settings")
@export_group("Build Settings")
## Set automatically when [member inverse_scale_factor] is changed. Used primarily during the build process.
var scale_factor: float = 0.03125
@ -21,30 +22,51 @@ var scale_factor: float = 0.03125
## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var entity_name_property: String = ""
## Class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
@export var entity_smoothing_property: String = "_phong"
## Class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
@export var entity_smoothing_angle_property: String = "_phong_angle"
## If true, will organize [SceneTree] using TrenchBroom Layers and Groups or Hammer Visgroups. Groups will be generated as [Node3D] nodes.
## All non-entity structural brushes will be moved out of their groups and merged into the `Worldspawn` entity.
## Any Layers toggled to be omitted from export in TrenchBroom and their child entities and groups will not be built.
@export var use_groups_hierarchy: bool = false
## Class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
## Utilizing this property can help reduce instances of seams between polygons.
@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
## Texel size for UV2 unwrapping.
## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export var uv_unwrap_texel_size: float = 2.0
#endregion
#region ENTITY
@export_group("Entity Settings")
## Optional array of node groups to add all generated nodes to.
@export var entity_node_groups: Array[String] = []
@export_subgroup("Entity Property Names")
## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var entity_name_property: String = ""
## Entity class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
@export var entity_smoothing_property: String = "_phong"
## Entity class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
@export var entity_smoothing_angle_property: String = "_phong_angle"
## Entity class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
## Utilizing this property can help reduce instances of seams between polygons.
@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
## Entity class property that tells whether interior faces should be culled for that brush entity.
## Interior faces are faces with matching vertices or are flush within a larger face.
## Note that this has a performance impact that scales with how many brushes are in the entity.
@export var cull_interior_faces_property: String = "_cull_interior_faces"
@export_subgroup("")
#endregion
#region TEXTURES
@export_category("Textures")
@export_group("Textures")
## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces.
@export_dir var base_texture_dir: String = "res://textures"
@ -52,25 +74,27 @@ var scale_factor: float = 0.03125
## File extensions to search for texture data.
@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
@export_subgroup("Hint Textures")
## Optional path for the clip texture, relative to [member base_texture_dir].
## Brush faces textured with the clip texture will have those faces removed from the generated [Mesh] but not the generated [Shape3D].
@export var clip_texture: String = "special/clip":
@export var clip_texture: String = "clip":
set(tex):
clip_texture = tex.to_lower()
## Optional path for the skip texture, relative to [member base_texture_dir].
## Brush faces textured with the skip texture will have those faces removed from the generated [Mesh].
## If [member FuncGodotFGDSolidClass.collision_shape_type] is set to concave then it will also remove collision from those faces in the generated [Shape3D].
@export var skip_texture: String = "special/skip":
@export var skip_texture: String = "skip":
set(tex):
skip_texture = tex.to_lower()
## Optional path for the origin texture, relative to [member base_texture_dir].
## Brush faces textured with the origin texture will have those faces removed from the generated [Mesh] and [Shape3D].
## The bounds of these faces will be used to calculate the origin point of the entity.
@export var origin_texture: String = "special/origin":
@export var origin_texture: String = "origin":
set(tex):
origin_texture = tex.to_lower()
@export_subgroup("")
## Optional [QuakeWadFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs.
@export var texture_wads: Array[QuakeWadFile] = []
@ -78,7 +102,7 @@ var scale_factor: float = 0.03125
#endregion
#region MATERIALS
@export_category("Materials")
@export_group("Materials")
## Base directory for loading and saving materials. When building materials, FuncGodot will search this directory for material resources
## with matching names to the textures assigned to map brush faces. If not found, will fall back to [member base_texture_dir].
@ -93,33 +117,33 @@ var scale_factor: float = 0.03125
## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial].
@export var default_material_albedo_uniform: String = ""
## Automatic [ShaderMaterial] generation mapping patterns. Only used when [member default_material] is a ShaderMaterial.
## Keys should be the names of the shader uniforms while the values should be the suffixes for the texture maps.
## Patterns only use one replacement String: the texture name, ex: [code]"%s_normal"[/code].
@export var shader_material_uniform_map_patterns: Dictionary[String, String] = {}
@export_subgroup("BaseMaterial3D Map Patterns")
## Automatic PBR material generation albedo map pattern.
@export var albedo_map_pattern: String = "%s_albedo.%s"
@export var albedo_map_pattern: String = "%s_albedo"
## Automatic PBR material generation normal map pattern.
@export var normal_map_pattern: String = "%s_normal.%s"
@export var normal_map_pattern: String = "%s_normal"
## Automatic PBR material generation metallic map pattern
@export var metallic_map_pattern: String = "%s_metallic.%s"
@export var metallic_map_pattern: String = "%s_metallic"
## Automatic PBR material generation roughness map pattern
@export var roughness_map_pattern: String = "%s_roughness.%s"
@export var roughness_map_pattern: String = "%s_roughness"
## Automatic PBR material generation emission map pattern
@export var emission_map_pattern: String = "%s_emission.%s"
@export var emission_map_pattern: String = "%s_emission"
## Automatic PBR material generation ambient occlusion map pattern
@export var ao_map_pattern: String = "%s_ao.%s"
@export var ao_map_pattern: String = "%s_ao"
## Automatic PBR material generation height map pattern
@export var height_map_pattern: String = "%s_height.%s"
@export var height_map_pattern: String = "%s_height"
## Automatic PBR material generation ORM map pattern
@export var orm_map_pattern: String = "%s_orm.%s"
@export var orm_map_pattern: String = "%s_orm"
@export_subgroup("")
## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes.
## [i]NOTE: Materials do not use the [member default_material] settings after saving.[/i]
@export var save_generated_materials: bool = true
@export_group("")
#endregion
@export_category("UV Unwrap")
## Texel size for UV2 unwrapping.
## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export var uv_unwrap_texel_size: float = 2.0

View file

@ -27,6 +27,9 @@ enum NetRadiantCustomMapType {
## this should be the master FGD that contains them in [member FuncGodotFGDFile.base_fgd_files].
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
## Collection of [NetRadiantCustomShader] resources for shader file generation.
@export var netradiant_custom_shaders : Array[Resource] = [
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
@ -34,28 +37,30 @@ enum NetRadiantCustomMapType {
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres")
]
## Supported texture file types.
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
## Supported model file types.
@export var model_types : PackedStringArray = ["glb", "gltf", "obj"]
## Supported audio file types.
@export var sound_types : PackedStringArray = ["wav", "ogg"]
## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_3
@export_group("Textures")
## Supported texture file types.
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
## Default scale of textures in NetRadiant Custom.
@export var default_scale : String = "1.0"
## Clip texture path that gets applied to [i]weapclip[/i] and [i]nodraw[/i] shaders.
@export var clip_texture: String = "textures/special/clip"
@export var clip_texture: String = "textures/clip"
## Skip texture path that gets applied to [i]caulk[/i] and [i]nodrawnonsolid[/i] shaders.
@export var skip_texture: String = "textures/special/skip"
## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_1
@export var skip_texture: String = "textures/skip"
@export_group("Build Menu")
## Variables to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
## Each [String] key defines a variable name, and its corresponding [String] value as the literal command-line string
## to execute in place of this variable identifier[br][br]
@ -312,5 +317,6 @@ func export_file() -> void:
# FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.generate_model_point_class_models = generate_model_point_class_models
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.NET_RADIANT_CUSTOM, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path)
print("NetRadiant Custom Gamepack export complete\n")

View file

@ -31,7 +31,7 @@ enum GameConfigVersion {
{ "format": "Quake3" }
]
@export_category("Textures")
@export_group("Textures")
## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom.
@export var textures_root_folder: String = "textures"
@ -42,7 +42,7 @@ enum GameConfigVersion {
## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information.
@export var palette_path: String = "textures/palette.lmp"
@export_category("Entities")
@export_group("Entities")
## [FuncGodotFGDFile] resource to include with this game. If using multiple FGD File resources,
## this should be the master FGD File that contains them in [member FuncGodotFGDFile.base_fgd_files].
@ -52,8 +52,11 @@ enum GameConfigVersion {
## See [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities"]TrenchBroom Manual Entity Configuration Information[/url] for more information.
@export var entity_scale: String = "32"
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
## Arrays containing the [TrenchbroomTag] resource type.
@export_category("Tags")
@export_group("Tags")
## [TrenchbroomTag] resources that apply to brush entities.
@export var brush_tags : Array[Resource] = []
@ -65,12 +68,12 @@ enum GameConfigVersion {
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
]
@export_category("Face Attributes")
@export_group("Face Attributes")
## Default scale of textures on new brushes and when UV scale is reset.
@export var default_uv_scale : Vector2 = Vector2(1, 1)
@export_category("Compatibility")
@export_group("Compatibility")
## Game configuration format compatible with the version of TrenchBroom being used.
@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
@ -229,6 +232,7 @@ func export_file() -> void:
# FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.generate_model_point_class_models = generate_model_point_class_models
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
print("TrenchBroom Game Config export complete\n")

View file

@ -55,18 +55,6 @@ const _CONFIG_PROPERTIES: Array[Dictionary] = [
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
},
#{
#"name": "game_path_models_folder",
#"usage": PROPERTY_USAGE_EDITOR,
#"type": TYPE_STRING,
#"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
#},
#{
#"name": "default_inverse_scale_factor",
#"usage": PROPERTY_USAGE_EDITOR,
#"type": TYPE_FLOAT,
#"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
#}
]
var _settings_dict: Dictionary
@ -117,9 +105,13 @@ func _get_config_property(name: StringName) -> Variant:
## Reload this system's configuration settings into the Local Config resource.
func reload_func_godot_settings() -> void:
_loaded = true
var path = _get_path()
var path = "user://func_godot_config.json"
if not FileAccess.file_exists(path):
return
var application_name: String = ProjectSettings.get('application/config/name')
application_name = application_name.replace(" ", "_")
path = "user://" + application_name + "_FuncGodotConfig.json"
if not FileAccess.file_exists(path):
return
var settings = FileAccess.get_file_as_string(path)
_settings_dict = {}
if not settings or settings.is_empty():
@ -137,14 +129,9 @@ func _try_loading() -> void:
func export_func_godot_settings() -> void:
if _settings_dict.size() == 0:
return
var path = _get_path()
var path = "user://func_godot_config.json"
var file = FileAccess.open(path, FileAccess.WRITE)
var json = JSON.stringify(_settings_dict)
file.store_line(json)
_loaded = false
print("Saved settings to ", path)
func _get_path() -> String:
var application_name: String = ProjectSettings.get('application/config/name')
application_name = application_name.replace(" ", "_")
return 'user://' + application_name + '_FuncGodotConfig.json'
print("Saved settings to ", file.get_path_absolute())

View file

@ -1,188 +0,0 @@
class_name FuncGodotTextureLoader
enum PBRSuffix {
ALBEDO,
NORMAL,
METALLIC,
ROUGHNESS,
EMISSION,
AO,
HEIGHT,
ORM
}
# Suffix string / Godot enum / StandardMaterial3D property
const PBR_SUFFIX_NAMES: Dictionary = {
PBRSuffix.ALBEDO: 'albedo',
PBRSuffix.NORMAL: 'normal',
PBRSuffix.METALLIC: 'metallic',
PBRSuffix.ROUGHNESS: 'roughness',
PBRSuffix.EMISSION: 'emission',
PBRSuffix.AO: 'ao',
PBRSuffix.HEIGHT: 'height',
PBRSuffix.ORM: 'orm'
}
const PBR_SUFFIX_PATTERNS: Dictionary = {
PBRSuffix.ALBEDO: '%s_albedo.%s',
PBRSuffix.NORMAL: '%s_normal.%s',
PBRSuffix.METALLIC: '%s_metallic.%s',
PBRSuffix.ROUGHNESS: '%s_roughness.%s',
PBRSuffix.EMISSION: '%s_emission.%s',
PBRSuffix.AO: '%s_ao.%s',
PBRSuffix.HEIGHT: '%s_height.%s',
PBRSuffix.ORM: '%s_orm.%s'
}
var PBR_SUFFIX_TEXTURES: Dictionary = {
PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO,
PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL,
PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC,
PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS,
PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION,
PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP,
PBRSuffix.ORM: ORMMaterial3D.TEXTURE_ORM
}
const PBR_SUFFIX_PROPERTIES: Dictionary = {
PBRSuffix.NORMAL: 'normal_enabled',
PBRSuffix.EMISSION: 'emission_enabled',
PBRSuffix.AO: 'ao_enabled',
PBRSuffix.HEIGHT: 'heightmap_enabled',
}
var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new()
var texture_wad_resources: Array = []
# Overrides
func _init(new_map_settings: FuncGodotMapSettings) -> void:
map_settings = new_map_settings
load_texture_wad_resources()
# Business Logic
func load_texture_wad_resources() -> void:
texture_wad_resources.clear()
for texture_wad in map_settings.texture_wads:
if texture_wad and not texture_wad in texture_wad_resources:
texture_wad_resources.append(texture_wad)
func load_textures(texture_list: Array) -> Dictionary:
var texture_dict: Dictionary = {}
for texture_name in texture_list:
texture_dict[texture_name] = load_texture(texture_name)
return texture_dict
func load_texture(texture_name: String) -> Texture2D:
# Load albedo texture if it exists
for texture_extension in map_settings.texture_file_extensions:
var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension]
if ResourceLoader.exists(texture_path, "Texture2D") or ResourceLoader.exists(texture_path + ".import", "Texture2D"):
return load(texture_path) as Texture2D
var texture_name_lower: String = texture_name.to_lower()
for texture_wad in texture_wad_resources:
if texture_name_lower in texture_wad.textures:
return texture_wad.textures[texture_name_lower]
return load("res://addons/func_godot/textures/default_texture.png") as Texture2D
func create_materials(texture_list: Array) -> Dictionary:
var texture_materials: Dictionary = {}
#prints("TEXLI", texture_list)
for texture in texture_list:
texture_materials[texture] = create_material(texture)
return texture_materials
func create_material(texture_name: String) -> Material:
# Autoload material if it exists
var material_dict: Dictionary = {}
var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension]
if not material_path in material_dict and (FileAccess.file_exists(material_path) or FileAccess.file_exists(material_path + ".remap")):
var loaded_material: Material = load(material_path)
if loaded_material:
material_dict[material_path] = loaded_material
# If material already exists, use it
if material_path in material_dict:
return material_dict[material_path]
var material: Material = null
if map_settings.default_material:
material = map_settings.default_material.duplicate()
else:
material = StandardMaterial3D.new()
var texture: Texture2D = load_texture(texture_name)
if not texture:
return material
if material is StandardMaterial3D:
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
elif material is ShaderMaterial && map_settings.default_material_albedo_uniform != "":
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
elif material is ORMMaterial3D:
material.set_texture(ORMMaterial3D.TEXTURE_ALBEDO, texture)
var pbr_textures : Dictionary = get_pbr_textures(texture_name)
for pbr_suffix in PBRSuffix.values():
var suffix: int = pbr_suffix
var tex: Texture2D = pbr_textures[suffix]
if tex:
if material is ShaderMaterial:
material = StandardMaterial3D.new()
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else ""
if(enable_prop != ""):
material.set(enable_prop, true)
material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex)
material_dict[material_path] = material
if (map_settings.save_generated_materials and material
and texture_name != map_settings.clip_texture
and texture_name != map_settings.skip_texture
and texture_name != map_settings.origin_texture
and texture.resource_path != "res://addons/func_godot/textures/default_texture.png"):
ResourceSaver.save(material, material_path)
return material
# PBR texture fetching
func get_pbr_suffix_pattern(suffix: int) -> String:
if not suffix in PBR_SUFFIX_NAMES:
return ''
var pattern_setting: String = "%s_map_pattern" % [PBR_SUFFIX_NAMES[suffix]]
if pattern_setting in map_settings:
return map_settings.get(pattern_setting)
return PBR_SUFFIX_PATTERNS[suffix]
func get_pbr_texture(texture: String, suffix: PBRSuffix) -> Texture2D:
var texture_comps: PackedStringArray = texture.split('/')
if texture_comps.size() == 0:
return null
for texture_extension in map_settings.texture_file_extensions:
var path: String = "%s/%s/%s" % [
map_settings.base_texture_dir,
'/'.join(texture_comps),
get_pbr_suffix_pattern(suffix) % [
texture_comps[-1],
texture_extension
]
]
if(FileAccess.file_exists(path)):
return load(path) as Texture2D
return null
func get_pbr_textures(texture_name: String) -> Dictionary:
var pbr_textures: Dictionary = {}
for pbr_suffix in PBRSuffix.values():
pbr_textures[pbr_suffix] = get_pbr_texture(texture_name, pbr_suffix)
return pbr_textures

View file

@ -1 +0,0 @@
uid://c0r8ajf4k061i

View file

@ -80,16 +80,33 @@ const _pbr_textures: PackedInt32Array = [
StandardMaterial3D.TEXTURE_EMISSION,
StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
StandardMaterial3D.TEXTURE_HEIGHTMAP,
ORMMaterial3D.TEXTURE_ORM
ORMMaterial3D.TEXTURE_ORM,
]
# Used during auto-PBR processing. Must match the _pbr_textures order.
# -1 means the feature is permanantly enabled.
const _pbr_features: PackedInt32Array = [
-1,
BaseMaterial3D.FEATURE_NORMAL_MAPPING,
-1,
-1,
BaseMaterial3D.FEATURE_EMISSION,
BaseMaterial3D.FEATURE_AMBIENT_OCCLUSION,
BaseMaterial3D.FEATURE_HEIGHT_MAPPING,
-1,
]
## Searches for a Texture2D within the base texture directory or the WAD files added to map settings.
## If not found, a default texture is returned.
static func load_texture(texture_name: String, wad_resources: Array[QuakeWadFile], map_settings: FuncGodotMapSettings) -> Texture2D:
for texture_file_extension in map_settings.texture_file_extensions:
var texture_path: String = map_settings.base_texture_dir.path_join(texture_name + "." + texture_file_extension)
if ResourceLoader.exists(texture_path):
return load(texture_path)
var texture_file = load(texture_path)
if texture_file is Texture2D:
return texture_file
else:
printerr("Error: Texture load failed! (%s) not a valid Texture2D resource", texture_path)
var texture_name_lower: String = texture_name.to_lower()
for wad in wad_resources:
@ -131,8 +148,8 @@ static func filter_face(texture: String, map_settings: FuncGodotMapSettings) ->
static func build_base_material(map_settings: FuncGodotMapSettings, material: BaseMaterial3D, texture: String) -> void:
var path: String = map_settings.base_texture_dir.path_join(texture)
# Check if there is a subfolder with our PBR textures
if DirAccess.open(path.path_join(path)):
path = path.path_join(path)
if DirAccess.open(path):
path = path.path_join(texture)
var pbr_suffixes: PackedStringArray = [
map_settings.albedo_map_pattern,
@ -145,12 +162,33 @@ static func build_base_material(map_settings: FuncGodotMapSettings, material: Ba
map_settings.orm_map_pattern,
]
for texture_file_extension in map_settings.texture_file_extensions:
for i in pbr_suffixes.size():
if not pbr_suffixes[i].is_empty():
var pbr: String = pbr_suffixes[i] % [path, texture_file_extension]
if ResourceLoader.exists(pbr):
material.set_texture(_pbr_textures[i], load(pbr))
for i in pbr_suffixes.size():
if not pbr_suffixes[i].is_empty():
var pbr: String = pbr_suffixes[i]
var token: int = pbr.find("%s", 0)
if token != -1:
if pbr.find("%s", token + 1) != -1:
token = 2
else:
token = 1
if token < 1:
printerr("No string replacement tokens found in auto-PBR pattern \'" + pbr + "\'! Must have at least one instance of \'%s\' per pattern.")
continue
if token > 0:
for texture_file_extension in map_settings.texture_file_extensions:
if token > 1:
pbr = pbr_suffixes[i] % [path, texture_file_extension]
else:
pbr = pbr_suffixes[i] % [path]
pbr += "." + texture_file_extension
if ResourceLoader.exists(pbr):
print(pbr)
if _pbr_features[i] > -1:
material.set_feature(_pbr_features[i], true)
material.set_texture(_pbr_textures[i], load(pbr))
break
## Builds both materials and sizes dictionaries for use in the geometry generation step of the build process.
## Both dictionaries use texture names as keys. The materials dictionary uses [Material] as values,
@ -202,7 +240,7 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
# Material generation
elif map_settings.default_material:
var material = map_settings.default_material.duplicate(true)
var material = map_settings.default_material.duplicate(false)
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
texture_sizes[texture_name] = texture.get_size()
@ -211,12 +249,29 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
build_base_material(map_settings, material, texture_name)
elif material is ShaderMaterial:
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
var path: String = map_settings.base_texture_dir
for uniform in map_settings.shader_material_uniform_map_patterns.keys():
if map_settings.shader_material_uniform_map_patterns[uniform].find("%s") < 0:
printerr("No string replacement tokens fuond in ShaderMaterial uniform map pattern \'" + map_settings.shader_material_uniform_map_patterns[uniform] + "\'! Must have one instance of \'%s\' per pattern.")
continue
for texture_file_extension in map_settings.texture_file_extensions:
var uniform_texture_path: String = map_settings.shader_material_uniform_map_patterns[uniform] % [texture_name] + "." + texture_file_extension
uniform_texture_path = path.path_join(uniform_texture_path)
if ResourceLoader.exists(uniform_texture_path):
material.set_shader_parameter(uniform, load(uniform_texture_path))
break
if (map_settings.save_generated_materials and material
and texture_name != map_settings.clip_texture
and texture_name != map_settings.skip_texture
and texture_name != map_settings.origin_texture
and texture.resource_path != default_texture_path):
# Make sure our material directory exists
var dir := DirAccess.open(material_path.get_base_dir())
if not dir:
dir = DirAccess.open("res://")
dir.make_dir_recursive(material_path.get_base_dir().trim_prefix("res://"))
# Save the new material
ResourceSaver.save(material, material_path)
texture_materials[texture_name] = material
@ -232,30 +287,30 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
## Returns UV coordinate calculated from the Valve 220 UV format.
static func get_valve_uv(vertex: Vector3, u_axis: Vector3, v_axis: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
var uv := Vector2(u_axis.dot(vertex), v_axis.dot(vertex))
uv += (uv_basis.origin * uv_basis.get_scale())
uv.x /= uv_basis.x.x
uv.y /= uv_basis.y.y
var scale := Vector2(uv_basis.x.x, uv_basis.y.y)
uv += (uv_basis.origin * scale)
uv /= scale;
uv.x /= texture_size.x
uv.y /= texture_size.y
return uv
## Returns UV coordinate calculated from the original id Standard UV format.
static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_in := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
var uv_out: Vector2
var nx := absf(normal.dot(Vector3.RIGHT))
var ny := absf(normal.dot(Vector3.UP))
var nz := absf(normal.dot(Vector3.FORWARD))
var uv: Vector2
if ny >= nx and ny >= nz:
uv = Vector2(vertex.x, -vertex.z)
uv_out = Vector2(vertex.x, -vertex.z)
elif nx >= ny and nx >= nz:
uv = Vector2(vertex.y, -vertex.z)
uv_out = Vector2(vertex.y, -vertex.z)
else:
uv = Vector2(vertex.x, vertex.y)
uv_out = Vector2(vertex.x, vertex.y)
var uv_out := uv.rotated(uv_basis.get_rotation())
uv_out /= uv_basis.get_scale()
uv_out += uv_basis.origin
uv_out = uv_out.rotated(uv_in.get_rotation())
uv_out /= uv_in.get_scale()
uv_out += uv_in.origin
uv_out /= texture_size
return uv_out
@ -322,10 +377,10 @@ static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array
#region MESH
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> Mesh:
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> ArrayMesh:
if not mesh:
push_error("Need a source mesh to smooth")
return
return null
var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))