Expand per-part fur customization

Load authored pattern masks by body component, add custom fur colors and the themed picker UI, and advance the signed profile protocol to version 6.
This commit is contained in:
Alexander Sellite 2026-08-15 14:27:53 -04:00
parent 64c2acb773
commit 84695c473b
27 changed files with 1337 additions and 327 deletions

View file

@ -6,19 +6,23 @@ func _initialize() -> void:
func _run() -> void:
CharacterCustomizationCatalog.refresh_fur_pattern_assets()
var legacy_snapshot := CharacterCustomizationCatalog.default_snapshot()
legacy_snapshot[CharacterCustomizationCatalog.FUR_STYLE_ID] = "spots_bengal"
for added_id: String in [
"fur_style",
"fur_color_2",
"fur_color_3",
"fur_color_4",
CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID,
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID,
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID,
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID,
]:
legacy_snapshot.erase(added_id)
var migrated := CharacterCustomizationCatalog.sanitized_snapshot(
legacy_snapshot
)
assert(CharacterCustomizationCatalog.validate_snapshot(migrated))
assert(migrated["fur_style"] == "solid")
for style_field: String in CharacterCustomizationCatalog.FUR_STYLE_IDS:
assert(migrated[style_field] == "bengal")
var fur_colors := CharacterCustomizationCatalog.options_for("fur_pattern")
assert(fur_colors.size() == 30)
for natural_color_id: String in [
@ -38,72 +42,128 @@ func _run() -> void:
"fur_pattern",
natural_color_id,
))
var custom_color := Color("2b91c7")
var custom_color_id := CharacterCustomizationCatalog.custom_fur_color_id(
custom_color
)
assert(custom_color_id == "custom_2b91c7")
assert(
CharacterCustomizationCatalog.custom_fur_color_id(
Color(custom_color.r, custom_color.g, custom_color.b, 0.2)
)
== custom_color_id
)
for color_field: String in CharacterCustomizationCatalog.FUR_COLOR_IDS:
assert(CharacterCustomizationCatalog.is_valid_option(
color_field,
custom_color_id,
))
assert(
CharacterCustomizationCatalog.option_color(
color_field, custom_color_id
).is_equal_approx(custom_color)
)
assert(not CharacterCustomizationCatalog.is_valid_option(
"fur_pattern", "custom_2b91cg"
))
assert(not CharacterCustomizationCatalog.is_valid_option(
"fur_pattern", "custom_2b91c7ff"
))
var pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal"
)
assert(pattern_texture != null)
assert(pattern_texture.get_width() == 1024)
assert(pattern_texture.get_height() == 1024)
var arm_pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal",
"body_arms",
)
assert(arm_pattern_texture != null)
assert(arm_pattern_texture.get_width() == 1024)
assert(arm_pattern_texture.get_height() == 1024)
var fox_pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
var pattern_ids: Array[String] = []
for option: Dictionary in CharacterCustomizationCatalog.options_for(
CharacterCustomizationCatalog.FUR_STYLE_ID
):
pattern_ids.append(str(option.get("id", "")))
assert(pattern_ids == [
"solid",
"bengal",
"calico",
"fox",
"body_main",
)
assert(fox_pattern_texture != null)
assert(fox_pattern_texture.get_width() == 1024)
assert(fox_pattern_texture.get_height() == 1024)
var fox_arm_pattern_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "body_arms"
)
)
assert(fox_arm_pattern_texture != null)
assert(fox_arm_pattern_texture.get_width() == 1024)
assert(fox_arm_pattern_texture.get_height() == 1024)
var bengal_head_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal", "head_round"
)
)
assert(bengal_head_texture != null)
assert(bengal_head_texture.get_width() == 1024)
assert(bengal_head_texture.get_height() == 1024)
var fox_head_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "head_pointy"
)
assert(fox_head_texture != null)
assert(fox_head_texture.get_width() == 1024)
assert(fox_head_texture.get_height() == 1024)
var fox_round_head_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "head_round"
)
)
assert(fox_round_head_texture != null)
assert(fox_round_head_texture.get_width() == 1024)
assert(fox_round_head_texture.get_height() == 1024)
var fox_tail_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "tails_fox"
)
assert(fox_tail_texture != null)
assert(fox_tail_texture.get_width() == 1024)
assert(fox_tail_texture.get_height() == 1024)
"paws",
"stripes",
"tiger",
"tummy",
])
var patterned := migrated.duplicate(true)
var patterned := CharacterCustomizationCatalog.default_snapshot()
patterned["species"] = "round"
patterned["ears"] = "pointy_short"
patterned["tail"] = "fox"
patterned["fur_pattern"] = "orange"
patterned["fur_style"] = "spots_bengal"
patterned[CharacterCustomizationCatalog.FUR_STYLE_ID] = "calico"
patterned[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "paws"
patterned[CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID] = "tiger"
patterned[CharacterCustomizationCatalog.FUR_STYLE_EARS_ID] = "fox"
patterned[CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID] = "fox"
patterned["fur_color_2"] = "cream"
patterned["fur_color_3"] = "brown"
patterned["fur_color_4"] = "red"
assert(CharacterCustomizationCatalog.validate_snapshot(patterned))
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_ID, patterned
),
["solid", "bengal", "calico", "fox", "paws", "stripes", "tiger", "tummy"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID, patterned
),
["solid", "bengal", "fox", "paws"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, patterned
),
["solid", "bengal", "fox", "tiger"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, patterned
),
["solid", "fox"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, patterned
),
["solid", "fox"],
)
var pointy_snapshot := patterned.duplicate(true)
pointy_snapshot["species"] = "pointy"
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, pointy_snapshot
),
["solid", "fox"],
)
var no_accessories := patterned.duplicate(true)
no_accessories["ears"] = "none"
no_accessories["tail"] = "none"
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, no_accessories
),
["solid"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, no_accessories
),
["solid"],
)
var body_texture := _assert_pattern_texture("calico", "body_main")
var arms_texture := _assert_pattern_texture("paws", "body_arms")
var head_texture := _assert_pattern_texture("tiger", "head_round")
var ears_texture := _assert_pattern_texture("fox", "ears_pointy_short")
var tail_texture := _assert_pattern_texture("fox", "tails_fox")
_assert_pattern_texture("tummy", "body_main")
_assert_pattern_texture("stripes", "body_main")
_assert_pattern_texture("fox", "head_pointy")
var visuals := PlayerVisualPresenter.instantiate_visuals()
root.add_child(visuals)
PlayerVisualPresenter.apply_appearance(visuals, patterned)
@ -121,72 +181,224 @@ func _run() -> void:
uv_max = uv_max.max(uv)
assert(uv_min.x < 0.05 and uv_min.y < 0.05)
assert(uv_max.x > 0.95 and uv_max.y > 0.95)
var material := body.material_override as ShaderMaterial
assert(material != null)
assert(material.shader == preload("res://player/fur_pattern.gdshader"))
assert(material.get_shader_parameter("pattern_texture") == pattern_texture)
assert(material.get_shader_parameter("base_color") == Color("c86c36"))
assert(material.get_shader_parameter("red_zone_color") == Color("e6d39b"))
assert(material.get_shader_parameter("green_zone_color") == Color("7b4a32"))
assert(material.get_shader_parameter("blue_zone_color") == Color("a9433f"))
var arms := skeleton.get_node_or_null("body_arms") as MeshInstance3D
assert(arms != null)
var arm_material := arms.material_override as ShaderMaterial
assert(arm_material != null)
assert(
arm_material.get_shader_parameter("pattern_texture")
== arm_pattern_texture
)
var round_head := skeleton.get_node_or_null("head_round") as MeshInstance3D
assert(round_head != null)
var round_head_material := round_head.material_override as ShaderMaterial
assert(round_head_material != null)
assert(
round_head_material.get_shader_parameter("pattern_texture")
== bengal_head_texture
)
var fox_snapshot := patterned.duplicate(true)
fox_snapshot["species"] = "pointy"
fox_snapshot["fur_style"] = "fox"
fox_snapshot["tail"] = "fox"
PlayerVisualPresenter.apply_appearance(visuals, fox_snapshot)
var pointy_head := (
skeleton.get_node_or_null("head_pointy") as MeshInstance3D
var body_material := _assert_mesh_pattern(
skeleton, "body_main", body_texture
)
assert(pointy_head != null)
var pointy_head_material := pointy_head.material_override as ShaderMaterial
assert(pointy_head_material != null)
assert(
pointy_head_material.get_shader_parameter("pattern_texture")
== fox_head_texture
var arms_material := _assert_mesh_pattern(
skeleton, "body_arms", arms_texture
)
fox_snapshot["species"] = "round"
PlayerVisualPresenter.apply_appearance(visuals, fox_snapshot)
assert(
round_head_material.get_shader_parameter("pattern_texture")
== fox_round_head_texture
var head_material := _assert_mesh_pattern(
skeleton, "head_round", head_texture
)
var fox_tail := skeleton.get_node_or_null("tails_fox") as MeshInstance3D
assert(fox_tail != null)
var fox_tail_material := fox_tail.material_override as ShaderMaterial
assert(fox_tail_material != null)
assert(
fox_tail_material.get_shader_parameter("pattern_texture")
== fox_tail_texture
var ears_material := _assert_mesh_pattern(
skeleton, "ears_pointy_short", ears_texture
)
var tail_material := _assert_mesh_pattern(
skeleton, "tails_fox", tail_texture
)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(material.shader == preload("res://player/fur_pattern.gdshader"))
assert(material.get_shader_parameter("base_color") == Color("c86c36"))
assert(material.get_shader_parameter("red_zone_color") == Color("e6d39b"))
assert(material.get_shader_parameter("green_zone_color") == Color("7b4a32"))
assert(material.get_shader_parameter("blue_zone_color") == Color("a9433f"))
var recolored := patterned.duplicate(true)
recolored["fur_color_3"] = "teal"
PlayerVisualPresenter.apply_appearance(visuals, recolored)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(material.get_shader_parameter("green_zone_color") == Color("3a8790"))
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": recolored})
)
var other_arms := patterned.duplicate(true)
other_arms[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "fox"
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": other_arms})
)
var custom_snapshot := patterned.duplicate(true)
custom_snapshot["fur_pattern"] = custom_color_id
assert(CharacterCustomizationCatalog.validate_snapshot(custom_snapshot))
PlayerVisualPresenter.apply_appearance(visuals, custom_snapshot)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(
(material.get_shader_parameter("base_color") as Color).is_equal_approx(
custom_color
)
)
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": custom_snapshot})
)
PlayerVisualPresenter.apply_appearance(visuals, migrated)
var solid := CharacterCustomizationCatalog.default_snapshot()
PlayerVisualPresenter.apply_appearance(visuals, solid)
assert(body.material_override is StandardMaterial3D)
await _validate_fur_color_ui(patterned)
print("Fur pattern validation: PASS")
visuals.queue_free()
await process_frame
quit()
func _validate_fur_color_ui(snapshot: Dictionary) -> void:
var profile_page := ProfilePage.new()
profile_page.size = Vector2(1280.0, 720.0)
root.add_child(profile_page)
await process_frame
profile_page.set("_draft_appearance", snapshot.duplicate(true))
profile_page.call("_select_category", "fur_pattern")
profile_page.call("_select_fur_section", "colors")
await process_frame
await process_frame
var color_panel := profile_page.find_child(
"FurColorOptionsPanel", true, false
) as PanelContainer
var channel_grid := profile_page.find_child(
"FurColorChannelGrid", true, false
) as GridContainer
var palette_grid := profile_page.find_child(
"FurPaletteGrid", true, false
) as GridContainer
var picker := profile_page.find_child(
"FurCustomColorPicker", true, false
) as ColorPickerButton
var picker_popup := picker.get_popup()
var picker_control := picker.get_picker()
assert(color_panel != null and color_panel.size.x > 360.0)
assert(channel_grid != null and channel_grid.columns == 4)
assert(channel_grid.get_child_count() == 4)
assert(palette_grid != null and palette_grid.columns == 10)
assert(palette_grid.get_child_count() == 30)
for option_button: Button in palette_grid.get_children():
assert(
option_button.custom_minimum_size
== Vector2.ONE * ProfilePage.FUR_PALETTE_SWATCH_SIZE
)
for color_index: int in CharacterCustomizationCatalog.FUR_COLOR_IDS.size():
var category_id: String = (
CharacterCustomizationCatalog.FUR_COLOR_IDS[color_index]
)
var channel_button := profile_page.find_child(
"FurColorSlot%d" % (color_index + 1), true, false
) as Button
assert(channel_button != null)
var swatch := channel_button.get_node(
"ChannelContents/ActiveColorSwatch"
) as Panel
var channel_label := channel_button.get_node(
"ChannelContents/ChannelNumber"
) as Label
assert(swatch.size.is_equal_approx(
Vector2.ONE * ProfilePage.FUR_CHANNEL_SWATCH_SIZE
))
assert(swatch.size.y <= channel_label.size.y)
var swatch_style := swatch.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(swatch_style != null)
assert(
swatch_style.corner_radius_top_left
== roundi(ProfilePage.FUR_CHANNEL_SWATCH_SIZE * 0.5)
)
assert(swatch_style.bg_color.is_equal_approx(
CharacterCustomizationCatalog.option_color(
category_id,
str(snapshot[category_id]),
)
))
var picker_style := picker.get_theme_stylebox("normal") as StyleBoxFlat
assert(picker_style != null and picker_style.corner_radius_top_left == 14)
assert(picker_popup.min_size == ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE)
assert(picker_popup.theme.default_font == UtilityPageStyle.TuffyFont)
var popup_style := picker_popup.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(popup_style != null)
assert(popup_style.bg_color.a == 1.0)
assert(popup_style.corner_radius_top_left == 20)
assert(
picker_control.get_theme_constant("sv_width")
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.x
)
assert(
picker_control.get_theme_constant("sv_height")
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.y
)
assert(not picker_control.edit_alpha)
assert(not picker_control.edit_intensity)
var picker_display := picker.get_node("FurCustomColorDisplay") as Panel
var display_style := picker_display.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(display_style != null and display_style.corner_radius_top_left == 11)
var live_custom_color := Color("2b91c7")
profile_page.call(
"_select_custom_fur_color",
live_custom_color,
CharacterCustomizationCatalog.FUR_COLOR_IDS[0],
)
var first_swatch := profile_page.find_child(
"FurColorSlot1", true, false
).get_node("ChannelContents/ActiveColorSwatch") as Panel
var first_swatch_style := first_swatch.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(first_swatch_style.bg_color.is_equal_approx(live_custom_color))
profile_page.free()
await process_frame
func _assert_option_ids(options: Array, expected: Array[String]) -> void:
var actual: Array[String] = []
for option: Dictionary in options:
actual.append(str(option.get("id", "")))
assert(actual == expected)
func _assert_pattern_texture(
pattern_id: String,
component_id: String,
) -> Texture2D:
var texture := CharacterCustomizationCatalog.fur_pattern_texture(
pattern_id, component_id
)
assert(texture != null)
assert(texture.get_width() == 1024)
assert(texture.get_height() == 1024)
return texture
func _assert_mesh_pattern(
skeleton: Skeleton3D,
mesh_name: String,
expected_texture: Texture2D,
) -> ShaderMaterial:
var mesh := skeleton.get_node_or_null(mesh_name) as MeshInstance3D
assert(mesh != null)
var material := mesh.material_override as ShaderMaterial
assert(material != null)
assert(material.get_shader_parameter("pattern_texture") == expected_texture)
return material