EDIT: it appears my problem is color-space conversion in Cycles.
Hi everybody. I’ve been trying to create a script that bakes displacement from one ‘source’ mesh to a ‘target’ mesh by comparing the position of each vertex. How I’m doing it is explained below and I’ll share the code if anyone asks.
Problem:
I’m trying to bake displacement for Cycles to use, but I don’t really understand what data Cycles expects, nor do I understand what it does with the data. I can’t reproduce the displacement Cycles produces with Python.
Cycles displaces the geometry almost correctly, but there’s a noticeable margin of error on the vertices that transform along something other than the world x,y, z axes (diagonal transformations). I think somehow the normal of the face or normal is being used by Cycles, since the margin of error is worse in some test-meshes than in others. The problem is that I simply don’t know what Cycles is doing! It obviously isn’t just moving the vertices in global space (v.co += disp) because when I do that in Python, it’s exactly correct. Any help in this matter would be appreciated.
I think it’s possible that Cycles is performing some sort of color-space transformation to the vertex-color data. Any ideas on how to test this, or work around?
( I tried looking through the source code for Cycles, but I didn’t understand it (maybe I just wasn’t looking in the right place). This is the part of the source code I was looking through- nowhere do I see the code that actually changes the location of anything, it looks like it’s being done somewhere else. I’ve been poking around elsewhere and can’t find anything relevant.)
Here’s how I’m doing it:
- I’m generate the data by subtracting the location of the target vertex from the source.
- Then, I take the square root of each (x, y, z) component of the displacement vector. I think this is what is going on: right now it represents a translation on x, y, and z, and what I want is a vector that goes straight through the rectangular space of that transformation. Or to put it another way, x and y, x and z, y and z all make the legs of right triangles, and I take the square root to get the hypotenuse. Scaliing by the magic number 1.05 seems to help some of the problems in Cycles, but it’s not consistent.
- Finally, I scale each vector until the maximum value in either direction, positive or negative, is 1. This way I can fit the data into vertex colors without it being clamped. I use a layer for positive coordinates and negative coordinates. The negative coordinates need to have their signs flipped, so they can be stored as colors.
If I skip the square root step, the result is bizarrely exaggerated, especially along the x, y and z axes. The direction is correct, but the magnitude is wrong. Taking the square root fixes this, for the most part.
My shader is simple, the two layers of vertex colors are subtracted ( pos - neg) and fed into a vector displacement node set to object space. Midlevel = 0, and the scale is the reciprocal of the value I scaled the vectors by in step 2 (right now I just have to do it manually, the value is printed in the console).
Here’s the code:
import bpy
import mathutils
import math
sign = lambda x: x and (1, -1)[x < 0]
def InactiveObject():
"""Returns the first selected object that is not Active"""
obReturn = None
if (len (bpy.context.selected_objects) != 0):
for ob in bpy.context.selected_objects:
if (ob != bpy.context.active_object):
obReturn = ob
break
return obReturn
def calcDisplacement(dict, matSource, vSource, matTarget, vTarget):
disp = dict
posOffset = 0
negOffset = 0
for i in range(len(vSource)):
v1 = (matTarget @ vTarget[i].co)
v2 = (matSource @ vSource[i].co)
value = v2 - v1
disp[i] = value
if (value.x > posOffset):
posOffset = value.x
elif (value.y > posOffset):
posOffset = value.y
elif (value.z > posOffset):
posOffset = value.z
if (value.x < negOffset):
negOffset = value.x
elif (value.y < negOffset):
negOffset = value.y
elif (value.z < negOffset):
negOffset = value.z
return (disp, posOffset, negOffset)
def scaleDisplacement(dispData, obSource, obTarget):
disp = dispData[0]
posOffset = dispData[1]
negOffset = dispData[2]
scale = 0
if ((posOffset != 0) and (negOffset != 0)):
if ( abs(posOffset) > abs(negOffset) ):
scale = 1/posOffset
else:
scale = -1/(negOffset)
print ("Scale: %s" % scale)
dispScaled = disp
for key, value in dispScaled.items():
value *= scale
#for some reason I can't iterate through the vector?
value.x = math.sqrt(abs(value.x)) * sign(value.x)
value.y = math.sqrt(abs(value.y)) * sign(value.y)
value.z = math.sqrt(abs(value.z)) * sign(value.z)
disp = dispScaled
return [disp, scale]
def CopyDisplacementToVPaint(dict, ob):
posVCol = "displacementPos"
negVCol = "displacementNeg"
ob.data.vertex_colors.new(name = posVCol)
ob.data.vertex_colors.new(name = negVCol)
for l in ob.data.loops:
i = l.vertex_index
val = dict[i]
val_x = 0
val_y = 0
val_z = 0
if (val.x > 0):
val_x = val.x
if (val.y > 0):
val_y = val.y
if (val.z > 0):
val_z = val.z # the values are automatically clamped, so unecessary
ob.data.vertex_colors[posVCol].data[l.index].color = (val_x,val_y,val_z, 1)
val_x = 0
val_y = 0
val_z = 0
if (val.x < 0):
val_x = -1 * val.x
if (val.y < 0):
val_y = -1 * val.y
if (val.z < 0):
val_z = -1 * val.z
ob.data.vertex_colors[negVCol].data[l.index].color = (val_x,val_y,val_z, 1)
def TestDisplacement(disp, scale, ob):
for v in ob.data.vertices:
d = disp[v.index]
d.x = (d.x**2)*sign(d.x)
d.y = (d.y**2)*sign(d.y)
d.z = (d.z**2)*sign(d.z)
d.x *= 1/scale
d.y *= 1/scale
d.z *= 1/scale
mat_loc = mathutils.Matrix.Translation(d)
v.co = mat_loc @ v.co
obTarget = bpy.context.active_object
obSource = InactiveObject()
assert (obSource), "Select two objects with identical topology."
vTarget = obTarget.data.vertices
vSource = obSource.data.vertices
assert (len(vSource) == len(vTarget)), "Select two objects with identical topology."
print (obSource.name, obTarget.name)
disp = {}
matSource = obSource.matrix_world
vSource = obSource.data.vertices
matTarget = obTarget.matrix_world
vTarget = obTarget.data.vertices
dispData = calcDisplacement(disp, matSource, vSource, matTarget, vTarget)
scaleData = scaleDisplacement(dispData, obSource, obTarget)
disp = scaleData[0]
scale = scaleData[1]
CopyDisplacementToVPaint(disp, obTarget)
#TestDisplacement(disp, scale, obTarget)
posOffset = 0
negOffset = 0
Thanks. I wasn’t sure whether to post here or on Stack exchange, so I’ll start with this.