HairMesher (Generate mesh from hair with UVs,C++)

Well I am going to post these here first. I just do not like facebook and I do not know what "gumroad’ is.
I made some screenshots that will post here.

The first is not a documentation problem but a problem with my personal machine.I can only include three graphics per post here so this will be a multipart post.

I seem to have two versions of Hair Mesher in my blender addon folder. Probably my fault.
I know that version 0.2.8 is an old version and I will download the new version as soon as I finish this post. The problem is that with the old version enabled I cannot seem to find when the entry for 0.2.8 is in the toolbar?

So after starting adding a simple plain that I subdivide twice and added hair guides to. ( I did it this way so that I will be really simple when I write the document. A plane is something that everyone can add to the 3d viewport. No need to have a particular mesh or model) I started 0.1.5 and got this screen.


I pressed the “object preset” buttom just becasue its at the top of the dialog and I get this


First question :

#1 What exactly is an “Object Preset”

EDIT the first image will be included in the next post. After I posted this I realized that I had some person al information in that screen capture that I had to cover up. So I edited this post and deleted that image and tried to repost it with the personal information deleted but the forum software still thinks that I have three images in this post.

To be continued in the next post.

Attachments


Well I am going to post these here first. I just do not like facebook and I do not know what "gumroad’ is.
I made some screenshots that will post here.

The first is not a documentation problem but a problem with my personal machine.I can only include three graphics per post here so this will be a multipart post.

I seem to have two versions of Hair Mesher in my blender addon folder. Probably my fault.


I know that version 0.2.8 is an old version and I will download the new version as soon as I finish this post. The problem is that with the old version enabled I cannot seem to find when the entry for 0.2.8 is in the toolbar?

So after starting adding a simple plain that I subdivide twice and added hair guides to. ( I did it this way so that I will be really simple when I write the document. A plane is something that everyone can add to the 3d viewport. No need to have a particular mesh or model) I started 0.1.5 and got this screen.


I pressed the “object preset” buttom just becasue its at the top of the dialog and I get this


First question :

#1 What exactly is an “Object Preset”

To be continued in the next post.

Part Two:

I somehow found version 0.2.8 Ingore my Toolbox tab categories. I have changed them using another addon in order to organize the addons so I do not have so many tabs that are so small that I cannot see what is what. In any case. After finding 0.2.8 I found that there was an addon setting panel in the bottom part of the tab. I pulled it up so I could see it and added press the ok button WITHOUT changing any settings at all. Where is what I got.


I added meshs to the guides but they were all black which normally means that the normals are facing the wrong way. Is that expected behaviour or did I do something wrongly?


Ok once again starting at the very top.

#3 What is ingore for?

#4 What is the text input box next to ingore for?

To be continued.

Attachments


Part Two:

I somehow found version 0.2.8 Ingore my Toolbox tab categories. I have changed them using another addon in order to organize the addons so I do not have so many tabs that are so small that I cannot see what is what. In any case. After finding 0.2.8 I found that there was an addon setting panel in the bottom part of the tab. I pulled it up so I could see it and added press the ok button WITHOUT changing any settings at all. Where is what I got.

[ATTACH=CONFIG]510948[/ATTACH]
I added meshs to the guides but they were all black which normally means that the normals are facing the wrong way. Is that expected behaviour or did I do something wrongly?

[ATTACH=CONFIG]510949[/ATTACH]

Ok once again starting at the very top.

#3 What is ingore for?

#4 What is the text input box next to ingore for?

To be continued.

@SHABA1
I think you’d better to enable both the old version and the new version at the same time ,they may be conflicted.
the new version panel will in the
tab call"addons" in the left toolbar.
1:the operator preset is for saving your all parameters setting into a text file,in order to reproduce the hairmesh in next time without re-setting the parameter again.
3:ignore : is for ignoring the faces that have weight map ,when you excuting one operator the hair strands that near to these weighted-faces will not be effected.(other weight map slots are working the similar way)
4:the text box is for naming your preset.

