About Texture Splatting

Hey, i was been studying how to create a real time game environment from Environment Modeling and Texturing DVD by CG Master Christopher Plush and him use Stencil Maps for Splatting, but i don’t know if jme use stencil maps for splatting, because i seen the

texture splatting doc in wiki. So i would like to know if is possible i create my terrains in blender with stencil maps and load them in jmp ( because this method is very useful ;P). Thanks in advance ;P.

no there is no built in way of doing this.

The only way to do this would be to modify the lighting shader to support stencilMap from blender.

Hmm. Yes it’s true. I found another way easier to do it, by baking the mesh’s textures with stencil map, using the “Textures” bake mode, generating so a new color texture ( perfectly equals the result of stencil map ) . It’s very useful, do you agree?

Yes can be good for small areas or models, but the problem in baking is that you will loose resolution on your color map.

when using stencil maps in blender the idea is that the associated color map is a relatively small map, but repeated n time.

For example, if you use a 512 x 512 color map repeated 10 times, you virtually have a 5120 x 5120 color map

If you bake the resulting color map after the stencil map has been applied, you’ll have to bake it in a smaller map, usually 1024 x 1024 or 2048 x 2048. this will result in a loss of resolution.



Also usually stencil map can be 1024 x 1024 maps, because you don’t need much details. and in the end a 512 map + a 1024 map use a lot less memory than a 2048 map.



On a side note, a similar idea is used for the terrain material in jME3, but instead of stencil map an alpha map is used.

Stencil map are black and white, so using an rbga image to store the data is a waste because it could be store in a single channel instead of 4.

The alpha map, store a different stencil map on each of its channel. So an alpha map can control up to 4 colormaps.



What could be nice, would be to make a blender script that would combine 4 stencil maps in an alpha map that could be used with the terrain material…

1 Like

Hm it should be possible with a shader am i wrong? Input the stencil map + the two textures you want to splat, and the rest is done by the shader.



→ I assume currently you use the whole texture once none repeating ?

The shader would allow you to repeat it (if written correctly) and makes your texture resolution independent of the terrain/object size.

1 Like
nehon said:
What could be nice, would be to make a blender script that would combine 4 stencil maps in an alpha map that could be used with the terrain material...


Yes, this realy could be nice. I haven't experience with terrains in jme yet, and I still didn't used alpha map combined with color maps in jme. So when I learn to do it, will try to create a blender script as you said ;P. I know i can do it, i have about 60 gb of blender tutorials ;P.
1 Like

@glaucomardano @nehon Is there actually an easy way to do that?, I would like to use blender to apply more than one texture to the same mesh (making terrains). The only way I can see is using the stencil mode, is there another way?, or is that script you talk about already made?

@NemesisMate said: @glaucomardano @nehon Is there actually an easy way to do that?, I would like to use blender to apply more than one texture to the same mesh (making terrains). The only way I can see is using the stencil mode, is there another way?, or is that script you talk about already made?
This topic is quite old by now...

You should be able to achieve splatting in Blender by using their new material node system, however you will still need to create the material manually when you import into jME3.

@Momoko_Fan said: This topic is quite old by now...

You should be able to achieve splatting in Blender by using their new material node system, however you will still need to create the material manually when you import into jME3.

That’s just what I wanted to avoid, the need of creating manually the material in JME3.

@NemesisMate said: That's just what I wanted to avoid, the need of creating manually the material in JME3.
You would be hard pressed to find any game engine that can seamlessly import materials from modelling tools. The material systems in those tools are designed for non real-time ray traced rendering used for CG pictures and movies, so they are vastly more complex than what is typically found in a game engine.
@Momoko_Fan said: You would be hard pressed to find any game engine that can seamlessly import materials from modelling tools. The material systems in those tools are designed for non real-time ray traced rendering used for CG pictures and movies, so they are vastly more complex than what is typically found in a game engine.

I know that but it’s also possible to make an importer adapt some functions to the engine system what is not implemented yet in this case in JME3 as I can see. I’ll manage to get it work just like you said, creating the material in JME3, till I find an automated external way (I’m trying to get a “just import” to have a scene working).

Thanks for the info.

Maybe I can provide a headstart for you. I have a script which will setup a node based material for splatting.

The splat map can then be painted on by the artist by changing the rgba of the splat map within blender.

it is based on an old jme shader written aeons ago for 4x splatting.

I have also a script somewhere which writes the node setup back to a jme material file.

I’ll post the stuff when I get back home.

some documentation on what you can do can be found here: https://code.google.com/p/l2jserver-client/wiki/Painting_terrain_in_blender

