Projecting Points from Object/World Space into Texture Space
-
Dear Community,
this question reached us via email-support in the context of C++, but I thought the answer might be interesting for other users too.
The underlying question in this case was how to project points from object or world space into the texture space of an object with UV data. I am showing here deliberately an approach that can be followed both in C++ and Python, so that all users can benefit from this. In C++ one has also the option of using VolumeData and its methods
VolumeData::GetUvw
orVolumeData::ProjectPoint
but must then either implement a volume shader (as otherwise the volume data attached to theChannelData
passed toShaderData::Output
will benullptr
), or useVolumeData:: AttachVolumeDataFake
to access::ProjectPoint
. There is however no inherent necessity to take this shader bound route as shown by the example.Cheers,
FerdinandResult
The script has created a texture with red pixels for the intersection points of the rays cast from each vertex of the spline towards the origin of the polygon object. The script also created the null object rays to visualize the rays which have been cast.
raycast_texture.c4d : The scene file.
Code
You must save the script to disk before running it, as the script infers from the script location the place to save the generated texture to.
"""Demonstrates how to project points from world or object space to UV space. This script assumes that the user has selected a polygon object and a spline object in the order mentioned. The script projects the points of the spline object onto the polygon object and creates a texture from the UV coordinates of the projected points. The texture is then applied to the polygon object. The script uses the `GeRayCollider` class to find the intersection of rays cast from the points of the spline object to the polygon object. The UV coordinates of the intersection points are then calculated using the `HairLibrary` class. In the C++ API, one should use maxon:: GeometryUtilsInterface::CalculatePolygonPointST() instead. Finally, using GeRayCollider is only an example for projecting points onto the mesh. In practice, any other method can be used as long as it provides points that lie in the plane(s) of a polygon. The meat of the example is in the `main()` function. The other functions are just fluff. """ import os import c4d import mxutils import uuid from mxutils import CheckType doc: c4d.documents.BaseDocument # The currently active document. op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`. def CreateTexture(points: list[c4d.Vector], path: str, resolution: int = 1000) -> None: """Creates a texture from the given `points` and saves it to the given `path`. Parameters: path (str): The path to save the texture to. points (list[c4d.Vector]): The points to create the texture from. """ # Check the input values for validity. if os.path.exists(path): raise FileExistsError(f"File already exists at path: {path}") if not path.endswith(".png"): raise ValueError("The path must end with '.png'.") # Create a drawing canvas to draw the points on. canvas: c4d.bitmaps.GeClipMap = CheckType(c4d.bitmaps.GeClipMap()) if not canvas.Init(resolution, resolution, 24): raise MemoryError("Failed to initialize GeClipMap.") # Fill the canvas with white. canvas.BeginDraw() canvas.SetColor(255, 255, 255) canvas.FillRect(0, 0, resolution, resolution) # Draw the points on the canvas. canvas.SetColor(255, 0, 0) for p in points: x: int = int(p.x * resolution) y: int = int(p.y * resolution) x0: int = max(0, x - 1) y0: int = max(0, y - 1) x1: int = min(resolution, x + 1) y1: int = min(resolution, y + 1) canvas.FillRect(x0, y0, x1, y1) canvas.EndDraw() # Save the canvas to the given path. bitmap: c4d.bitmaps.BaseBitmap = CheckType(canvas.GetBitmap()) bitmap.Save(path, c4d.FILTER_PNG) c4d.bitmaps.ShowBitmap(bitmap) def ApplyTexture(obj: c4d.BaseObject, path: str) -> None: """Applies the texture at the given `path` to the given `obj`. """ CheckType(obj, c4d.BaseObject) # Check the input values for validity. if not os.path.exists(path): raise FileNotFoundError(f"File does not exist at path: {path}") # Create a material and apply the texture to it. material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial), c4d.BaseMaterial) obj.GetDocument().InsertMaterial(material) shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap), c4d.BaseShader) shader[c4d.BITMAPSHADER_FILENAME] = path material.InsertShader(shader) material[c4d.MATERIAL_COLOR_SHADER] = shader material[c4d.MATERIAL_PREVIEWSIZE] = c4d.MATERIAL_PREVIEWSIZE_1024 # Apply the material to the object. tag: c4d.TextureTag = CheckType(obj.MakeTag(c4d.Ttexture)) tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW tag[c4d.TEXTURETAG_MATERIAL] = material def CreateDebugRays(spline: c4d.SplineObject, p: c4d.Vector) -> None: """Adds spline objects to the document to visualize the rays from the given `p` to the points of the given `spline`. """ doc: c4d.documents.BaseDocument = CheckType(spline.GetDocument(), c4d.documents.BaseDocument) rays: c4d.BaseObject = c4d.BaseObject(c4d.Onull) rays.SetName("Rays") doc.InsertObject(rays) for q in spline.GetAllPoints(): ray: c4d.SplineObject = c4d.SplineObject(2, c4d.SPLINETYPE_LINEAR) ray.SetPoint(0, p) ray.SetPoint(1, q * spline.GetMg()) ray.Message(c4d.MSG_UPDATE) ray.InsertUnder(rays) def main() -> None: """Carries out the main logic of the script. """ # Check the object selection for being meaningful input. selected: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER) if (len(selected) != 2 or not selected[0].CheckType(c4d.Opolygon) or not selected[1].CheckType(c4d.Ospline)): raise ValueError("Please select a polygon object and a spline object.") polygonObject, splineObject = selected # Get the uvw tag, the points, and the polygons of the polygon object. uvwTag: c4d.UvwTag = mxutils.CheckType(polygonObject.GetTag(c4d.Tuvw)) points: list[c4d.Vector] = [polygonObject.GetMg() * p for p in polygonObject.GetAllPoints()] polys: list[c4d.CPolygon] = polygonObject.GetAllPolygons() # We are casting here in a dumb manner towards the center of the polygon object. In practice, # one should cast rays towards the plane of the polygon object. Or even better, use another # method to project the points onto the polygon object, as GeRayCollider is not the most # efficient thing in the world. rayTarget: c4d.Vector = polygonObject.GetMg().off CreateDebugRays(splineObject, rayTarget) # Initialize the GeRayCollider to find the intersection of rays cast from the points of the # spline object to the polygon object. collider: c4d.utils.GeRayCollider = c4d.utils.GeRayCollider() if not collider.Init(polygonObject): raise MemoryError("Failed to initialize GeRayCollider.") # Init our output list and iterate over the points of the spline object. uvPoints: list[c4d.Vector] = [] for p in splineObject.GetAllPoints(): # Transform the point from object to world space (q) and then to the polygon object's space # (ro). Our ray direction always points towards the center of the polygon object. q: c4d.Vector = splineObject.GetMg() * p ro: c4d.Vector = ~polygonObject.GetMg() * q rd: c4d.Vector = rayTarget - ro # Cast the ray and check if it intersects with the polygon object. if not collider.Intersect(ro, rd, 1E6) or collider.GetIntersectionCount() < 1: continue # Get the hit position and the polygon ID of the intersection. hit: dict = collider.GetNearestIntersection() pos: c4d.Vector = mxutils.CheckType(hit.get("hitpos", None), c4d.Vector) pid: int = mxutils.CheckType(hit.get("face_id", None), int) # One mistake would be now to use the barycentric coordinates that are in the intersection # data, as Cinema uses an optimized algorithm to interpolate in a quad and not the standard # cartesian-barycentric conversion. In Python these polygon weights are only exposed in a # bit weird place, the hair library. In C++ these barycentric coordinates make sense because # there exist methods to convert them to weights. In Python the barycentric coordinates are # pretty much useless as we do not have such a conversion function here. # Compute the weights s, t for the intersection point in the polygon. s, t = c4d.modules.hair.HairLibrary().GetPolyPointST( pos, points[polys[pid].a], points[polys[pid].b], points[polys[pid].c], points[polys[pid].d], True) # Get the uv polygon and bilinearly interpolate the coordinates using the weights. It would # be better to use the more low-level variable tag data access functions in VariableTag # than UvwTag.GetSlow() in a real-world scenario. uvw: list[c4d.Vector] = list(uvwTag.GetSlow(pid).values()) t0: c4d.Vector = c4d.utils.MixVec(uvw[0], uvw[1], s) t1: c4d.Vector = c4d.utils.MixVec(uvw[3], uvw[2], s) uv: c4d.Vector = c4d.utils.MixVec(t0, t1, t) # Append the UV coordinates to the output list. uvPoints.append(uv) # Write the UV coordinates to a texture and apply it to the polygon object. path: str = os.path.join(os.path.dirname(__file__), f"image-{uuid.uuid4()}.png") CreateTexture(uvPoints, path, resolution=1024) ApplyTexture(polygonObject, path) c4d.EventAdd() if __name__ == '__main__': main()