Jump to content

Simple Outline/Highlight effect


photo

Recommended Posts

Hello there,

i want to create an outline effect for objects in my game, i got some help already but i do not have the desired effect yet.

right now i have this:
image.png.4b946de1abe1d7f1990a7508bbd223f3.png

as you can see it is indeed an outline but it looks more like a glow.

what i want is an outline like when you select an object in the editor (i only want the outer lines):
image.png.9be49664910f77bea1e5ab18259dc9f6.png

i followed some instructions from somebody in discord and i had to copy these files from the SDK:
image.png.7b8b1f65bceefa75afc09ed4ce4039d2.png

image.png.7871800b8affe76bb95f55f9b3f64e00.png

it works to get the glow effect and all the code/calculation happens in 'filter_selection_sample.frag'

i also found the 'Scriptable Materials' (Scriptable Materials - Documentation - Unigine Developer) guide to use the 'sobel' post process effect but i can only apply it to the whole world or the camera and not on a specific object (at least i dont know how to do this yet)

image.png.9ffde9b0ea2f7b01bc73465ee4a4aa61.png

this makes a nice 'cartoon' effect but i just want to highlight specific objects and i also want to control the color.

my idea was to take the 'filter_selection_frag' and turn it into a .basemat but i have no idea how i can do this.

this is the 'filter_selection_frag' code:
 

#include <core/shaders/common/fragment.h>

INIT_TEXTURE(0,TEX_COLOR)
INIT_TEXTURE(1,TEX_AUXILIARY)
INIT_TEXTURE(2,TEX_SELECTION)

STRUCT(FRAGMENT_IN)
	INIT_POSITION
	INIT_IN(float2,0)
	INIT_IN(float3,1)
END

MAIN_BEGIN(FRAGMENT_OUT,FRAGMENT_IN)
	
	float2 uv = IN_DATA(0).xy;
	
	float4 auxiliary = TEXTURE_BIAS_ZERO(TEX_AUXILIARY,uv); 
	
	float4 color = TEXTURE_BIAS_ZERO(TEX_COLOR,uv); 
	float4 selection = TEXTURE_BIAS_ZERO(TEX_SELECTION,uv); 
	
	OUT_COLOR = color + saturate(selection - auxiliary) * 0.250f;
	
MAIN_END

so i want to achieve 2 things:

  • make a post-process effect that outlines objects 
  • turn it into a material so i can apply it to any object i desire

the only problem is that i don't understand how it all works because the code given in the Scriptable Materials is similiar to the .frag file but somehow i cannot make the 'filter_selection_frag' work. here was my failed attempt:

BaseMaterial post_x <options_hidden=1 preview_hidden=1 var_prefix=var texture_prefix=tex>
{
// specify an internal texture to be used
Texture src_color <type=procedural internal=true>

// define the material's states and parameters available in the Editor
Group "Sobel operator"
{
	State multiply = false
	Slider threshold = 1.0 <min=0.0 max=1.0>
	Slider scale = 1.0 <min=0.0 max=1.0>
}

// define the outline render pass
Pass render_sobel_operator
{
	// write the fragment shader in UUSL or HLSL/GLSL
	Fragment=
	#{
		#include <core/shaders/common/fragment.h>
		
		MAIN_BEGIN(FRAGMENT_OUT, FRAGMENT_IN)
	
	float2 uv = IN_DATA(0).xy;
	
	float4 auxiliary = TEXTURE_BIAS_ZERO(TEX_AUXILIARY,uv); 
	
	OUT_COLOR = auxiliary;
	
		MAIN_END
	#}
}

// the expression in Unigine Script defines a callback 
Expression RENDER_CALLBACK_END_POST_MATERIALS = 
#{
	// declare the source texture from the screen frame
	Texture source = engine.render.getTemporaryTexture(engine.render_state.getScreenColorTexture());
	// define the source texture from the screen frame
	source.copy(engine.render_state.getScreenColorTexture());
	//set the color source texture to use it in the shader
	setTexture("src_color", source);
	// render the outline result texture to output it to the screen 
	renderPassToTexture("render_sobel_operator", engine.render_state.getScreenColorTexture());
	// release the temporaty texture
	engine.render.releaseTemporaryTexture(source);
#}
}

