Notifications
Clear all

[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?

9 Replies
1 Reply
(@bobo)
Joined: 11 months ago

Posts: 0

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.

 JHN

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

 JHN

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!

 JHN

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.