Hello, I 'm importing an animation made using "Tint Black" option in some of slots and I'm having trouble to see them correctly in Godot 3.5. It seems that they don't have assigned that darkcolor. I only managed to see that slots with correct color using one SpineSlotNoe for every Slot that has darkcolor and aplying to it a multiply shader material. It doesnt work the same if I apply that shader to the parent SpineSprite, looks like I have to create an SpineSlotNode for every node that uses the "Tint black" option in spine. Am I correct? Do I have to create a node for every slot that uses darkcolor?
Problems using "dark color" in Godot 3.5
Based on the provided documentation and the nature of your issue, it seems that the spine-godot runtime does not explicitly mention handling the "Tint Black" or "dark color" feature directly within the SpineSprite
or SpineSlotNode
documentation. The spine-godot runtime documentation focuses on setting custom materials for slots and the entire SpineSprite
, manipulating slots via SpineSlotNode
, and other functionalities like mixing and matching skins, 2D lighting with normal maps, and animation control. However, it does not detail how to apply or manage the "Tint Black" feature from Spine animations within Godot.
Given this, your approach of using a SpineSlotNode
for each slot that utilizes the "Tint Black" option and applying a multiply shader material to it seems to be a practical workaround. This method allows you to individually manipulate the appearance of each slot to achieve the desired effect, albeit at the cost of increased setup time and complexity.
In summary, while the spine-godot documentation does not explicitly address handling "Tint Black" or dark colors, your method of applying a shader to each relevant SpineSlotNode
appears to be a valid workaround to achieve the desired visual effect in Godot 3.5. It suggests that, indeed, you may need to create a node for every slot that uses dark color to properly apply and manage this effect.
spine-godot does not currently support tint black. The issue is that we currently can not submit additional vertex attributes to the render server which are needed for the feature. Your solution will sort of work, but will not result in the exact same output. It's also rather tedious I'm afraid.
There's currently sadly no good solution for this problem.
Happy to find this post. I did what the poster above described, but the runtime performance is poor. It's only a problem in our web exports. But it breaks batching to keep switching materials, which increases draw calls substantially. I think 2 colors can be rammed into the 'color' attribute on each vertex. I think you can either do it by encoding 2 colors into 4 floats, or you can do it by sampling the final color from a second texture.
you can do it by sampling the final color from a second texture.
My goal here is to render all of our spine characters with as few draw calls as possible.
To expand on this, here's what I'm thinking:
You each spine mesh with the same material, the material has a texture uniform that contains all the color gradients you want to use. Each row of pixels is a gradient. In the spine mesh's COLOR attributes, you can encode in the r channel with 1.0 or 0.0 for how much to weight the gradient tint, and in the g channel you can encode which of the gradients you want to select. the b and a channels are unused. Then in the shader, you can use COLOR to determine which gradient and how much of it to apply. You select where on the gradient based the red channel of the regular grayscale texture sample.
- Изменено
This is way too complex and is likely to result in bad performance as well, as cache locality goes entirely out the window during texture sampling from the gradient texture. The access would be "random". Additionally, having to maintain an additional "gradient" texture is easy, if you do it as a one off in your own project. It gets much more complex if you provide an extension like we do, where we don't have control over the number of gradient textures that will be needed. This is not a good solution I'm afraid.
The optimal solution is to cram the additional color into a vertex attribute, and have a shader/material that knows how to interpret that. We can't cram "2 colors into 4 floats" because that's not how RenderServer
works. Internally, it converts those 4 floats to a 32-bit RGBA int, each byte encoding a channel. Have a look at the SpineMesh2D
implementation:
EsotericSoftware/spine-runtimesblob/4.1/spine-godot/spine_godot/SpineSprite.cpp#L128
We need to reverse engineer how RenderServer::mesh_create_surface_data_from_arrays
to figure out how to add custom attributes. That entire API isn't documented at all. We currently have a lot of balls to juggle, so we lack the resources to do yet another deep dive into undocumented Godot territory I'm afraid. I'll talk to Remi and see if they can provide either the necessary docs or a new API that makes this trivial.
I have implemented this approach successfully. I can render many spine characters with different tint gradients using 1 draw call. The performance impact was not detectable. Are you referrring to GPU cache locality? Thats a topic beyond my understanding but I believe the GPU cost of rendering these 2D characters is negligable. The cost I am seeing is in Godots processing of the vertex data.
Can you share your code and shader?
Script that merges a bunch of Godot gradients into a single texture
tool
extends EditorScript
const output_file := "res://project_specific/gfx/player_gradients/player_gradients.png"
func _run() -> void:
var merged_image := Image.new()
var texture_width := 32
var texture_height := PlayerGradients.gradients.size()
merged_image.create(texture_width, texture_height, false, Image.FORMAT_RGBA8)
for i in PlayerGradients.gradients.size():
var gradient:Gradient = load(PlayerGradients.gradients[i])
var gradient_texture := GradientTexture.new()
gradient_texture.width = 32
#gradient_texture.height = 1
gradient_texture.gradient = gradient
var gradient_data := gradient_texture.get_data()
merged_image.blit_rect(gradient_data, Rect2(Vector2.ZERO, gradient_data.get_size()), Vector2(0.0, i))
merged_image.save_png(output_file)
get_editor_interface().get_resource_filesystem().scan()
the output:
Singleton that has some helper functions:
extends Node
class_name PlayerGradients
const gradients := [
"res://project_specific/gfx/player_gradients/beige.tres",
"res://project_specific/gfx/player_gradients/blue-pink.tres",
"res://project_specific/gfx/player_gradients/blue.tres",
"res://project_specific/gfx/player_gradients/blue_dark.tres",
"res://project_specific/gfx/player_gradients/brown.tres",
"res://project_specific/gfx/player_gradients/cyan-pink.tres",
"res://project_specific/gfx/player_gradients/cyan-yellow.tres",
"res://project_specific/gfx/player_gradients/green.tres",
"res://project_specific/gfx/player_gradients/indigo-green.tres",
"res://project_specific/gfx/player_gradients/lemon-lime.tres",
"res://project_specific/gfx/player_gradients/lime-lemon.tres",
"res://project_specific/gfx/player_gradients/navy-peach.tres",
"res://project_specific/gfx/player_gradients/notint.tres",
"res://project_specific/gfx/player_gradients/orange.tres",
"res://project_specific/gfx/player_gradients/pink-cyan.tres",
"res://project_specific/gfx/player_gradients/pink.tres",
"res://project_specific/gfx/player_gradients/purple-gold.tres",
"res://project_specific/gfx/player_gradients/purple-teal.tres",
"res://project_specific/gfx/player_gradients/purple.tres",
"res://project_specific/gfx/player_gradients/red-yellow.tres",
"res://project_specific/gfx/player_gradients/red.tres",
"res://project_specific/gfx/player_gradients/rose.tres",
"res://project_specific/gfx/player_gradients/white.tres",
"res://project_specific/gfx/player_gradients/yellow-pink.tres",
"res://project_specific/gfx/player_gradients/yellow.tres",
]
static func index_of_gradient_name(gradient_name:String)->int:
var full_path := "res://project_specific/gfx/player_gradients/" + gradient_name + ".tres"
return gradients.find(full_path)
static func uv_index_of_gradient_name(gradient_name:String)->float:
var index := index_of_gradient_name(gradient_name)
var half_pixel := 0.5 / gradients.size()
return (float(index) / gradients.size()) + half_pixel
static func load_gradient(gradient_name:String)->Gradient:
var full_path := "res://project_specific/gfx/player_gradients/" + gradient_name + ".tres"
return load(full_path) as Gradient
Script on the spine sprite that sets the vertex color of the slots that we want to tint
func apply_gradient_tint():
var slots := get_skeleton().get_slots()
for s in slots:
var slot:SpineSlot = s
var attachment:SpineAttachment = slot.get_attachment()
if not attachment:
continue
var should_tint := attachment.get_attachment_name().find("-tint") != -1
if should_tint:
var y_coordinate := PlayerGradients.uv_index_of_gradient_name(gradient_name)
var tint_color := Color(0.0, y_coordinate, 0.0, 1.0)
slot.set_color(tint_color)
else:
slot.set_color(Color.white)
Shader that uses those vertex coordinates to tint (or not tint) the characters
shader_type canvas_item;
uniform sampler2D gradient;
void fragment() {
vec4 vertex_color = COLOR;
vec4 sample = texture(TEXTURE, UV);
vec2 grad_uv = vec2(sample.r, vertex_color.y);
vec3 grad = texture(gradient, grad_uv).rgb;
COLOR.rgb = mix(sample.rgb, grad.rgb, 1.0 - vertex_color.r);
COLOR.a = sample.a;
}
This shader is used as the 'normal' shader on the spine sprite.
This technique allows us to render all of our spine sprites with gradient tinting and with 1 draw call.
Interesting solution I'm afraid we can't go with a solution like this, as it doesn't cover the entire color space. Other possible issues (haven't tested):
- attribute precision may be insufficient on mobile GPUs for this to work
- texture filtering may cause "bleeding" from neighbouring pixel colors into the sampled color. Since the color is applied to the entire attachment, you'd end up with slightly errornously shaded full attachments.
We're wrapping up Spine 4.2 this week, after which I should have cycles to look into proper two color tint support in spine-godot (along with other changes, such as making spine-godot an extension for Godot 4.x)