i basically combined the 'post_sobel' code with the 'filter_selection_sample.frag' but i didnt get anywhere..

i hope someone can help me.

Link to comment

Hi, Kireita.

It's would be a lot helpful for you to check out some shader tutorials to better understand shaders, but i am gonna try my best to tell you how this one works.

How it works:
1) Since we using aux buffer for our mask we already know what objects we want to have this effect and it makes our life a bit easier
2) Sample current pixel color
3) Calculate edge value by using Sobel operator
4) Apply selection color based on our previously created mask and calculated edge values


How to use it:
1) You want to have aux state enabled in your mesh base or any other material that supports rendering into aux bufferimage.png

2) Select some aux color in parameters tab. This shader uses only red channel of the aux buffer, so your color must be some kind of red, you can use other 3 channels as you wish.

We use aux buffer to create some sort of mask for our materials, so it will be quite easy for our simple post processing effect

3) Add scriptable material to your camera or globally in settings -> Runtime -> World -> Render -> Scriptable Materials
4) Tweak some parameters and your done!

image.png

// More information on ULON format - https://developer.unigine.com/docs/code/formats/ulon_format
// More information on scriptable materials - https://developer.unigine.com/docs/content/materials/scriptable
BaseMaterial post_selection_example <options_hidden=1 preview_hidden=1 var_prefix=var texture_prefix=tex>
{
	// Texture to be used in a shader (see Fragment shader below). Will be displayed in the Editor UI.
	Texture2D screen_texture <type=procedural internal=true>
	Texture2D auxiliary <type=auxiliary internal=true>
	
	// Custom parameter to be used in a shader (see Fragment shader below). Will be displayed in the Editor UI.
	Color selection_color = [0.5 0.5 0.5 1.0]
	Slider threshold = 1.0 <min=0.0 max=1.0>
	Slider distance = 1.0 <min=0.0 max=5.0>
	
	// Declaration of a custom rendering pass named 'selection_pass'. You can add as many passes as necessary.
	Pass selection_pass
	{
		// Shader declaration (you can add multiple shaders for a single pass)
		Fragment =
		#{
			#include <core/shaders/common/fragment.h>
			
			MAIN_BEGIN(FRAGMENT_OUT, FRAGMENT_IN)
				
				// Get scene color
				float4 color = TEXTURE_BIAS_ZERO(tex_screen_texture, IN_UV);
				
				// get the inverse resolution of the viewport (1.0 / width, 1.0 / height)
				float2 offset = s_viewport.zw * var_distance;
				
				// take 3x3 samples
				OUT_COLOR = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV).r;
				
				float c0 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2( 1, 0)).r;
				float c1 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2( 0, 1)).r;
				float c2 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2( 1, 1)).r;
				float c3 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2( 1, -1)).r;
				
				float c4 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2(-1, 0)).r;
				float c5 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2( 0, -1)).r;
				float c6 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2(-1, -1)).r;
				float c7 = TEXTURE_BIAS_ZERO(tex_auxiliary, IN_UV + offset * float2(-1, 1)).r;
				
				// find edge with Sobel filter
				float sobel_x = c6 + c4 * 2.0f + c7 - c3 - c0 * 2.0f - c2;
				float sobel_y = c6 + c5 * 2.0f + c3 - c7 - c1 * 2.0f - c2;
				float sobel = sqrt(sobel_x * sobel_x + sobel_y * sobel_y);
				
				// apply threshold
				float edge = saturate(1.0f - dot(sobel, to_float3(var_threshold)));
				
				// Choose between scene and selection colors
				OUT_COLOR = lerp(color, var_selection_color, OUT_COLOR * (1.0f - edge));
				
			MAIN_END
		#}
	}
	
	// Expression written in Unigine Script to be called after the specified stage of the rendering sequence 
	// the name should be the same as the corresponding callback of the Render class
	// (here RENDER_CALLBACK_END_POST_MATERIALS to execute it after rendering Post Materials)
	Expression RENDER_CALLBACK_END_POST_MATERIALS =
	#{
		// Get the screen color texture rendered for the current frame
		Texture screen_texture = engine.render_state.getScreenColorTexture();
		
		Texture source = engine.render.getTemporaryTexture(screen_texture);
		source.copy(screen_texture);
		
		// Set the source texture to be used as the 'screen_texture' in the shader
		setTexture("screen_texture", source);
		
		// Render the pass named 'selection_pass' and put the result to the screen color texture of the current frame
		renderPassToTexture("selection_pass", screen_texture);
		
		// Release the temporary texture
		engine.render.releaseTemporaryTexture(source);
	#}
}