this one is the file to add the splat material to an object

#----------------------------------------------------------
# File addSplattMaterial.py
#----------------------------------------------------------

bl_info = {
	'name': 'Add Splatt Materials',
	'author': 'tom [et] 3d-inferno [point] com',
	'version': (0, 0, 1),
	'blender': (2, 6, 5),
	"location": "View3D > Specials > Add 4x splatting",
	'description': 'Add Shadernodes for 4x texture splatting to a mesh, be sure to switch to glsl display',
	'warning': '',
	'wiki_url': '',
	'tracker_url': '',
	"support": 'COMMUNITY',
	"category": "Object"}

import bpy

def add_image_texture(name):
	tex = bpy.data.textures.new(name, type = 'IMAGE')
	return tex

def add_splat_material(name, texture, x, y, z) :
	mat = bpy.data.materials.new(name)
	mat.specular_intensity = 0
	mat.use_mist = False
	ts = mat.texture_slots.add()
	ts.texture = texture
	ts.texture_coords = 'UV'
	#ts.specular_factor = 0
	ts.scale = x, y, z
	return mat

#make sure the display of the 3d view is set to glsl
def do_4x_splatting(ob) :
	#new material based on nodes
	mat = bpy.data.materials.new('SplattMaterial')
	mat.specular_hardness = 0
	mat.use_mist = False
	mat.use_nodes = True
	#Get the shaderNodeTree
	tree = mat.node_tree
	links = tree.links
	# clear default nodes
	for n in tree.nodes:
		tree.nodes.remove(n)
	#splat material
	splat = tree.nodes.new('MATERIAL')
	splat.name = 'Splat'
	splat.location = 216.3437, 781.6590
	dif1 = tree.nodes.new('MATERIAL')
	dif1.name = 'SplatDif01'
	dif1.location = 358.4370, 504.2595
	dif2 = tree.nodes.new('MATERIAL')
	dif2.name = 'SplatDif02'
	dif2.location = 643.1497, 505.2844
	dif3 = tree.nodes.new('MATERIAL')
	dif3.name = 'SplatDif03'
	dif3.location = 901.6075, 511.2451
	dif4 = tree.nodes.new('MATERIAL')
	dif4.name = 'SplatDif04'
	dif4.location = 1087.5452, 504.4561
	out = tree.nodes.new('OUTPUT')
	out.name = 'Output'
	out.location = 1213.7106, 663.7264
	mix1 = tree.nodes.new('MIX_RGB')
	mix1.name = 'Mix01'
	mix1.location = 790.5494, 618.7881
	mix1.blend_type = 'MIX'
	mix2 = tree.nodes.new('MIX_RGB')
	mix2.name = 'Mix03'
	mix2.location = 960.7827, 716.9671
	mix2.blend_type = 'MIX'
	mix3 = tree.nodes.new('MIX_RGB')
	mix3.name = 'Mix03'
	mix3.location = 1088.2360, 837.9338
	mix3.blend_type = 'MIX'
	val = tree.nodes.new('VALUE')
	val.name = 'Value'
	val.location = 460.1999, 850.9328
	val.outputs[0].default_value = -1.0
	sep = tree.nodes.new('SEPRGB')
	sep.name = 'Separate RGB'
	sep.location = 373.5712, 714.5419
	mult = tree.nodes.new('MIX_RGB')
	mult.name = 'Mult'
	mult.location = 529.8510, 585.7188
	mult.blend_type = 'MULTIPLY'
	mult.inputs[0].default_value = 1.0
	add = tree.nodes.new('MIX_RGB')
	add.name = 'Add'
	add.location = 698.0180, 849.9446
	add.blend_type = 'ADD'
	add.inputs[0].default_value = 0.5
	#link splat
	links.new(splat.outputs[0], sep.inputs[0])  # color - sep image
	links.new(splat.outputs[1], add.inputs[2])  # alpha - color 2
	#link value
	links.new(val.outputs[0], add.inputs[1])    # value - color 1
	#link seperate rgb
	links.new(sep.outputs[0], mult.inputs[1])   # R - color 1
	links.new(sep.outputs[1], mix1.inputs[0])   # G - factor
	links.new(sep.outputs[2], mix2.inputs[0])   # B - factor
	#link add
	links.new(add.outputs[0], mix3.inputs[0])   # color - factor
	#link material 1
	links.new(dif1.outputs[0], mult.inputs[2])  # color - color 2
	#link material 2
	links.new(dif2.outputs[0], mix1.inputs[2])  # color - color 2
	#link material 3
	links.new(dif3.outputs[0], mix2.inputs[2])  # color - color 2
	#link material 4
	links.new(dif4.outputs[0], mix3.inputs[2])  # color - color 2
	#link mult
	links.new(mult.outputs[0], mix1.inputs[1])  # color - color 1
	#link mix 1
	links.new(mix1.outputs[0], mix2.inputs[1])  # color - color 1
	#link mix 2
	links.new(mix2.outputs[0], mix3.inputs[1])  # color - color 1
	#link mix 3
	links.new(mix3.outputs[0], out.inputs[0])  # color - color 1
	#
	#add 4 diffuse textures and 1 base splat texture, based on uv
	sb = add_image_texture('SplatBlendMask')
	s1 = add_image_texture('Splat01')
	s2 = add_image_texture('Splat02')
	s3 = add_image_texture('Splat03')
	s4 = add_image_texture('Splat04')
	#add 4 diffuse mats and 1 base splat mat, with removed specular and removed mist
	#these mats are not added to the object, just to the nodes
	sx = sy = sz = 5
	splat.material = add_splat_material('SplatBlendMask', sb, 1, 1, 1)
	splat.use_diffuse = True
	splat.use_specular = False
	dif1.material = add_splat_material('Splat01', s1, sx, sy, sz)
	dif1.use_diffuse = True
	dif1.use_specular = False
	dif2.material = add_splat_material('Splat02', s2, sx, sy, sz)
	dif2.use_diffuse = True
	dif2.use_specular = False
	dif3.material = add_splat_material('Splat03', s3, sx, sy, sz)
	dif3.use_diffuse = True
	dif3.use_specular = False
	dif4.material = add_splat_material('Splat04', s4, sx, sy, sz)
	dif4.use_diffuse = True
	dif4.use_specular = False
	#do this at the end
	ob.data.materials.append(mat)
	return ob