I got the most recent version installed now. I will start outlining the document tomorrow and send you the outline in .doc, .docx or .rtf format( whichever you prefer) then I will start writing this weekend.

imjs I got your pm. I am outlining the document right now and will start writing and filling in the outline this Sunday. I still have some question though like what the “Auto” buttons do and what weight painting the hairs and not the emitter mesh does. BUT I like to be detailed and specific so when I have some specific questions I will email them to you.

@shaba1
thank you when your document is done I will add it to the addon and will add your name as one of developer.

Thank you. Purchased. I’ve been waiting for a feature like this. Hopefully this saves some time creating low poly hair.

Justb: It is a great product. It is worth every penny imjs charges for it. It is one of the very very few blender addons that I have paid for and it is well worth the price.

imjs: I am still working on that document. the real paying job has had me very busy for the last couple of months.

thank you don’t be hurry,I 'm busy with the animation job too.I don’t make money by selling addons mainly, I develope it just because I need it in my job.

update----20180627(v 0.3.5)----
you can offset the segment lenght form top to root(that allow use less mesh to perserve the shape).
add some UV offset options.


1 Like

Hello! I purchased the addon on sellify but there do not seem to be any updates past 0.3.1 though it appears the gumroad one goes as high as 0.3.5. How can I get the latest version?

now is ok.

Thank you for the quick upload! I seem to be having issues with this version though. PYLIB appears to be an issue. I get the same error as described here. I see there is a PYLIB folder in this release but all the contained files are .pyc rather than .py (I assume is the culprit).

I have uploaded the new file to sellfy and gumroad. please test it.

imdjs: I think I may have discovered a bug. But I think that it is just may machine or maybe this older blender 2.78c version. I was trying to use your plugin to add hair to a MBL character yesterday and when I pressed the button in the toolshel nothing happend… only the IMJS Hairmesher button appeared in the toolshel and the three checkboxes. None of the other interface element appeard I though it was something I was doing becasue I had not used the addon in a few months. So then I started a new instance of 2.78c added a grid then weight painted it and added a particle system with the hairs generated by length and density according to the weight paint. I got this error.

! DATA change
UPDATE WEIGHT~~~
time0---->: 0.001
IS CHANGE?? True
== 3
psys->totpart= 128
PARTICLE +++>
time1---->: 0.002
time2---->: 0.002
LAST1 0.5333333333333333
Traceback (most recent call last):
File “C:\Users\Desktop\Blender Files\blender278c\2.78\scripts\addons\IMDJS_HairMesher\HairMesher.py”, line 262, in execute
G.DLL.ΔΔ毛得最近头皮面ξlast(cvpO头皮,G.i忽略面数G,oA.pp毛发o.ip毛发数,oA.pp毛发o.iip毛点数Max,c_float(scale/5));
OSError: exception: integer divide by zero

location: :-1:
I then shut blender completely down and started it again and tried to load some old projects that I had previously done using Hair Mesher. Those worked just fine. Just as they always had worked.

Then I started Blender 2.79 and loaded the weight painted grid in it again. Now the full hair mesher interface appeared and everything worked as normal.

Hi imdjs,

I got some sort of bold question. I think you would understand the problem we have in an addon.

For an external render engine we are generating mesh lints from hairs, either parent, child or both.
The issue we have is that the mesh is needs to be triangulated but this causes he normals to be flipping.

See attached image

I also see there is an error with the amount of normals when i compare it to the uvs. I believe these should be the same number right? Because when i make a mesh in sort of the same shape. For example 4 faces, i get these numbers:

2d mesh modeled hair plane
Vertex Count: 10
Normal Count: 24
Triangle Count: 8
uvCount Count: 24

But with this export, i get these numbers

hair export
Vertex Header: 10
Normal Count: 8
triangleCount Count: 8
uvCount Count: 24

This is part of the code, it gets a mesh with other data from a function and than does the export. It looks okay in color channel, but when i check the normal channel i see he above result.