One of the downsides of this naïve approach is: if we place some polygons near each other we have this effect where we don't have outline of the all geometric surfaces, but only outer one. In case you need to account for that use depth buffer to account for any depth changes, while you sample edges using sobel operator.
Also it doesn't account for transparent materials, it would require another depth sample to check depth values and early exit shader if our object is behind transparent geometry.

image.pngimage.png

Kind regards, Kirill.

  • Like 2
Link to comment

thank you @sweetluna I will try this when I get home.

just for clarification why do you say it is a 'naïve' approach? is there a better way to do this?
my purpose is simple highlight objects when the worldtrigger detects you are in range. and i was looking around and unfortunately there is little to be found on this topic... at least for me.

also can you give me some pointers as to where I can find tutorials on shaders? or at least the shaders that can be used in UNIGINE? i would love to learn the math or logic behind it.

Link to comment

Unigine supports both GLSL and HLSL shaders, but it would be better for you to write them using UUSL, so they will work on both Linux and Windows without any need of modification. 

Currently engine supports Vertex, Control, Evaluate, Geometry, Fragment and Compute shaders. And almost any shader tutorial can be adopted for use in Unigine.

You might find this links helpful in your shader journey:
1) https://learnopengl.com/ - This site is mostly about opengl but it covers some part of the shader basics with good explanations
2) https://thebookofshaders.com/ - Some more explanations of shaders
3) https://www.shadertoy.com/ - A site where you can experiment with some shaders
4) https://iquilezles.org/www/index.htm - More advanced shader articles
Or just google for "Shader tutorial" or something like that.

As for "naïve approach" statement there is indeed might be some technique that will be a lot better, but mostly for your applications this could be an overkill. Also you can upgrade or change this implementation to any other at any time.

Kind regards, Kirill.

  • Like 1
Link to comment
27 minutes ago, sweetluna said:

Unigine supports both GLSL and HLSL shaders, but it would be better for you to write them using UUSL, so they will work on both Linux and Windows without any need of modification. 

Currently engine supports Vertex, Control, Evaluate, Geometry, Fragment and Compute shaders. And almost any shader tutorial can be adopted for use in Unigine.

You might find this links helpful in your shader journey:
1) https://learnopengl.com/ - This site is mostly about opengl but it covers some part of the shader basics with good explanations
2) https://thebookofshaders.com/ - Some more explanations of shaders
3) https://www.shadertoy.com/ - A site where you can experiment with some shaders
4) https://iquilezles.org/www/index.htm - More advanced shader articles
Or just google for "Shader tutorial" or something like that.

As for "naïve approach" statement there is indeed might be some technique that will be a lot better, but mostly for your applications this could be an overkill. Also you can upgrade or change this implementation to any other at any time.

Kind regards, Kirill.

thank you master i shall go into the journey of 'learning shaders' and thou shall be proud of me XD.

but in all seriousness thank you so much to take the time to respond me and give me some solid materials to go through.

right now its important for me to look for the option that gives most performance so i don't mind 'overkill' as long as it gives the 'best performance'

if you have 1 last advice i would gladly take it. for now i will try your solution when i get back home and post the result.

 

again... THANK YOU!

  • Like 1
  • Thanks 1
Link to comment
8 minutes ago, Kireita said:

right now its important for me to look for the option that gives most performance so i don't mind 'overkill' as long as it gives the 'best performance'

You won't win that much from any other technique that I can remember. But you can also try experimenting with fewer samples and using depth buffer to account for surface changes.
 

11 minutes ago, Kireita said:

if you have 1 last advice i would gladly take it

Shaders are fun, if you know how and when to use them! Just don't stop and try your best to learn more about them. I hope you will continue this quite demanding but really fun journey!

Kind regards, Kirill.

  • Like 1
Link to comment

@sweetluna thank you so so much! i was finally able to do it! 