class Splatt(bpy.types.Operator):
	"""Adds a ShaderNode material for game texture splatting """ \
	"""based on a blending mask texture, render in GLSL """
	bl_idname = 'mesh.splatt'
	bl_label = 'Splatt'
	bl_options = {'REGISTER', 'UNDO'}

	@classmethod
	def poll(cls, context):
		obj = context.active_object
		return (obj and obj.type == 'MESH')

	def execute(self, context):
		do_4x_splatting(context.active_object)
		return {'FINISHED'}

def menu_func(self, context):
	self.layout.operator(Splatt.bl_idname, text="Add 4x splatt")

def register():
	bpy.utils.register_module(__name__)

	#bpy.types.VIEW3D_MT_edit_mesh_specials.append(menu_func)
	#bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func)
	bpy.types.VIEW3D_MT_object.append(menu_func)

def unregister():
	bpy.utils.unregister_module(__name__)

	#bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
	#bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func)
	bpy.types.VIEW3D_MT_object.remove(menu_func)

if __name__ == "__main__":
	register()

this one is the file to export a splat material to a j3m file, should be also able to create std. lighting material and unlit

#----------------------------------------------------------
# File exportSplattMaterial.py
#----------------------------------------------------------

bl_info = {
	'name': 'Export Splatt Material',
	'author': 'tom [et] 3d-inferno [point] com',
	'version': (0, 0, 1),
	'blender': (2, 6, 5),
	"location": "View3D > Specials > Export Splatt Material",
	'description': 'Export Shadernodes to jme material',
	'warning': '',
	'wiki_url': '',
	'tracker_url': '',
	"support": 'COMMUNITY',
	"category": "Object"}

import bpy

def mapSplatNode(node) :
	#Texture2D DiffuseMap_0
	#Float DiffuseMap_0_scale
	#Texture2D NormalMap_0
	#
	#Texture2D AlphaMap
	if node.name == "Splat":
		return "AlphaMap"
	if node.name == "SplatDif01" :
		return "DiffuseMap_0"
	if node.name == "SplatDif02" :
		return "DiffuseMap_1"
	if node.name == "SplatDif03" :
		return "DiffuseMap_2"
	if node.name == "SplatDif04" :
		return "DiffuseMap_3"
	else : #TODO throw a warning
		return ""

