[Closed] Extracting UVW object colors
Here is a neat script to extract pixel values of an objects diffuse map:
--Map colors extractor
--
--INSTRUCTIONS
--Select any poly object with a diffuse map applied.
--Script will create a colored box on each poly face, using the average map vertex pixel color as wirecolor.
(
obj=$ --target poly object
subdivs=256 --resolution of the rendered diffuse texture, for pixel color extraction
theChannel=1 --uvw map channel
delete $Facebox*
box_size=10 --size of the colored boxes
fn is_poly obj=
(
targetClass=classOf obj
targetClass==Editable_poly or targetClass==PolyMeshObject
)
if is_poly obj then
(
--render map into bitmap with given resolution
local thisTexture = renderMap obj.material.diffuseMap size:[subdivs,subdivs] filter:on
--display thisTexture
targetFaces = obj.faces as bitarray
--loop all faces to find map vertices
for f in targetFaces do
(
thisMapVerts = polyOp.getMapFace obj theChannel f
--------------------------------------------------
--collect all map vertices colors of current face
--------------------------------------------------
thisMapVertsColors = for v in thisMapVerts collect
(
--get single map vertex coordinates
uvCoords = polyOp.getMapVert obj theChannel v
--convert vertex coords to bitmap coords
textureCoords = [(uvCoords.x)*(thisTexture.width-1),thisTexture.height-(uvCoords.y)*(thisTexture.height-1)]
--get bitmap pixel value with bitmap coords
vertColor = (getPixels thisTexture textureCoords 1)[1]
--format "face: % color: % uvcoords: % texturecoords: %
" f vertColor uvCoords textureCoords
--vertColor
)
--------------------------------------------------
--------------------------------------------------
--calculate average face color from vertex colors
--------------------------------------------------
local thisAverageFaceColor=color 0 0 0 --init empty color
for c in thisMapVertsColors do --loop all map vertices of face
(
if c==undefined then c=color 255 0 255 --out of range value (pink)
thisAverageFaceColor+=c --add vertex color
)
thisAverageFaceColor=thisAverageFaceColor/thisMapVertsColors.count --make average
--format "face: % color: %
" f thisAverageFaceColor
--------------------------------------------------
--------------------------------------------------
--create a box on every face
--------------------------------------------------
dir = polyop.getFaceNormal obj f
pos = polyop.getFaceCenter obj f
thisBox=box width:box_size length:box_size height:box_size name:("Facebox" + (i as string)) pos:pos dir:dir
thisBox.wirecolor=thisAverageFaceColor
)
)
else
(
format "Select Poly Object with diffuse map"
)
)
however, there is an issue with tiled maps. In the third picture it is illustrated with pink boxes, those are out-of-range values, where the map is tiled/mirrored.
original plane with fitting planar uvw map
each box is using the average face color value as wirecolor
pink boxes are out of range…tiled mapping gives coordinates outside of the bitmap dimensions
i’m not sure why it works, but strange mapping is fine (shrink warp) as long as the texture isn’t repeated.
I have the feeling that the whole method of rendering the diffuse map into a bitmap and reading out pixel values with coordinates isn’t the most efficient way.
I’d have to calculate the whole tile/mirror mapping offsets by myself (but how?).
Is there no simple way to find out a color value of a mapped texture on an object?
It is not too bad actually, but if you are going to assign a single color to all vertices, you could use barycentric coordinates instead and sample ONE pixel exactly at the center of the polygon instead of averaging the colors of the corner vertices.
For fighting the Tiling and Negative values, you can use the MOD() function.
Something like:
fn removeTiling theVal =
(
if abs theVal > 1.0 do theVal = mod theVal 1.0
if theVal < 0.0 do theVal = 1.0+theVal
theVal
)
--Tests:
removeTiling 0.6
removeTiling 1.6
removeTiling -0.4
removeTiling -5.4
--Results:
0.6
0.6
0.6
0.6
Another approach would be to use IntersectRayEx to shoot rays at the mesh at any desired point, grab the value from the intersection which is an array containing both the barycentric coordinates and the mesh face, and use that data. Thus, you can sample at ANY point, not just face centers…
I did something like this, let me share a bit of code on how I dealt with texture tiling.
--corrects negative UVs
if SamplePointUCoord < 0 do
(SamplePointUCoord = 1.0 - abs(SamplePointUCoord))
if SamplePointVCoord < 0 do
(SamplePointVCoord = 1.0 - abs(SamplePointVCoord))
--Combines the coordinates for easier reference
SamplePointUV = point2 (mod SamplePointUCoord 1.0) (mod SamplePointVCoord 1.0) --we mod these coordinates to account for texture tiling
let me know if you want any more explanation.
Do you need them to be seperate boxes? I’ve made a script that simply averages the UVW mapping coords to the face, that way the UV is looking up a single pixel. It works quite well.
-Johan
Hi Johan,
I’m not sure how your script works, but it sounds interesting!
The boxes are only for visualisation of the problem. What I really want to do is to know the average color of any face´s assigned material, to convert it to vertex colors, or do anything else, like a geometric operation based on that color (similar to displacement).
In a nutshell, I wan’t to do my own displacement style geometric deformations, and other things, using the uvw mapped material/texture info. in relation to the object topology
Alright here’s a bit of demo code that shows what my tool does.
(
local p = plane length:100 width:150 lengthSegs:20 widthSegs:30 isSelected:true
p.material = standard diffuseMap:(bitmapTex fileName:((getDir #maxroot) + "\\splash.bmp")) showInViewport:true
-- local obj = selection[1]
local obj = p
addModifier obj (Turn_To_Poly())
local numFaces = polyOp.getNumFaces obj
local unWrap = UVWunwrap()
addModifier obj unWrap
for i = 1 to numfaces do
(
unWrap.edit()
unWrap.setTVSubObjectMode 3
unWrap.selectFaces #{i}
unWrap.breakSelected()
unWrap.scaleSelectedCenter 0.001 0
)
local myUvXf = UVW_Xform()
addModifier obj myUvXf
for i = animationRange.start to animationRange.end by 10 do
(
at time i
with animate on
(
myUvXf.u_offset = random -1. 1.
myUvXf.v_offset = random -1. 1.
)
)
-- You have to manually turn apply to entire object on! It's not directly accesible via maxscript
)
Now you can for example break the planes edges so every polygon is it’s own island. Then apply the UVW script and put a shell modifier on top of that. It will look the same as your image, but it will be one object offcourse. And I don’t know if this eventually will be what you want.
Cheers,
-Johan
yeah, that’s interesting…what I’m looking for is a more “analytical” approach though…
kind of like intersect ray but for maps…
thanks anyway!
That could still work, if you figure out the what UV coord the ray returns then just shrink the UV coords of the object to that of the object. Then just use the same texture.
But that’s me
-Johan
Here is an example which takes the XY plane in world space and samples a grid along world bounding box axes, shooting rays down the -Z:
macroScript ColorBoxesXYPlane category:"Forum Help"
(
fn removeTiling theVal = --
(
if abs theVal > 1.0 do theVal = mod theVal 1.0
if theVal < 0.0 do theVal = 1.0+theVal
theVal
)
on isEnabled return selection.count == 1 and superclassof selection[1] == GeometryClass and classof selection[1] != TargetObject
on execute do
(
local theObj = selection[1] --grab the selected object
local theMS = Mesh_Select() --create a Mesh Select modifier
addModifier theObj theMS --add the Mesh Select to the object to turn it into EMesh
delete $ColorBox_* --delete any previous color boxes
local theMesh = snapshotAsmesh theObj --grab the geometry of the object into memory
local theChannel = 1 --this is the Mapping Channel to use
local theSize = 1.5 --this is the size of the box
local theStep = 2.0 --this is step to scan by
local useMap = false --this flag defines whether to use the Material's Diffuse or the UVs as color
local theBoxSource = box width:theSize length:theSize height:theSize --this is the box prototype to instance
--if the object has a standard material with a diffuse bitmap, we will use it
if classof theObj.material == Standard and classof theObj.material.diffusemap == BitmapTexture then
(
local theBmp = theObj.material.diffusemap.bitmap --grab the bitmap from the diffuse texture
local theW = theBmp.width-1 --get the width (0-based!)
local theH = theBmp.height-1 --and the height (0-based!)
useMap = true --raise the flag to use the map
)
for x = theObj.min.x to theObj.max.x by theStep do --loop through the geometry in world space X aligned to the bbox
(
for y = theObj.min.y to theObj.max.y by theStep do --loop through the geometry in world space Y aligned to the bbox
(
local theRay = Ray [x,y, theObj.max.z+10] [0,0,-1] --prepare a ray to shoot down the negative Z from the sample point
local theInt = intersectRayEx theObj theRay --shoot the ray (requires EMesh object, thus the Mesh_Select modifier)
if theInt != undefined do --if the ray did hit something,
(
local theBox = instance theBoxSource --instance the box
theBox.name = uniquename "ColorBox_" --give it a unique name
theBox.pos = theInt[1].pos --place it at the point of intersection
theBox.dir = theInt[1].dir --align it to the surface normal
local theFace = theInt[2] --get the face index
local theBary = theInt[3] --get the barycentric coordinates
local theMapFace = meshop.getMapFace theMesh theChannel theFace --get the map face
local theV1 = meshop.getMapVert theMesh theChannel theMapFace.x --get the map vertices
local theV2 = meshop.getMapVert theMesh theChannel theMapFace.y
local theV3 = meshop.getMapVert theMesh theChannel theMapFace.z
local theUV = theV1*theBary.x + theV2*theBary.y + theV3*theBary.z --interpolate the UVs at the intersection point
theUV = [removeTiling theUV.x, removeTiling theUV.y, removeTiling theUV.z] --remove the tiling
if useMap then --if we will be using the bitmap,
(
local theColor = getPixels theBmp [theUV.x*theW, theH - theUV.y*(theBmp.height-1)] 1 --grab the pixel at the int. point
theBox.wirecolor = theColor[1] --and assign the result to the wirecolor of the box
)
else
(
theBox.wirecolor = theUV*255 --if no bitmap, assign the UVs as color (Red/Green gradient)
)
)--end if intersection valid
)--end y loop
)--end x loop
deleteModifier theObj theMS --delete the Mesh_Select modifier
delete theMesh --delete the TriMesh from memory
delete theBoxSource --delete the source box
)--end execute
)--end script
Since all boxes are instances of the same box, you can change the width, length and height after the fact to change the look of the "puzzle".
It will work with any geometry object, but does not follow its faces, it projects top-down, so if you have a teapot or sphere, the boxes will land in a grid on top of the object and will orient by its surface, a bit like snow falling and gathering on the object…
[img] http://forums.cgsociety.org/attachment.php?attachmentid=156324&stc=1 [/img]
Thanks for the code and suggestions, I managed to do what I wanted, although it was much harder than I expected:
--Map colors extractor
--
--INSTRUCTIONS
--Select any poly object with a diffuse map applied.
--Script will create a colored box on each poly face, using the average map vertex pixel color as wirecolor.
(
obj=$ --target poly object
subdivs=256 --resolution of the rendered diffuse texture, for pixel color extraction
theChannel=1 --uvw map channel
bitmapSamplingSize=1 -- 4 means 4x4 pixels are sampled per vertex/face center, this should improve the result with noisy textures. 1 is fastest (single pixel)
useVertexMapping=false --if true then face color is interpolated from vertex map colors, which is faster than sampling the (triangulated) face center, but less accurate
delete $Facebox*
box_size=50 --size of the colored boxes
fn removeTiling theVal = --compensate bitmap uv tiling for proper coordinates
(
if abs theVal > 1.0 do theVal = mod theVal 1.0
if theVal < 0.0 do theVal = 1.0+theVal
theVal
)
--returns single pixel color
fn getSinglePixelColor thisTexture textureCoords=
(
local vertColor = (getPixels thisTexture textureCoords 1)[1]
)
--returns average color of a sampled pixel matrix. sampleSize 4 means 4x4=16 pixel...out of range pixels are ignored.
fn getAveragePixelColor thisTexture textureCoords sampleSize:4=
(
if sampleSize<2 then sampleSize=2
local textureWidth=thisTexture.width-1
local textureHeight=thisTexture.height-1
local pixelValues=#()
--read pixels
for row=1 to sampleSize do
(
local thisTextureCoords=[0,0]
thisTextureCoords.x=textureCoords.x-sampleSize/2
thisTextureCoords.y=textureCoords.y-sampleSize/2+row
--skip collecting when row out of range
if (thisTextureCoords.y<=textureHeight and thisTextureCoords.y>=0) do
(
local thisSampleSize=sampleSize
--check if column coordinates below zero, then clip
if thisTextureCoords.x<0 do
(
thisSampleSize+=thisTextureCoords.x --reduce sample size by negative out of range value
thisTextureCoords.x=0 --fix position
)
--check if sample size grows out of bitmap dimensions, then clip it
if (thisTextureCoords.x+sampleSize)>textureWidth do thisSampleSize=textureWidth-thisTextureCoords.x
local thisRowVertColors = getPixels thisTexture thisTextureCoords thisSampleSize
--format "% % %
" textureCoords thisTextureCoords thisRowVertColors
join pixelValues thisRowVertColors
)
)
--calculte single average color value
local averageColor=color 0 0 0
if pixelValues.count>0 do
(
for thisColor in pixelValues do
(
averageColor+=thisColor
)
averageColor=averageColor/pixelValues.count
)
)
--finds triangulated mesh faces in polygon faces
--works with a poly object and the same poly object converted to mesh
--returns array with mesh faces index and the corresponding poly face
fn meshFacesInPolyFaces polyObj meshObj=
(
local polyFaces=polyObj.faces as bitarray
local meshFaces=meshObj.faces as bitarray
--create collection of face vertices of each poly face
local polyFaceVerts = for f in polyFaces collect polyop.getFaceVerts polyObj f as bitarray
local meshFacesInPolyFaces=#()
--loop all mesh faces
for f=1 to meshFaces.numberset do
(
local thisVerts=meshop.getVertsUsingFace meshObj f
--loop all poly faces to find current mesh face in poly face
local notFound = true
for i=1 to polyFaces.numberset while notFound do
(
local thisPolyFace=polyFaceVerts[i]
--subtract current mesh face vertices from poly face vertices
local thisRemainingVertices=thisPolyFace-thisVerts
--check if all mesh vertices were subtracted, if true then this mesh face is part of the poly face
if (thisRemainingVertices.numberset==thisPolyFace.numberSet-thisVerts.numberset) do
(
--store corresponding poly face for current mesh face
meshFacesInPolyFaces[f]=i
notFound=false
)
)
)
--format "%
" meshFacesInPolyFaces
meshFacesInPolyFaces
)
fn getVertexPositionMapColors obj f thisTexture textureWidth textureHeight theChannel:1 bitmapSamplingSize:1=
(
--------------------------------------------------
--collect all map vertex position colors of current face
--------------------------------------------------
local thisMapVerts = polyOp.getMapFace obj theChannel f
local thisMapVertsColors = for v in thisMapVerts collect
(
--get single map vertex coordinates
local uvCoords = polyOp.getMapVert obj theChannel v
--convert vertex coords to bitmap coords
local textureCoords = [(removeTiling uvCoords.x)*textureWidth,textureHeight-(removeTiling uvCoords.y)*textureHeight]
--get bitmap pixel value with bitmap coords
if bitmapSamplingSize==1 then vertColor = getSinglePixelColor thisTexture textureCoords
else vertColor = getAveragePixelColor thisTexture textureCoords
)
--------------------------------------------------
--------------------------------------------------
--calculate average face color from vertex position colors
--------------------------------------------------
local thisAverageFaceColor=color 0 0 0 --init empty color
local previousColor=color 0 0 0
for c in thisMapVertsColors do --loop all map vertices of face
(
--if c==undefined then c=color 255 0 255 --out of range value (pink)
if c==undefined then c=previousColor --make out of range color same value as previous vertex
thisAverageFaceColor+=c --add vertex color
--cache last color tor use as possible out-of-range values
previousColor=c
)
thisAverageFaceColor=thisAverageFaceColor/thisMapVertsColors.count --make average
--------------------------------------------------
)
fn getFaceCenterMapColors obj meshObj thisMeshFacesInPolyFace thisTexture textureWidth textureHeight theChannel:1 bitmapSamplingSize:1=
(
--------------------------------------------------
--collect all map vertex position colors of current face
--------------------------------------------------
--loop tri mesh faces of current poly
local thisMapFaceColors = for f in thisMeshFacesInPolyFace collect
(
local theBary=meshop.getBaryCoords meshObj f (meshop.getFaceCenter meshObj f) --get single face center coordinates
local theMapFace = meshop.getMapFace meshObj theChannel f --get the map face
local theV1 = meshop.getMapVert meshObj theChannel theMapFace.x --get the map vertices
local theV2 = meshop.getMapVert meshObj theChannel theMapFace.y
local theV3 = meshop.getMapVert meshObj theChannel theMapFace.z
local uvCoords = theV1*theBary.x + theV2*theBary.y + theV3*theBary.z --interpolate the UVs at the intersection point
--convert face center coords to bitmap coords
local textureCoords = [(removeTiling uvCoords.x)*textureWidth,textureHeight-(removeTiling uvCoords.y)*textureHeight]
if bitmapSamplingSize==1 then vertColor = getSinglePixelColor thisTexture textureCoords
else vertColor = getAveragePixelColor thisTexture textureCoords
)
--------------------------------------------------
--------------------------------------------------
--calculate average face color from vertex position colors
--------------------------------------------------
local thisAverageFaceColor=color 0 0 0 --init empty color
local previousColor=color 0 0 0
for c in thisMapFaceColors do --loop all map vertices of face
(
--if c==undefined then c=color 255 0 255 --out of range value (pink)
if c==undefined then c=previousColor --make out of range color same value as previous vertex
thisAverageFaceColor+=c --add face color
--cache last color tor use as possible out-of-range values
previousColor=c
)
thisAverageFaceColor=thisAverageFaceColor/thisMapFaceColors.count --make average
--------------------------------------------------
)
fn is_poly obj=
(
targetClass=classOf obj
targetClass==Editable_poly or targetClass==PolyMeshObject
)
if is_poly obj then
(
--render map into bitmap with given resolution
local thisTexture = renderMap obj.material.diffuseMap size:[subdivs,subdivs] filter:on
local textureWidth=thisTexture.width-1
local textureHeight=thisTexture.height-1
--need mesh for map face coordinates
local meshObj = snapshotAsmesh obj --grab the geometry of the object into memory
local meshFacesInPolyFaces=meshFacesInPolyFaces obj meshObj --get corresponding triangulated mesh faces of converted poly
targetFaces = obj.faces as bitarray
--loop all faces to find map vertices
for f in targetFaces do
(
--get colors from averaged vertex positions
if useVertexMapping then thisAverageFaceColor=getVertexPositionMapColors obj f thisTexture textureWidth textureHeight theChannel:theChannel bitmapSamplingSize:bitmapSamplingSize
else
--get colors from face center positions
(
--find triangulated mesh faces for current poly
thisMeshFacesInPolyFace=for i=1 to meshFacesInPolyFaces.count where meshFacesInPolyFaces[i] == f collect i
--get average color of mesh face centers
thisAverageFaceColor=getFaceCenterMapColors obj meshObj thisMeshFacesInPolyFace thisTexture textureWidth textureHeight theChannel:theChannel bitmapSamplingSize:bitmapSamplingSize
)
--create a box on every face
dir = polyop.getFaceNormal obj f
pos = polyop.getFaceCenter obj f
thisBox=box width:box_size length:box_size height:box_size name:("Facebox" + (i as string)) pos:pos dir:dir wirecolor:thisAverageFaceColor
)
)
else
(
format "Select Poly Object with diffuse map"
)
)
changes:
-) Instead of average vertex map colors, the face center is used for sampling the texture pixel.
-) Option to sample a bitmap cluster instead of single pixel.
-) Tiled, mirrored and rotated maps are correctly interpreted.
bugs, problems:
-) Real World Mapping is not supported, I have no clue how this works.
-) The whole script could be faster…The above picture takes a few seconds to calculate the boxes.
I created this function to collect tri faces from a poly object and a mesh snapshot, to be able to use barycentric coordinates (face center instead of vertex):
--finds triangulated mesh faces in polygon faces
--works with a poly object and the same poly object converted to mesh
--returns array with mesh faces index and the corresponding poly face
fn meshFacesInPolyFaces polyObj meshObj=
(
local polyFaces=polyObj.faces as bitarray
local meshFaces=meshObj.faces as bitarray
--create collection of face vertices of each poly face
local polyFaceVerts = for f in polyFaces collect polyop.getFaceVerts polyObj f as bitarray
local meshFacesInPolyFaces=#()
--loop all mesh faces
for f=1 to meshFaces.numberset do
(
local thisVerts=meshop.getVertsUsingFace meshObj f
--loop all poly faces to find current mesh face in poly face
local notFound = true
for i=1 to polyFaces.numberset while notFound do
(
local thisPolyFace=polyFaceVerts[i]
--subtract current mesh face vertices from poly face vertices
local thisRemainingVertices=thisPolyFace-thisVerts
--check if all mesh vertices were subtracted, if true then this mesh face is part of the poly face
if (thisRemainingVertices.numberset==thisPolyFace.numberSet-thisVerts.numberset) do
(
--store corresponding poly face for current mesh face
meshFacesInPolyFaces[f]=i
notFound=false
)
)
)
--format "%
" meshFacesInPolyFaces
meshFacesInPolyFaces
)
local polyObj=$
local meshObj=snapshotasmesh polyObj
meshFacesInPolyFaces=meshFacesInPolyFaces polyObj meshObj
format "%" meshFacesInPolyFaces
Please tell me that this is madness and you can solve it with one line, because that’s what I expected before I started digging into it.