- 수정됨
[godot] Custom outline shader question
By default, the shader outlines every slot that has enough white space in the mesh. I can use SpineSlotNode to then manually reset some slot materials to default so they're not outlined, which is very involved, complicates other material setups and doesn't always produce a satisfying result. Could we perhaps get something akin to RenderExistingMesh in Node form to make this simpler?
Sorry, I'm not familiar with RenderExistingMesh
. Could you link to some documentation on it? I also don't know what outline shader you are using. Please provide more info on that as well.
RenderExistingMesh is a Unity component that ships with Spine Unity URP shaders package. (or Spine Examples, I don't recall)
In URP the Outline has to be a separate GameObject.
To add an outline to your SkeletenRenderer:
1) Add a child GameObject and move it a bit back.
2) Add aRenderExistingMesh
component, found in latestSpine Examples
3) Copy the original material, add _Outline to its name and set the shader toUniversal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly
.
4) Assign this _Outline material at theRenderExistingMesh
under Replacement Materials.
Since no multipass shader exists, the outline has to be rendered separately from the main mesh. RenderExistingMesh mimics everything the main mesh is doing, such as animation, while also rendering only the outline for performance reasons.
I'm new to Godot and shader scripting, but I was wondering if something similar could be achieved in Godot runtimes as well with a specialized node that mimics everything the parent mesh is doing and then can swap to an outline only shader, which I have no clue how to write just yet. This theoritcal node could also be used to display other custom shader effects without messing with the main mesh setup with the main benefit being that you don't have manually sync animations between skeleton instances.
I grabbed the outline shader from here: https://godotshaders.com/shader/higher-detail-outline-shader/
Docs entry for RenderExistingMesh: spine-unity Runtime Documentation: RenderExistingMesh
Ahhh, I see. spine-unity and spine-godot are nothing alike, especially when it comes to rendering. Sadly, I can't just transplant the mechanism found in spine-unity.
A SpineSprite
in Godot actually has a bunch of child Mesh2DInstance
s, one per slot. The meshes can not be merged into a single mesh like in Unity for various reasons out of our control. It will be pretty much impossible getting a closed outline by selectively disabling/enabling the outline shader you found on individual slots (and thus their corresponding Mesh2DInstances
).
I haven't looked into Godot shaders myself too deeply yet, but I think for a proper outline effect that's independent from the skeleton slot arrangement, we'd have to look into post-processing shaders, with the Mesh2DInstance
s likely writing to some buffer which can be used by post-processing. At this point, your guess is as good as mine how this might be achieved. Maybe we could ask on the Godot discord if people more familiar with Godot's shading and post-processing system have an idea how we could do that.
Mario wroteA
SpineSprite
in Godot actually has a bunch of childMesh2DInstance
s, one per slot. The meshes can not be merged into a single mesh like in Unity for various reasons out of our control. It will be pretty much impossible getting a closed outline by selectively disabling/enabling the outline shader you found on individual slots (and thus their correspondingMesh2DInstances
).
I don't think the current Unity approach is merging meshes either, this is the outline only shader in Unity and is the latest recommended approach of handling outlines:
It just renders the outline as a separate GameObject or Node in Godot terms and layers it behind the main skeleton. RenderExistingMesh component then syncs their animations automatically as they're effectively two separate skeleton instances.
I can do something similar, where I copy/paste SpineSprite Node, layer it behind the original mesh and apply the outline shader to it: But I now have to manually keep them in sync and I'm rendering the full mesh twice + outline. I have no idea how batching works on Godot, something to explore for later.
So it would be nice to have a Node that can sync with its parent SpineSprite Node so even if I'm not saving any rendering workload, I can easily set up outline and other custom effects for duplicate SpineSprite Nodes. But now that I think about it, I can probably inherit SpineSprite and do it myself, albeit less efficiently than a native node could.
Mario wroteI haven't looked into Godot shaders myself too deeply yet, but I think for a proper outline effect that's independent from the skeleton slot arrangement, we'd have to look into post-processing shaders, with the
Mesh2DInstance
s likely writing to some buffer which can be used by post-processing. At this point, your guess is as good as mine how this might be achieved. Maybe we could ask on the Godot discord if people more familiar with Godot's shading and post-processing system have an idea how we could do that.
I'll see if some Godot gurus can help with this.
IIRC the Unity approach does actually just use the computed mesh(es) from the "main" entity. In Godot, such an approach doesn't work, because instead of directly submitting meshes, we have to go through creating Mesh2DInstance
objects behind the scene, and then setting their triangle soups via VisualServer
. See:
https://github.com/EsotericSoftware/spine-runtimes/blob/4.1/spine-godot/spine_godot/SpineSprite.cpp#L501
When constructing the triangle soups, the soups are temporary. SpineSprite would have to keep them around for another node to access them and set them on their own Mesh2DInstance
children.
I'd be surprised if that is the most efficient way. My gut tells me rendering to an offscreen buffer, then post-processing that with an adequat outline shader is likely better.
Also please note that the visual quality of an actual post-processing based outline effect would be superior to the one provided in the Spine outline shaders, which have their limitations due to determining the outline alpha value based on just 8 samples in one pass. This leads to artifacts at sharp angled corners of less than 90 degrees. A post processing outline effect, which could sample and paint pixels in multiple (inexpensive) iterations (growing an outline outwards) would not have such artifacts.
Mario wroteIIRC the Unity approach does actually just use the computed mesh(es) from the "main" entity. In Godot, such an approach doesn't work, because instead of directly submitting meshes, we have to go through creating
Mesh2DInstance
objects behind the scene, and then setting their triangle soups viaVisualServer
. See:
https://github.com/EsotericSoftware/spine-runtimes/blob/4.1/spine-godot/spine_godot/SpineSprite.cpp#L501When constructing the triangle soups, the soups are temporary. SpineSprite would have to keep them around for another node to access them and set them on their own
Mesh2DInstance
children.I'd be surprised if that is the most efficient way. My gut tells me rendering to an offscreen buffer, then post-processing that with an adequat outline shader is likely better.
Noted, I have a lot of reading and learning ahead but it's fun to dig into this.
Looks like I solved it using Godot's Viewport node.
I'm really starting to love the simplicity of Godot. And the few artifacts should be fixable with a better shader. Post processing indeed seems to be the better direction to explore.
Hmm, one limiting factor of Viewport approach is that due to SpineSlot and SpineBone Node hierarchical expectations, everything ends up inside the Viewport. So say I want to add a particle effect to the weapon via SpineSlot Node, that particle has to go inside the viewport and therefore also gets outlined as a child Node of SpineSlot.
It would then be preferable if SpineSlot and SpineBone had an exported SpineSprite property in the inspector instead of enforcing the current Node structure so I could place SpineSlot outside the Viewport.
Workarounds as of this moment I can think of - one would be to have the particle outside the viewport and script it to follow SpineSlot transform in _process. Another would be to have duplicate SpineSprites - one for outline via Viewport, and the other for everything else such as adding slot specific particle effects. Viewports can be stacked and not pass on their contents to parent Viewports but they don't have a transform, so this is a dead end.
To illustrate a common problem:
Since SpineSlot expects to be a child of SpineSprite, Viewport post processing currently is all or nothing deal. Viewports can be stacked, but I can't place only a SlotNode in the Viewport due to current parent/child requirements of SpineSlot.
The issue is that SpineSlotNode
itself doesn't do anything, like rendering. So even if you could parent it to something other than SpineSprite
, the actual attachment of the slot is still rendered as part of the internal SpineSprite
hierarchy that contains Mesh2DInstance
children, one for each attachment. There's currently no way around that I'm afraid. Those are what actual perform the rendering.
For your use case of attaching a particle system to a specific node, there's a better and more flexible solution. You can write your own node in either GDScript or C++ which listens for the world_transforms_changed
signal emitted by SpineSprite
. When you react to that signal, all bones have their transforms updated. You can get a specific bone's transform via SpineSprite.get_global_bone_transform(bone_name)
. Apply that transform to your node's global transform with set_global_transform()
. Any child of your node and the node itself will then follow the bone. And you are free to put that node anywhere you want in your scene hierarchy.
Mario wroteThe issue is that
SpineSlotNode
itself doesn't do anything, like rendering. So even if you could parent it to something other thanSpineSprite
, the actual attachment of the slot is still rendered as part of the internalSpineSprite
hierarchy that containsMesh2DInstance
children, one for each attachment. There's currently no way around that I'm afraid. Those are what actual perform the rendering.For your use case of attaching a particle system to a specific node, there's a better and more flexible solution. You can write your own node in either GDScript or C++ which listens for the
world_transforms_changed
signal emitted bySpineSprite
. When you react to that signal, all bones have their transforms updated. You can get a specific bone's transform viaSpineSprite.get_global_bone_transform(bone_name)
. Apply that transform to your node's global transform withset_global_transform()
. Any child of your node and the node itself will then follow the bone. And you are free to put that node anywhere you want in your scene hierarchy.
Understood and thank you for the detailed workaround.