Automatically loading 2D normal maps from file paths - normal not appearing

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By jarlowrey
:warning: Old Version Published before Godot 3 was released.

I’ve gotten normals working as described in this article - http://jamesrlowrey.com/blog/2d-normal-godot.html

I wanted to automate the process so I won’t have to manually create every sprite node in the editor, attach a shader, attach the shader param’s normal texture, etc.

So I attach the following script to a sprite and attempt to load the normal map from it’s texture, but it’s not working

  func create_normal_map(normal_png_path):
var shader = Shader.new()
var shader_fragment_code = "uniform texture normal;/*normal maps expect Y-up, but 2D is Y-down, so must mirror this.*/NORMAL = tex(normal,UV).rgb * vec3(2.0,-2.0,1.0) - vec3(1.0,-1.0,0.0);"
shader.set_code("",shader_fragment_code,"")

var material = CanvasItemMaterial.new()
material.set_shader(shader)
material.set_shader_param("Normal",load(normal_png_path))
return material


  func _ready():
var file = File.new()
var texture_name = get_texture().get_name()
var texture_minus_ext = texture_name.split(".")[0]
var normal_map_png_path = normals_path + texture_minus_ext + "_n.png"
if file.file_exists(normal_map_png_path):
			var normal_material = create_normal_map(normal_map_png_path)
			print(normal_map_png_path, normal_material)
			add_child(normal_material)
pass

The png files definitely all have the right paths, but the normal is not appearing on top of the editor’s sprite when the game is ran. Any ideas?

Edit: Just noticed that the line material.set_shader(shader) is throwing the error: Condition 'p_shader.is_valid() && p_shader->get_mode() != Shader::MODE_CANVAS_ITEM' is true.

Edit: Doing this all through scripting was a bad idea. @eons showed me I can save the material and/or shader to .tres files and load them whenever needed. This is awesome. However I realized it might be even easier to create a “LitSprite” node with the (not saved to .tres files) material/shader defined on it with a little script for setting the shader’s “Normal” parameter when the sprite is instanced or changes texture. Then instance the LitSprite node instead of a normal Sprite.

However, it is still not working. In the remote inspector the Shader param is null despite the whole code path being walked and the normal file path being correct (verified thru print statements). Code below

extends Sprite

func set_texture(texture_path):
.set_texture(texture_path) #call super method
set_normal_map()

func set_normal_map():
var texture_name = get_texture().get_name()
var name_minus_extension = texture_name.split(".")[0]
#load the normal map if it exists
var normal_map_png_path = get_normal_map_file_path(name_minus_extension)
if File.new().file_exists(normal_map_png_path):
	get_material().set_shader_param("Normal", load(normal_map_png_path))

func get_normal_map_file_path(file_name):
#maybe you want to do some complicated stuff if multiple textures can share a normal map, or vice-versa
return "res://assets/normal_maps/" + file_name + "_n.png"

func _ready():
set_normal_map()
# Called every time the node is added to the scene.
# Initialization here
pass

I tried using get_canvas_item().set_material(normal_material) but that results in error

Nonexistent function ‘set_material’ in base ‘RID’.

Edit: set_material(normal_material) doesn’t crash, but the map still does not show up

jarlowrey | 2017-04-20 17:59

Check on the remote inspector to see if the material and shader are there.

Also, try to create a material with the editor and only add a shader to reduce some steps, then try the same with the material too if the other part worked.


Materials and shaders can be saved as resource files too, btw, it would reduce a lot of code.

eons | 2017-04-20 18:30

That would be awesome, how can we save materials/shaders? I didn’t see any options like that in the editor and my google-fu has failed me. Is this the remote inspector you’re referring to - http://codetuto.com/2016/06/godot-engine-remote-debug-live-edit/ ?

jarlowrey | 2017-04-20 18:51

All the things that are Resource can be saved in the editor, on the Inspector go right or use edit on the dropdown menu, the inspector will open to edit the resource and the save and load buttons will be enabled on it (on the top of the panel), there you can save it as a tres file, then load or preload it in a script or another scene anytime.


The remote inspector is the one inside the debugger, while running it will show the current scene tree and let you see all the Nodes and Resources data.

eons | 2017-04-20 20:38

Can’t believe I didn’t see that, thank you so much (x2)!

jarlowrey | 2017-04-20 21:58

@eons, how about a LitSprite node with the material + shader defined on it, and the shader param modified thru a gdscript? I updated the question with my current setup.

jarlowrey | 2017-04-21 01:45

mmm… the uniform is Normal or normal?

eons | 2017-04-21 04:00

It works! I swear I tried all different capitalizations of “normal” and they weren’t working, I turned up the light energy and saw “normal” worked! Thanks @eons!

jarlowrey | 2017-04-21 13:27

The inspector capitalizes all properties labels to make them look prettier, I don’t like that…
Just stick to your naming conventions and don’t trust the inspector ¬_¬

eons | 2017-04-21 15:23

:bust_in_silhouette: Reply From: jarlowrey

@eons was a big help and we solved it in the comments. Full scripting was a bad idea, instead the material or the scene should be saved and reused. I opted to create a scene called “LitSprite” that only has Sprite node inside it with a normal map material/shader. There is also the following script attached, which will fill in the sprite’s shader param with the correct normal map png texture.

extends Sprite

func set_texture(texture_path):
.set_texture(texture_path) #call super method
set_normal_map()

func set_normal_map():
var texture_name = get_texture().get_name()
var name_minus_extension = texture_name.split(".")[0]
#load the normal map if it exists
var normal_map_png_path = get_normal_map_file_path(name_minus_extension)
if File.new().file_exists(normal_map_png_path):
	get_material().set_shader_param("normal", load(normal_map_png_path))

func get_normal_map_file_path(file_name):
#maybe you want to do some complicated stuff if multiple textures can share a normal map, or vice-versa
return "res://assets/normal_maps/" + file_name + "_n.png"

func _ready():
set_normal_map()
# Called every time the node is added to the scene.
# Initialization here
pass