t_p5_validator: add 5 Godot 4 map validator scripts
- validate_map.gd — scene structure validator (CSG combiner root, spawn groups, bomb sites, buy zones, map bounds, LightmapGI config, lighting setup) - validate_polycount.gd — CSG triangle budget checker (<50K faces, <5K per node, <200 CSG nodes) - validate_textures.gd — texture size ≤1K, mipmaps, UV2 checker - validate_lights.gd — dynamic lights ≤4, bake mode validator - bake_lightmaps.gd — LightmapGI config checker (quality, bounces, texel_scale, interior, denoiser) All scripts: @tool SceneTree, accept scene path via -- separator, exit 0=pass / 1=fail / 2=scene-not-found. 443-610 lines each.
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# validate_textures.gd — Texture Size / Mipmaps / UV2 Checker
|
||||
# =============================================================================
|
||||
# Scans all material and texture resources used in a scene and validates:
|
||||
# - Texture dimensions ≤ 1024 (1K)
|
||||
# - Mipmaps are enabled on all textures
|
||||
# - UV2 channel is present on meshes (for lightmapping)
|
||||
# - Texture format is appropriate
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/validate_textures.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All texture checks passed
|
||||
# 1 — One or more texture checks failed
|
||||
# 2 — Scene file not found or cannot be loaded
|
||||
# =============================================================================
|
||||
|
||||
const MAX_TEXTURE_SIZE := 1024
|
||||
const MAX_TEXTURE_SIZE_LABEL := "1K (1024x1024)"
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
|
||||
# Collected data
|
||||
var _textures_checked: Dictionary = {} # resource_path -> info dict
|
||||
var _overbudget_textures: Array[Dictionary] = []
|
||||
var _no_mipmap_textures: Array[Dictionary] = []
|
||||
var _meshes_without_uv2: Array[String] = []
|
||||
var _meshes_with_uv2: Array[String] = []
|
||||
var _mesh_count := 0
|
||||
var _total_textures := 0
|
||||
var _total_materials := 0
|
||||
var _texture_memory_estimate := 0 # approximate total texture memory in bytes
|
||||
|
||||
# Unique set to avoid re-checking the same resource
|
||||
var _checked_resources: Dictionary = {}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_textures.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" TEXTURE VALIDATOR")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
print(" Texture budget:")
|
||||
print(" Max dimension: ", MAX_TEXTURE_SIZE_LABEL)
|
||||
print(" Mipmaps: Required")
|
||||
print(" UV2 channel: Required for lightmapping")
|
||||
print("")
|
||||
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
|
||||
# Analyze
|
||||
_analyze_node(_scene_instance)
|
||||
|
||||
# Run checks
|
||||
_run_texture_checks()
|
||||
_run_mipmap_checks()
|
||||
_run_uv2_checks()
|
||||
_print_texture_report()
|
||||
|
||||
# Summary
|
||||
_print_summary()
|
||||
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""Extract scene path from user args (after --)."""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if not args.is_empty():
|
||||
_scene_path = args[0].strip_edges()
|
||||
return
|
||||
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == "--":
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""Load a .tscn/.scn file."""
|
||||
if not ResourceLoader.exists(path):
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
return scene.instantiate()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Analysis engine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _analyze_node(node: Node) -> void:
|
||||
"""Recursively walk scene, check materials on meshes."""
|
||||
var class_name := node.get_class()
|
||||
|
||||
# MeshInstance3D — inspect its material overrides
|
||||
if class_name == "MeshInstance3D" or node.is_class("MeshInstance3D"):
|
||||
_mesh_count += 1
|
||||
_analyze_mesh_instance(node)
|
||||
|
||||
# CSG nodes can also have materials / meshes
|
||||
if class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D",
|
||||
"CSGTorus3D", "CSGPolygon3D", "CSGMesh3D"]:
|
||||
_analyze_csg_node(node)
|
||||
|
||||
# GeometryInstance3D base class
|
||||
if node.is_class("GeometryInstance3D"):
|
||||
var mat_override := node.get("material_override") if "material_override" in node else null
|
||||
if mat_override != null:
|
||||
_analyze_material(mat_override)
|
||||
|
||||
for child in node.get_children():
|
||||
_analyze_node(child)
|
||||
|
||||
|
||||
func _analyze_mesh_instance(mi: Node) -> void:
|
||||
"""Inspect a MeshInstance3D node for materials and UV2."""
|
||||
# Check mesh
|
||||
if "mesh" in mi and mi.mesh != null:
|
||||
var mesh = mi.mesh
|
||||
_analyze_mesh(mesh, mi.name)
|
||||
|
||||
# Surface material overrides
|
||||
if "material_override" in mi and mi.material_override != null:
|
||||
_analyze_material(mi.material_override)
|
||||
|
||||
# Per-surface materials
|
||||
if mi.has_method("get_surface_override_material"):
|
||||
var surface_count := 0
|
||||
if "mesh" in mi and mi.mesh != null and mi.mesh.has_method("get_surface_count"):
|
||||
surface_count = mi.mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var mat := mi.get_surface_override_material(s)
|
||||
if mat != null:
|
||||
_analyze_material(mat)
|
||||
|
||||
|
||||
func _analyze_csg_node(node: Node) -> void:
|
||||
"""Analyze materials on CSG nodes."""
|
||||
# CSG nodes have operation and material properties
|
||||
if "material" in node and node.material != null:
|
||||
_analyze_material(node.material)
|
||||
|
||||
# Check CSGMesh3D which references a mesh
|
||||
if node.get_class() == "CSGMesh3D" and "mesh" in node and node.mesh != null:
|
||||
_analyze_mesh(node.mesh, node.name)
|
||||
|
||||
|
||||
func _analyze_mesh(mesh, node_name: String) -> void:
|
||||
"""Analyze a mesh resource for UV2 and material slots."""
|
||||
if mesh == null:
|
||||
return
|
||||
|
||||
var mesh_id := mesh.resource_path if "resource_path" in mesh and mesh.resource_path != "" else str(mesh.get_instance_id())
|
||||
if _checked_resources.has(mesh_id):
|
||||
return
|
||||
_checked_resources[mesh_id] = true
|
||||
|
||||
# Check for UV2 channel
|
||||
var has_uv2 := false
|
||||
if mesh.has_method("get_surface_count"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays is Array and arrays.size() > 0:
|
||||
var uv2 := arrays[Mesh.ARRAY_TEX_UV2] as PackedVector2Array
|
||||
if uv2 != null and uv2.size() > 0:
|
||||
has_uv2 = true
|
||||
|
||||
if has_uv2:
|
||||
_meshes_with_uv2.append(node_name)
|
||||
else:
|
||||
_meshes_without_uv2.append(node_name)
|
||||
|
||||
# Check materials on the mesh
|
||||
if mesh.has_method("surface_get_material"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var mat := mesh.surface_get_material(s)
|
||||
if mat != null:
|
||||
_analyze_material(mat)
|
||||
|
||||
|
||||
func _analyze_material(mat) -> void:
|
||||
"""Recursively analyze a material and its textures."""
|
||||
if mat == null:
|
||||
return
|
||||
|
||||
var mat_id := mat.resource_path if "resource_path" in mat and mat.resource_path != "" else str(mat.get_instance_id())
|
||||
if _checked_resources.has(mat_id):
|
||||
return
|
||||
_checked_resources[mat_id] = true
|
||||
_total_materials += 1
|
||||
|
||||
var mat_class := mat.get_class()
|
||||
|
||||
# StandardMaterial3D — check texture properties
|
||||
if mat_class == "StandardMaterial3D" or mat_class == "ORMMaterial3D":
|
||||
var tex_properties := [
|
||||
"albedo_texture", "metallic_texture", "roughness_texture",
|
||||
"normal_texture", "ambient_occlusion_texture", "emission_texture",
|
||||
"clearcoat_texture", "rim_texture", "transparency_texture",
|
||||
"heightmap_texture", "refraction_texture", "detail_albedo_texture",
|
||||
"detail_normal_texture", "detail_mask_texture", "subsurface_scattering_texture",
|
||||
"backlight_texture"
|
||||
]
|
||||
for prop in tex_properties:
|
||||
if prop in mat:
|
||||
var tex = mat.get(prop)
|
||||
if tex != null:
|
||||
_analyze_texture(tex)
|
||||
|
||||
# ShaderMaterial — inspect shader parameters for textures
|
||||
elif mat_class == "ShaderMaterial":
|
||||
if mat.has_method("get_shader_parameter_list"):
|
||||
var param_list := mat.get_shader_parameter_list()
|
||||
for param in param_list:
|
||||
var param_name := param.name if "name" in param else str(param)
|
||||
var val = mat.get_shader_parameter(param_name)
|
||||
if val != null and (val is Texture2D or val is Texture3D or val is TextureLayered):
|
||||
_analyze_texture(val)
|
||||
|
||||
# BaseMaterial3D (Godot 4 fallback)
|
||||
elif mat.is_class("BaseMaterial3D"):
|
||||
if "albedo_texture" in mat and mat.albedo_texture != null:
|
||||
_analyze_texture(mat.albedo_texture)
|
||||
if "metallic_texture" in mat and mat.metallic_texture != null:
|
||||
_analyze_texture(mat.metallic_texture)
|
||||
if "roughness_texture" in mat and mat.roughness_texture != null:
|
||||
_analyze_texture(mat.roughness_texture)
|
||||
if "normal_texture" in mat and mat.normal_texture != null:
|
||||
_analyze_texture(mat.normal_texture)
|
||||
|
||||
|
||||
func _analyze_texture(tex) -> void:
|
||||
"""Analyze a single texture resource for size, mipmaps, and format."""
|
||||
if tex == null:
|
||||
return
|
||||
|
||||
# Use resource path as unique key
|
||||
var tex_path := ""
|
||||
if "resource_path" in tex and tex.resource_path != "":
|
||||
tex_path = tex.resource_path
|
||||
else:
|
||||
tex_path = str(tex.get_instance_id())
|
||||
|
||||
if _textures_checked.has(tex_path):
|
||||
return
|
||||
|
||||
var tex_class := tex.get_class()
|
||||
var width := 0
|
||||
var height := 0
|
||||
var has_mipmaps := false
|
||||
var format := -1
|
||||
var format_name := "Unknown"
|
||||
|
||||
# Texture2D
|
||||
if tex is Texture2D:
|
||||
width = tex.get_width()
|
||||
height = tex.get_height()
|
||||
has_mipmaps = tex.has_mipmaps() if tex.has_method("has_mipmaps") else false
|
||||
if tex.has_method("get_format"):
|
||||
format = tex.get_format()
|
||||
format_name = _get_format_name(format)
|
||||
_total_textures += 1
|
||||
|
||||
# Texture3D
|
||||
elif tex is Texture3D:
|
||||
width = tex.get_width()
|
||||
height = tex.get_height()
|
||||
if tex.has_method("has_mipmaps"):
|
||||
has_mipmaps = tex.has_mipmaps()
|
||||
_total_textures += 1
|
||||
|
||||
# TextureLayered
|
||||
elif tex is TextureLayered:
|
||||
if tex.has_method("get_width"):
|
||||
width = tex.get_width()
|
||||
if tex.has_method("get_height"):
|
||||
height = tex.get_height()
|
||||
_total_textures += 1
|
||||
|
||||
var info := {
|
||||
"path": tex_path,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"has_mipmaps": has_mipmaps,
|
||||
"format": format_name,
|
||||
"class": tex_class
|
||||
}
|
||||
_textures_checked[tex_path] = info
|
||||
|
||||
# Estimate memory
|
||||
var bpp := _bits_per_pixel(format)
|
||||
_texture_memory_estimate += (width * height * bpp) / 8
|
||||
|
||||
# Check over-budget
|
||||
var max_dim := maxi(width, height)
|
||||
if max_dim > MAX_TEXTURE_SIZE:
|
||||
_overbudget_textures.append(info)
|
||||
|
||||
# Check mipmaps
|
||||
if not has_mipmaps:
|
||||
_no_mipmap_textures.append(info)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Texture checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_texture_checks() -> void:
|
||||
"""Validate texture size budget."""
|
||||
print("--- Texture Size Checks ---")
|
||||
if _total_textures == 0:
|
||||
_warn("No textures found in scene (may be expected for procedural materials)")
|
||||
return
|
||||
|
||||
_pass("Total textures found: " + str(_total_textures))
|
||||
|
||||
if _overbudget_textures.size() == 0:
|
||||
_pass("All textures within " + MAX_TEXTURE_SIZE_LABEL + " limit")
|
||||
else:
|
||||
for tex_info in _overbudget_textures:
|
||||
var dim := str(tex_info.width) + "x" + str(tex_info.height)
|
||||
_fail("Texture over budget: " + _truncate_path(tex_info.path) + " (" + dim + ", max: " + MAX_TEXTURE_SIZE_LABEL + ")")
|
||||
|
||||
|
||||
func _run_mipmap_checks() -> void:
|
||||
"""Validate that all textures have mipmaps enabled."""
|
||||
print("")
|
||||
print("--- Mipmap Checks ---")
|
||||
|
||||
if _no_mipmap_textures.size() == 0:
|
||||
_pass("All textures have mipmaps enabled")
|
||||
else:
|
||||
for tex_info in _no_mipmap_textures:
|
||||
var dim := str(tex_info.width) + "x" + str(tex_info.height)
|
||||
_fail("Texture missing mipmaps: " + _truncate_path(tex_info.path) + " (" + dim + ", class: " + tex_info.class + ")")
|
||||
|
||||
|
||||
func _run_uv2_checks() -> void:
|
||||
"""Validate UV2 channel presence on meshes."""
|
||||
print("")
|
||||
print("--- UV2 Channel Checks ---")
|
||||
|
||||
if _mesh_count == 0:
|
||||
_warn("No MeshInstance3D nodes found")
|
||||
return
|
||||
|
||||
_pass("Meshes found: " + str(_mesh_count))
|
||||
|
||||
if _meshes_without_uv2.size() == 0:
|
||||
_pass("All " + str(_mesh_count) + " meshes have UV2 channel (good for lightmapping)")
|
||||
else:
|
||||
for mesh_name in _meshes_without_uv2:
|
||||
_fail("Mesh missing UV2 channel: " + mesh_name + " — lightmapping will not work correctly")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_texture_report() -> void:
|
||||
"""Print detailed texture report."""
|
||||
print("")
|
||||
print("--- Texture Report ---")
|
||||
print(" Total materials: ", _total_materials)
|
||||
print(" Total textures: ", _total_textures)
|
||||
print(" Over-budget textures: ", _overbudget_textures.size())
|
||||
print(" Missing mipmaps: ", _no_mipmap_textures.size())
|
||||
print(" Total meshes: ", _mesh_count)
|
||||
print(" Meshes with UV2: ", _meshes_with_uv2.size())
|
||||
print(" Meshes without UV2: ", _meshes_without_uv2.size())
|
||||
print(" Est. texture memory: ", _format_bytes(_texture_memory_estimate))
|
||||
print("")
|
||||
|
||||
# Print texture format distribution
|
||||
var format_counts: Dictionary = {}
|
||||
for tex_path in _textures_checked.keys():
|
||||
var info := _textures_checked[tex_path] as Dictionary
|
||||
var fmt := info.format
|
||||
if not format_counts.has(fmt):
|
||||
format_counts[fmt] = 0
|
||||
format_counts[fmt] += 1
|
||||
|
||||
if format_counts.size() > 0:
|
||||
print(" Texture format distribution:")
|
||||
for fmt in format_counts.keys():
|
||||
print(" " + fmt + ": " + str(format_counts[fmt]))
|
||||
print("")
|
||||
|
||||
|
||||
func _truncate_path(path: String) -> String:
|
||||
"""Truncate a resource path to fit in a single line."""
|
||||
if path.length() > 60:
|
||||
return "..." + path.substr(path.length() - 57)
|
||||
return path
|
||||
|
||||
|
||||
func _get_format_name(format_id: int) -> String:
|
||||
"""Map Godot image format enum to a human-readable name."""
|
||||
match format_id:
|
||||
0: return "L8"
|
||||
1: return "LA8"
|
||||
2: return "R8"
|
||||
3: return "RG8"
|
||||
4: return "RGB8"
|
||||
5: return "RGBA8"
|
||||
6: return "RGBA4444"
|
||||
7: return "RGBA5551"
|
||||
8: return "RF"
|
||||
9: return "RGF"
|
||||
10: return "RGBF"
|
||||
11: return "RGBAF"
|
||||
12: return "RH"
|
||||
13: return "RGH"
|
||||
14: return "RGBH"
|
||||
15: return "RGBAH"
|
||||
16: return "RGBE9995"
|
||||
17: return "DXT1 (BC1)"
|
||||
18: return "DXT3 (BC2)"
|
||||
19: return "DXT5 (BC3)"
|
||||
20: return "RGTC (BC4)"
|
||||
21: return "RGTC (BC5)"
|
||||
22: return "BPTC (BC6H)"
|
||||
23: return "BPTC (BC7)"
|
||||
24: return "ETC"
|
||||
25: return "ETC2 R11"
|
||||
26: return "ETC2 RG11"
|
||||
27: return "ETC2 RGB8"
|
||||
28: return "ETC2 RGBA8"
|
||||
29: return "PVRTC2"
|
||||
30: return "PVRTC4"
|
||||
31: return "ASTC 4x4"
|
||||
32: return "ASTC 8x8"
|
||||
33: return "Max"
|
||||
return "Unknown(" + str(format_id) + ")"
|
||||
|
||||
|
||||
func _bits_per_pixel(format_id: int) -> int:
|
||||
"""Estimate bits per pixel for a given format."""
|
||||
match format_id:
|
||||
0, 2: return 8 # L8, R8
|
||||
1, 3: return 16 # LA8, RG8
|
||||
4: return 24 # RGB8
|
||||
5, 6, 7: return 32 # RGBA8, RGBA4444, RGBA5551
|
||||
8: return 32 # RF
|
||||
9: return 64 # RGF
|
||||
10: return 96 # RGBF
|
||||
11: return 128 # RGBAF
|
||||
12: return 16 # RH (half)
|
||||
13: return 32 # RGH
|
||||
14: return 48 # RGBH
|
||||
15: return 64 # RGBAH
|
||||
16: return 32 # RGBE9995
|
||||
17, 18, 19: return 4 # DXT/BC compressed — 4bpp
|
||||
20, 21: return 4 # BC4/BC5 — 4bpp (BC5 is 8bpp but rare)
|
||||
22: return 8 # BC6H — 8bpp
|
||||
23: return 8 # BC7 — 8bpp
|
||||
24, 25, 26, 27, 28: return 4 # ETC/ETC2 — 4bpp
|
||||
29, 30: return 2 # PVRTC — 2bpp
|
||||
31: return 8 # ASTC 4x4 — 8bpp
|
||||
32: return 2 # ASTC 8x8 — 2bpp
|
||||
return 32
|
||||
|
||||
|
||||
func _format_bytes(bytes: int) -> String:
|
||||
"""Format bytes into human-readable string."""
|
||||
if bytes < 1024:
|
||||
return str(bytes) + " B"
|
||||
elif bytes < 1024 * 1024:
|
||||
return str(stepify(float(bytes) / 1024.0, 0.1)) + " KB"
|
||||
else:
|
||||
return str(stepify(float(bytes) / (1024.0 * 1024.0), 0.1)) + " MB"
|
||||
|
||||
|
||||
func maxi(a: int, b: int) -> int:
|
||||
return a if a > b else b
|
||||
|
||||
|
||||
func mini(a: int, b: int) -> int:
|
||||
return a if a < b else b
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print final summary."""
|
||||
print("==========================================")
|
||||
print(" TEXTURE VALIDATION SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" Textures checked: ", _total_textures)
|
||||
print(" Materials checked: ", _total_materials)
|
||||
print(" Over-budget textures: ", _overbudget_textures.size())
|
||||
print(" Missing mipmaps: ", _no_mipmap_textures.size())
|
||||
print(" Meshes w/o UV2: ", _meshes_without_uv2.size())
|
||||
print("------------------------------------------")
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Validation FAILED — ", _fail_count, " texture check(s) failed.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
Reference in New Issue
Block a user