Below is the code

from mathutils import Vector

def perpendicular_vector(CameraZAxis):

    a=CameraZAxis.cross((0,1,0))
    b=CameraZAxis.cross((0,0,1))
    #if (a.dot(a)>b.dot(b)):
    #	return a.normalized()
    #else:
    #	return b.normalized()

    return b.normalized()


# vector substration
def vector_substract(a, b):
    return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]

def vector_cross_product(x, y):
    v = [0, 0, 0]
    v[0] = x[1]*y[2] - x[2]*y[1]
    v[1] = x[2]*y[0] - x[0]*y[2]
    v[2] = x[0]*y[1] - x[1]*y[0]
    return v

def calc_normal(v0, v1, v2):
    return vector_cross_product(vector_substract(v0, v1),vector_substract(v0, v2))

if exportGeometry: #if we should export geometry too
	# remember list of selected and active objects
	vertexNum = 0
	normalNum = 0
	faceNum = 0
	uvNum = 0
	modelP = []
	modelN = []
	modelF = []
	modelU = []

	#get active UV
	me = bpy.data.meshes[ob[4]]
	uv_layer = me.uv_layers.active
	modelUVs = len(uvlayers)
	for uvParticle in range(0, len(uvlayers)):
		activelayer = me.uv_layers[uvParticle]
		if activelayer == uv_layer:
			uvParticle

	rootSize = 0.1
	tipSize = 0.01
	width = rootSize
	hairKeysNum = partSystem.settings.draw_step
	stepV = 1 / (hairKeysNum)
	#why double declare variable