def writeSplatMaterial(mat, filename, oname) :
	#print (str(path) + "," + str(oname))
	with open(filename, "wt") as f:
		f.write("Material "+oname+" : Common/MatDefs/Misc/Splatt4x.j3md {\n")
		f.write("\tMaterialParameters {\n") 
		#Diffuse maps aand Blendmask
		tree = mat.node_tree
		#print(str(tree))
		for n in tree.nodes :
			#print(str(n.type))
			if n.type == 'MATERIAL':
				if n.material.texture_slots[0] != None :
				#if n.material and len(n.material.texture_slots) > 0 : #this will always be true, check if a texture is set then move on!
					#print("ts >0, "+str(n.material.texture_slots[0].texture))
					if  n.material.texture_slots[0].texture and n.material.texture_slots[0].texture.type == 'IMAGE' :
						mapname = mapSplatNode(n)
						f.write("\t"+mapname+" : "+ bpy.path.abspath(n.material.texture_slots[0].texture.image.filepath) +"\n")
						#print("abs: "+ bpy.path.abspath(n.material.texture_slots[0].texture.image.filepath) )
						#next line will blow up if textures would be on a different drive than the blend files :-(
						# so we use abspath and correct this later in the pipeline
						#print("rel: "+ (bpy.path.relpath(n.material.texture_slots[0].texture.image.filepath)[2:]) )
						if mapname != "AlphaMap" :
							#ts.scale only one value
							f.write("\t"+mapname+"_scale : %f\n" % max(n.material.texture_slots[0].scale.x, n.material.texture_slots[0].scale.y, n.material.texture_slots[0].scale.z) )
							#we assume second texture is normal map in splatting (not used so far), only write out if diffuse was written
							#if n.material and len(n.material.texture_slots) > 1 :  #this is always the case belnder has 18 texture slots
							if n.material.texture_slots[1] != None and n.material.texture_slots[1].texture != None:
								#print("slots: "+str(len(n.material.texture_slots)))
								#print("type: "+n.material.texture_slots[1].type )
								if n.material.texture_slots[1].texture.type == 'IMAGE' :
									f.write("\tNormalMap"+mapname[0:2]+" : "+ bpy.path.abspath(n.material.texture_slots[1].texture.image.filepath) +"\n")
		f.write("\tShininess : %f\n" % mat.specular_hardness)
		f.write("\tAmbient : 0.5 0.5 0.5 %f\n" % mat.ambient)
		f.write("\tDiffuse : %f %f %f %f\n" % (mat.diffuse_color.r, mat.diffuse_color.g, mat.diffuse_color.b, mat.alpha) )
		f.write("\tSpecular : %f %f %f %f\n" % (mat.specular_color.r, mat.specular_color.g, mat.specular_color.b, mat.specular_alpha) )
		f.write("\t}\n")
		f.write("}\n")
		f.close()

#		f.write("\tDiffuseMap : Models/Buggy/buggy_diffuse.jpg\n")
#		f.write("\tGlowMap : Models/Buggy/buggy_glow.jpg\n")
#		f.write("\tSpecularMap : Models/Buggy/buggy_specular.jpg\n")
#		f.write("\tNormalMap : Models/Buggy/buggy_normals.png\n")
def mapLightingParam(name) :
	lname = lowercase(name)
	if lname == "diffuse" or lname == "difuse" :
		return "DiffuseMap"
	if lname == "glow" :
		return "GlowMap"
	if lname == "specular" :
		return "SpecularMap"
	if lname == "normal" :
		return "NormalMap"
	else : #TODO what is the dummy texture? throw a warning
		return ""

#write out a j3m material based on lighted values
#example from jme3
#Material My Material : Common/MatDefs/Light/Lighting.j3md {
#     MaterialParameters {   
#        DiffuseMap : Models/Buggy/buggy_diffuse.jpg
#        GlowMap : Models/Buggy/buggy_glow.jpg
#        SpecularMap : Models/Buggy/buggy_specular.jpg
#        NormalMap : Models/Buggy/buggy_normals.png
#        Shininess : 10
#        
#        UseMaterialColors : true
#        Ambient : 0.5 0.5 0.5 1
#        Diffuse : 1 1 1 1
#        Specular : 1 1 1 1
#     }
#}
#example end
def writeLightingMaterial(mat, filename, oname):
	with open(filename, "wt") as f:
		f.write("Material "+oname+" : Common/MatDefs/Lighting/Lighting.j3md {\n")
		f.write("\tMaterialParameters {\n") 
		useMatColor = "false"
		if len(mat.texture_slots) > 0 :
			for ts in mat.texture_slots :
					if ts != None and ts.texture != None and ts.texture.type == 'IMGAE' :
						useMatColor = "true"
						f.write("\t%s : %s\n" % (mapLightingParam(ts.texture.name),bpy.path.abspath(ts.texture.image.filepath)[2:]) )
		f.write("\tShininess : "+ mat.specular_hardness +"\n")
		f.write("\n")
		f.write("\tUseMaterialColors : "+useMatColor+"\n")
		f.write("\tAmbient : 0.5 0.5 0.5 %f\n" % mat.ambient)
		f.write("\tDiffuse : %f %f %f %f\n" % (mat.diffuse_color.r, mat.diffuse_color.g, mat.diffuse_color.b, mat.alpha) )
		f.write("\tSpecular : %f %f %f %f\n" % (mat.specular_color.r, mat.specular_color.g, mat.specular_color.b, mat.specular_alpha) )
		f.write("\t}\n")
		f.write("}\n")
		f.close()