1123.gif.810ea13fd0127c731914f7cf8f8ffea2.gif

basically i added the material that you made for me to the 'scriptable materials' in the 'settings'

image.thumb.png.84caaa6cb0a3e9076349dc5dc4df2ac9.png

followed the 'worldtrigger' instructions video from How To Use World Triggers to Detect Nodes by Their Bounds - Quick How-To Guides - UNIGINE Developers Community
image.png.991d67291f6ddf0a8dc3477fe35132ff.png

and made this code:

using System.Linq.Expressions;
using System.Runtime.Versioning;
using System.Reflection;
using System.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "763bf6332c8dc5f6518804dd0bdc4098e732cb17")]
public class ItemHighlightTrigger : Component
{
    WorldTrigger worldTrigger;

    private void Init()
    {
        // write here code to be called on component initialization
        worldTrigger = node as WorldTrigger;

        Unigine.Console.Run("show_visualizer 1");
		Unigine.Console.Run("render_auxiliary 1");
        
		// add the enter callback to be fired when a node enters the world trigger
        if (worldTrigger != null)
        {
            worldTrigger.AddEnterCallback(EnterCallback);
            worldTrigger.AddLeaveCallback(LeaveCallback);
        }
    }

    private void Update()
    {
        // write here code to be called before updating each render frame
        node.RenderVisualizer();
    }

    void EnterCallback(Node target)
    {
        ObjectMeshDynamic kees = target as ObjectMeshDynamic;
        kees.SetMaterialState("auxiliary", 1, 0);
    }

    void LeaveCallback(Node target)
    {
        ObjectMeshDynamic kees = target as ObjectMeshDynamic;
        kees.SetMaterialState("auxiliary", 0, 0);
    }
}

 

attached the class to the worldtrigger:
image.png.29bc28e18454e9624798e7498c539c03.png

 

and voila! we have a simple highlighter when you are close to an object :D

thank you so much! @sweetluna

TOPIC CLOSED!

 

Edited by Kireita
  • Like 1
Link to comment

@sweetluna just a last question.

 

how can i make the color changeable? right now its hard coded to be the color picked in the setting, but i want to make the color changeable 

for example:

image.png.7f26b7ca5b25f59aa76c8fd10c4b7413.png
 

here i want to change the object to a green outline (lets say because its a rare item)

i want to be able to control the 'Selection Color' externally.

image.png.ac08fdd7d630c19ad1430b6d4f45ece6.png

i only know that in the code it is this part that controlls it:
image.png.52575de6c9cc439c788a094a613e741a.png

but i dont know how i could control it in code or make it invisible because if i combine the 2 colors it does kind of combine them
image.thumb.png.fffb3b9ff58d3c7644665c4f6241167f.png

it looks like it literally combines the 2 colors and i just want it to take into account 1 color which is the 'Auxiliary Color' of the object itself.

any ideas? i tried to cheat on the code you send me and i tried:
OUT_COLOR = lerp(color, color, OUT_COLOR * (1.0f - edge));

OUT_COLOR = lerp(var_selection_color, var_selection_color, OUT_COLOR * (1.0f - edge));

OUT_COLOR = OUT_COLOR * (1.0f - edge));

OUT_COLOR = lerp(0, 0, OUT_COLOR * (1.0f - edge));

but it doesn't work of course :(

any advice?

 

Link to comment

Since post_selection is scriptable material and you have applied it to the global scriptable material slot, then you have to modify the value accordingly:

Render.GetScriptableMaterial(0).SetParameterFloat4("selection_color", vec4.CYAN);

Don't forget that GetScriptableMaterial returns material in slot 0, you have to check if it's exists and if it's the actual material that you need. You can also make enum like so:

public enum ScriptableMaterials
{
	Selection, 
}

public override bool Init()
{
	// Write here code to be called on world initialization: initialize resources for your world scene during the world start.
	Render.GetScriptableMaterial((int)ScriptableMaterials.Selection).SetParameterFloat4("selection_color", vec4.CYAN);

	return true;
}

Just don't forget to keep order in the editor and in enum the same.

Also, since you can apply scriptable material to the camera, just use Camera class instead of Render to reference Camera's materials.

Link to comment
×
×
  • Create New...