#			stepV = 0.25
	vertexNum = 0
	faceNum = 0

	ob_camera = bpy.context.scene.camera
	CameraZAxis = mathutils.Vector((ob_camera.matrix_world.to_3x3()[0][2], ob_camera.matrix_world.to_3x3()[1][2], ob_camera.matrix_world.to_3x3()[2][2]))

	#why double declare variable
	steps = partSystem.settings.draw_step
	steps = 2**steps + 1
	stepV = 1 / (steps-1)
	sizeStep = (rootSize - tipSize) / (steps-1)
	num_parents = len(partSystem.particles)
	num_children = len(partSystem.child_particles)

	particleColors = False

	if getattr(partSystem.settings, "child_type")!="NONE":
		loop_start = 0 if getattr(partSystem.settings, "use_parent_particles", False) else num_parents
	else:
		loop_start = 0
	for pindex in range(loop_start,  num_children + num_parents):
		if getattr(partMat, 'thea_ColoredHair', False):
			if pindex >= num_parents:
			   particle = partSystem.particles[(pindex - num_parents) % num_parents]
			else:
			   particle = partSystem.particles[pindex]
			
			p_uv = partSystem.uv_on_emitter(mod, particle, pindex, uvParticle)
			particleColors = True

		vStart = vertexNum
		width = rootSize
		#rWidthX = width
		rWidthX = perpendicular_vector(CameraZAxis)[0]*width
		sizeStepX = sizeStep
		#rWidthY = width
		rWidthY = perpendicular_vector(CameraZAxis)[1]*width
		sizeStepY = sizeStep
		prevCo = Vector((0,0,0))
		for step in range(0, steps):
			co = partSystem.co_hair(object, pindex, step)
			if(co == Vector((0,0,0))):
				co=prevCo
			pX, pY, pZ = co
			prevCo = co
			######
			#perhaps this causes the issue with normals in hair
			# i think normal method is [+x, -x, +y, -y, +z, -z]
			#####
			modelP.append(pX-(rWidthX/2))
			modelP.append(pY-(rWidthY/2))
			modelP.append(pZ)
			modelP.append(pX+(rWidthX/2))
			modelP.append(pY+(rWidthY/2))
			modelP.append(pZ)
			vertexNum +=2
			width -= sizeStep
			rWidthX = perpendicular_vector(CameraZAxis)[0]*width
			rWidthY = perpendicular_vector(CameraZAxis)[1]*width
			#rWidthX -= sizeStepX
			#rWidthY -= sizeStepY

		V = 0
		for v in range(vStart,vertexNum-2,2):
			modelF.append(v)
			modelF.append(v+1)
			modelF.append(v+2)
			modelF.append(v+1)
			modelF.append(v+2)
			modelF.append(v+3)
			# calc normals
			v1 = v
			v2 = v+1
			v3 = v+2
			v1X = modelP[v1*3]
			v1Y = modelP[v1*3+1]
			v1Z = modelP[v1*3+2]
			v2X = modelP[v2*3]
			v2Y = modelP[v2*3+1]
			v2Z = modelP[v2*3+2]
			v3X = modelP[v3*3]
			v3Y = modelP[v3*3+1]
			v3Z = modelP[v3*3+2]
			v1c = (v1X,v1Y,v1Z)
			v2c = (v2X,v2Y,v2Z)
			v3c = (v3X,v3Y,v3Z)
			normal = calc_normal(v1c,v2c, v3c)
			modelN.append(normal[0])
			modelN.append(normal[1])
			modelN.append(normal[2])

			#Why is this done 2 times?
			v1 = v+1
			v2 = v+2
			v3 = v+3
			v1X = modelP[v1*3]
			v1Y = modelP[v1*3+1]
			v1Z = modelP[v1*3+2]
			v2X = modelP[v2*3]
			v2Y = modelP[v2*3+1]
			v2Z = modelP[v2*3+2]
			v3X = modelP[v3*3]
			v3Y = modelP[v3*3+1]
			v3Z = modelP[v3*3+2]
			v1c = (v1X,v1Y,v1Z)
			v2c = (v2X,v2Y,v2Z)
			v3c = (v3X,v3Y,v3Z)
			normal = calc_normal(v1c,v2c, v3c)
			modelN.append(normal[0])
			modelN.append(normal[1])
			modelN.append(normal[2])


			if particleColors:
				for i in range(6):
					modelU.append(p_uv[0])
					modelU.append(1-p_uv[1])
			else:
				modelU.append(0)
				modelU.append(1-V)
				modelU.append(1)
				modelU.append(1-V)
				V += stepV
				modelU.append(0)
				modelU.append(1-V)
				V -= stepV
				##print("V: ",V)
				modelU.append(1)
				modelU.append(1-V)
				V += stepV
				##print("V: ",V)
				modelU.append(0)
				modelU.append(1-V)
				modelU.append(1)
				modelU.append(1-V)
			faceNum += 2

#			modelFile = open(os.path.join(tempDir,modelFilename), "wb")
	modelFile = open(modelPath, "wb")

	magicHeader = 0x54524d01
	modelFile.write(struct.pack('<l',magicHeader))

	vertexCount = len(modelP)
	if vertexCount > 0:
		modelFile.write(struct.pack('<l',int(vertexCount/3)))
		modelFile.write(struct.pack("<%df" % (vertexCount), *modelP))
		#print("Vertex header: %s" % int(vertexCount/3))
		#print("Vertex points: %s" % (vertexCount), *modelP)


	normalCount = len(modelN)
	if normalCount > 0:
		modelFile.write(struct.pack('<l',int(normalCount/3)))
		modelFile.write(struct.pack("<%df" % (normalCount), *modelN))
		#print("Normal header: %s" % int(normalCount/3))
		#print("Normal points: %s" % (normalCount), *modelN)


	triangleCount = len(modelF)
	if triangleCount > 0:
		modelFile.write(struct.pack('<l',int(triangleCount/3)))
		modelFile.write(struct.pack("<%dl" % (triangleCount), *modelF))
		#print("Triangle header: %s" % int(triangleCount/3))
		#print("Triangle points: %s" % (triangleCount), *modelF)


	uvCount = len(modelU)
	if vertexCount > 0:
		modelFile.write(struct.pack('<l',int(uvCount/2)))
		modelFile.write(struct.pack("<%df" % (uvCount), *modelU))
		#print("UV header: %s" % int(uvCount/3))
		#print("UV points: %s" % (uvCount), *modelU)