#write out a j3m material based on diffuse color of material only
#example from jme3
#Material Red Color : Common/MatDefs/Misc/Unshaded.j3md {
#     MaterialParameters {
#         Color : 1 0 0 1
#     }
#}
#example end
def writeUnlitMaterial(ob, filename) :
	#name of object/group
	oname = ob.name
	if ob.users_group :
		oname = ob.users_group
	with open(filename, "wt") as f:
		f.write("Material "+oname+" : Common/MatDefs/Misc/Unshaded.j3md {\n")
		f.write("\tMaterialParameters {\n")
		if ob.material_slots != None and len(ob.material_slots) > 0 and ob.material_slots[0].material != None :
			mat = ob.material_slots[0].material
			f.write("\tColor : %f %f %f %f\n" % (mat.diffuse_color.r, mat.diffuse_color.g, mat.diffuse_color.b, mat.alpha) )
		else :
			f.write("\tColor : 0.8 0 0.45 1\n")
		f.write("\t}\n")
		f.write("}\n")
		f.close()

def hasUVs(ob) :
	if len(ob.data.uv_layers) > 0 : 
		return True 
	else : 
		return False

def writeMaterial(ob, filename):
	#name of object/group
	oname = ob.name
	if len(ob.users_group) > 0 :
		oname = ob.users_group[0].name
	#nodes based material (splatting for terrain, etc)
	if hasUVs(ob) and ob.material_slots[0] != None and ob.material_slots[0].material != None :
		if ob.material_slots[0].material.use_nodes :
			writeSplatMaterial(ob.material_slots[0].material, filename, oname)
		else : #no its a simple material with up to 3 textures
			writeLightingMaterial(ob.material_slots[0].material, filename, oname)
	else :
		writeUnlitMaterial(ob, filename)
	#TODO copy textures over!
	#for each material of the object
	#	stuff in parameters
	#	for each texture in material
	#		writeImageTexture(texture, filename)
	return {'FINISHED'}

from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty

class ExportSplatt(bpy.types.Operator, ExportHelper):
	"""Export a ShaderNode material into a jme material """
	bl_idname = 'mesh.exportsplatt'
	bl_label = 'ExportSplatt'
	bl_options = {'REGISTER', 'UNDO'}
	# ExportHelper mixin class uses this
	filename_ext = ".j3m"

	filepath = ""
	filter_glob = StringProperty(default="*.j3m",options={'HIDDEN'})

#	@classmethod
#	def poll(cls, context):
#		obj = context.active_object
#		return (obj and obj.type == 'MESH')

	def execute(self, context):
		#return write_some_data(context, self.filepath, self.use_setting)
		return writeMaterial(context.active_object, self.filepath)
		#return {'FINISHED'}

def menu_func(self, context):
	self.layout.operator(ExportSplatt.bl_idname, text="Export Splatt Material")

def register():
	bpy.utils.register_module(__name__)

	#bpy.types.VIEW3D_MT_edit_mesh_specials.append(menu_func)
	#bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func)
	bpy.types.VIEW3D_MT_object.append(menu_func)
	bpy.types.INFO_MT_file_export.append(menu_func)

def unregister():
	bpy.utils.unregister_module(__name__)

	#bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
	#bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func)
	bpy.types.VIEW3D_MT_object.remove(menu_func)
	bpy.types.INFO_MT_file_export.remove(menu_func)

if __name__ == "__main__":
	register()
	# test call
	#bpy.ops.mesh.exportsplatt('INVOKE_DEFAULT')

just give me a pm where to send the files in case you get into trouble with the indents for the python scripts.

these files are addon files for blender and should be placed in the scripts addons folder, last edition tested with was blender 2.65, nodes were quite new, some things might have changed and will not work for sure in 2.7+ But it will give anybody willing to write an exporter/node setup a good head start.