#			input()
	s = '<Parameter Name="Smoothing Angle" Type="Real" Value="180"/>\n'
#			modelFile.write(struct.pack('c', s.encode('UTF-8')))
	modelFile.write(struct.pack('p', s.encode()))
	modelFile.close()

The render enigne also spits out an error due the the wrong normals i guess. This is the error i get there:
Warning: normal list size is truncated (normals: 8, vertices: 10, indices: 24).

Do you perhaps have some tips or guidance?

PS there was also an older code, it show better normals but was much slower. The original coder did some adjustments and now we see the prior result.

Problem is the old dev doesnt do support anymore, i tried contacting him many times…

If you look at this normal channel you see its much better. It does have 1 or 2 triangles which are bad. But this also show the error in the engine about the normals. Though they look better
It give a different number because it uses co_hair for the steps

Warning: normal list size is truncated (normals: 14, vertices: 16, indices: 42).

if exportGeometry: #if we should export geometry too
    # remember list of selected and active objects
    vertexNum = 0
    normalNum = 0
    faceNum = 0
    uvNum = 0
    modelP = []
    modelN = []
    modelF = []
    modelU = []

    rootSize = self.materialList[matKey].blenderMat.thea_StrandRoot
    tipSize = self.materialList[matKey].blenderMat.thea_StrandTip

    hairKeysNum = partSystem.settings.hair_step
    sizeStep = (rootSize - tipSize) / (hairKeysNum)
    stepV = 1 / (hairKeysNum)
    #stepV = 0.25
    vertexNum = 0
    faceNum = 0
    rand = 1

    for particle in partSystem.particles:
        width = rootSize
        vStart = vertexNum
        mul = 1
        #rand = random()
        rand = -rand
        if rand > 0.5:
            rWidthX = width
            sizeStepX = sizeStep
        else:
            rWidthX = width*-1
            sizeStepX = sizeStep*-1
        #rand = random()
        if rand > 0.5:
            rWidthY = width
            sizeStepY = sizeStep
        else:
            rWidthY = width*-1
            sizeStepY = sizeStep*-1

#                 rWidthX = width
#                 sizeStepX = sizeStep
#                 rWidthY = width
#                 sizeStepY = sizeStep

        for pL in particle.hair_keys:
            pX, pY, pZ = pL.co
            modelP.append(pX-(rWidthX/2))
            modelP.append(pY-(rWidthX/2))
            modelP.append(pZ)
            modelP.append(pX+(rWidthX/2))
            modelP.append(pY+(rWidthY/2))
            modelP.append(pZ)
            mul *= -1
            width -= sizeStep
            rWidthX -= sizeStepX
            rWidthY -= sizeStepY
            vertexNum +=2



        fStart = faceNum
        V = 0
        for v in range(vStart,vertexNum-2,2):
            modelF.append(v)
            modelF.append(v+1)
            modelF.append(v+2)
            modelF.append(v+1)
            modelF.append(v+2)
            modelF.append(v+3)
            # calc normals
            v1 = v
            v2 = v+1
            v3 = v+2
            v1X = modelP[v1*3]
            v1Y = modelP[v1*3+1]
            v1Z = modelP[v1*3+2]
            v2X = modelP[v2*3]
            v2Y = modelP[v2*3+1]
            v2Z = modelP[v2*3+2]
            v3X = modelP[v3*3]
            v3Y = modelP[v3*3+1]
            v3Z = modelP[v3*3+2]
            v1c = (v1X,v1Y,v1Z)
            v2c = (v2X,v2Y,v2Z)
            v3c = (v3X,v3Y,v3Z)
            normx = (v1Z-v2Z)*(v3Y-v2Y)-(v1Y-v2Y)*(v3Z-v2Z);
            normy = (v1X-v2X)*(v3Z-v2Z)-(v1Z-v2Z)*(v3X-v2X);
            normz = (v1Y-v2Y)*(v3X  -v2X)-(v1X-v2X)*(v3Y-v2Y);
            normlength = sqrt((normx*normx)+(normy*normy)+(normz*normz));
            normx /= normlength;
            normy /= normlength;
            normz /= normlength;
            modelN.append(normx)
            modelN.append(normy)
            modelN.append(normz)

            v1 = v+1
            v2 = v+2
            v3 = v+3
            v1X = modelP[v1*3]
            v1Y = modelP[v1*3+1]
            v1Z = modelP[v1*3+2]
            v2X = modelP[v2*3]
            v2Y = modelP[v2*3+1]
            v2Z = modelP[v2*3+2]
            v3X = modelP[v3*3]
            v3Y = modelP[v3*3+1]
            v3Z = modelP[v3*3+2]
            v1c = (v1X,v1Y,v1Z)
            v2c = (v2X,v2Y,v2Z)
            v3c = (v3X,v3Y,v3Z)
            normx = (v1Z-v2Z)*(v3Y-v2Y)-(v1Y-v2Y)*(v3Z-v2Z);
            normy = (v1X-v2X)*(v3Z-v2Z)-(v1Z-v2Z)*(v3X-v2X);
            normz = (v1Y-v2Y)*(v3X  -v2X)-(v1X-v2X)*(v3Y-v2Y);
            normlength = sqrt((normx*normx)+(normy*normy)+(normz*normz));
            normx /= normlength;
            normy /= normlength;
            normz /= normlength;
            modelN.append(normx)
            modelN.append(normy)
            modelN.append(normz)

            modelU.append(0)
            modelU.append(1-V)
            modelU.append(1)
            modelU.append(1-V)
            V += stepV
            modelU.append(0)
            modelU.append(1-V)
            V -= stepV
            modelU.append(1)
            modelU.append(1-V)
            V += stepV
            modelU.append(0)
            modelU.append(1-V)
            modelU.append(1)
            modelU.append(1-V)
            faceNum += 2




    modelFile = open(os.path.join(tempDir,modelFilename), "wb")

    magicHeader = 0x54524d01
    modelFile.write(struct.pack('<l',magicHeader))

    vertexCount = len(modelP)

    if vertexCount > 0:
        modelFile.write(struct.pack('<l',int(vertexCount/3)))
        modelFile.write(struct.pack("<%df" % (vertexCount), *modelP))

    normalCount = len(modelN)
    if normalCount > 0:
        modelFile.write(struct.pack('<l',int(normalCount/3)))
        modelFile.write(struct.pack("<%df" % (normalCount), *modelN))

    triangleCount = len(modelF)
    if triangleCount > 0:
        modelFile.write(struct.pack('<l',int(triangleCount/3)))
        modelFile.write(struct.pack("<%dl" % (triangleCount), *modelF))

    uvCount = len(modelU)
    if vertexCount > 0:
        modelFile.write(struct.pack('<l',int(uvCount/2)))
        modelFile.write(struct.pack("<%df" % (uvCount), *modelU))

    modelFile.close()

The problem is when its exported that some of the normal points become minus. Here’s how the code looks when export. When i change all the minus to plus manually All normals point in the same direction.

<Parameter Name="Normal List" Type="Point3D List" Value="10">
<P xyz="0 0.961947 0.273238"/>
<P xyz="0 0.961947 0.273238"/>
<P xyz="0 0.961946 0.273238"/>
<P xyz="0 -0.961947 -0.273238"/>
<P xyz="0 0.961947 0.273238"/>
<P xyz="0 -0.961947 -0.273238"/>
<P xyz="0 -0.961946 -0.273238"/>
<P xyz="0 -0.961946 -0.273238"/>
<P xyz="0 0.961947 0.273238"/>
<P xyz="0 -0.961947 -0.273238"/>
</Parameter>

Is there a method to fix this?