|
| 1 | +#coding: utf-8 |
| 2 | +"""Explains how to generate UVW data for polygon objects. |
| 3 | +
|
| 4 | +Can be run as Script Manager script with no scene state requirements. |
| 5 | +
|
| 6 | +Topics: |
| 7 | + * Manually creating uvw data via `UVWTag`. |
| 8 | + * Using `CallUVCommand` to use higher level UVW functions built into Cinema 4D. |
| 9 | +
|
| 10 | +Examples: |
| 11 | + * GenerateUvwData(): Demonstrates how to generate UVW data for polygon objects. |
| 12 | +
|
| 13 | +Overview: |
| 14 | + UV(W) data is a set of coordinates that are used to map a texture onto a polygon object. The |
| 15 | + coordinates are stored in a `UVWTag` instance, which is attached to a `PolygonObject`. Each |
| 16 | + polygon in the object has an uv coordinate for each of its vertices, i.e., the number of uv |
| 17 | + coordinates is four times the polygon count for an object consisting entirely of quads. This |
| 18 | + also means a vertex can have multiple uv coordinates, one for each polygon it is part of. |
| 19 | +
|
| 20 | +See also: |
| 21 | + 04_3d_concepts/modeling/uvw_tag : More technical examples for UVW tag. |
| 22 | + 05_modules/bodypaint: More examples for how to deal with UVs and UV commands. |
| 23 | +""" |
| 24 | +__author__ = "Ferdinand Hoppe" |
| 25 | +__copyright__ = "Copyright (C) 2025 MAXON Computer GmbH" |
| 26 | +__date__ = "05/03/2025" |
| 27 | +__license__ = "Apache-2.0 License" |
| 28 | +__version__ = "2025.1.0" |
| 29 | + |
| 30 | +import c4d |
| 31 | +import typing |
| 32 | +import mxutils |
| 33 | + |
| 34 | +doc: c4d.documents.BaseDocument # The currently active document. |
| 35 | +op: typing.Optional[c4d.BaseObject] # The selected object within that active document. Can be None. |
| 36 | + |
| 37 | +def GenerateUvwData(geometries: tuple[c4d.PolygonObject]) -> None: |
| 38 | + """Demonstrates how to generate UVW data for polygon objects.. |
| 39 | + """ |
| 40 | + def MapVector(value: c4d.Vector, inMin: c4d.Vector, inMax: c4d.Vector) -> c4d.Vector: |
| 41 | + """Maps a vector from a given range to the positive unit quadrant. |
| 42 | + """ |
| 43 | + # We swizzle the vector to only consider the x and z components, as UV are the relevant c |
| 44 | + # components for us and the x and z components happened to be the relevant ones for this |
| 45 | + # example (the plane object). |
| 46 | + return c4d.Vector( |
| 47 | + c4d.utils.RangeMap(value.x, inMin.x, inMax.x, 0, 1, True), |
| 48 | + c4d.utils.RangeMap(value.z, inMin.z, inMax.z, 0, 1, True), 0) |
| 49 | + |
| 50 | + def ProjectIntoPlane(p: c4d.Vector, q: c4d.Vector, normal: c4d.Vector) -> c4d.Vector: |
| 51 | + """Projects the point #p orthogonally into the plane defined by #q and #normal. |
| 52 | +
|
| 53 | + Args: |
| 54 | + p: The point to project. |
| 55 | + q: A point in the plane. |
| 56 | + normal: The normal of the plane (expected to be a normalized vector). |
| 57 | +
|
| 58 | + Returns: |
| 59 | + The projected point. |
| 60 | + """ |
| 61 | + # The distance from the point #p to its orthogonal projection #p' on the plane. Or, in short, |
| 62 | + # the length of the shortest path (in Euclidean space) from #p to the plane. |
| 63 | + distance = (p - q) * normal |
| 64 | + # Calculate #p' by moving #p #distance units along the inverse plane normal. |
| 65 | + return p - normal * distance |
| 66 | + |
| 67 | + # Check our inputs for being what we think they are, at least two PolygonObjects. |
| 68 | + mxutils.CheckIterable(geometries, c4d.PolygonObject, minCount=3) |
| 69 | + |
| 70 | + # Give the three inputs meaningful names. |
| 71 | + plane: c4d.PolygonObject = geometries[0] |
| 72 | + sphere: c4d.PolygonObject = geometries[1] |
| 73 | + cylinder: c4d.PolygonObject = geometries[2] |
| 74 | + |
| 75 | + # The simplest way to generate UVW data is the simple planar layout which can be directly |
| 76 | + # derived from point or construction data. An example is of course a plane object, but similar |
| 77 | + # techniques can also be applied when extruding or lofting a line, as we then can also associate |
| 78 | + # each point with a percentage of the total length of the line, and a percentage of the total |
| 79 | + # extrusion height or lofting rotation, the uv coordinates of that point. |
| 80 | + |
| 81 | + # Create a UVW tag for the plane object and get the points of the plane object. |
| 82 | + uvwTag: c4d.UVWTag = plane.MakeVariableTag(c4d.Tuvw, plane.GetPolygonCount()) |
| 83 | + points: typing.List[c4d.Vector] = plane.GetAllPoints() |
| 84 | + |
| 85 | + # Get the radius of the plane object and iterate over all polygons to calculate the uvw data. |
| 86 | + radius: c4d.Vector = plane.GetRad() |
| 87 | + poly: c4d.CPolygon |
| 88 | + for i, poly in enumerate(plane.GetAllPolygons()): |
| 89 | + # Calculate the uvw data for each point of the polygon. We operate here on the implicit |
| 90 | + # knowledge that the plane object is centered on its origin, e.g., goes form -radius to |
| 91 | + # radius in all three dimensions. We then just map -radius to radius to 0 to 1, as UV data |
| 92 | + # is always placed in the positive quadrant unit square ('goes' from 0 to 1). The reason why |
| 93 | + # always calculate uvw data for four points, is because Cinema 4D always handles polygons as |
| 94 | + # quads, even if they are triangles or n-gons. |
| 95 | + a: c4d.Vector = MapVector(points[poly.a], -radius, radius) |
| 96 | + b: c4d.Vector = MapVector(points[poly.b], -radius, radius) |
| 97 | + c: c4d.Vector = MapVector(points[poly.c], -radius, radius) |
| 98 | + d: c4d.Vector = MapVector(points[poly.d], -radius, radius) |
| 99 | + |
| 100 | + # Set the uvw data for the polygon, nothing special here, just setting the uvw data for the |
| 101 | + # polygon #i to the calculated values. |
| 102 | + uvwTag.SetSlow(i, a, b, c, d) |
| 103 | + |
| 104 | + # Now we basically do the same for the sphere object. We could also just take the x and z |
| 105 | + # components of each point to get a top-down uvw projection on the sphere. But we make it a |
| 106 | + # bit more interesting by projecting each point into a plane defined by the normal of (1, 1, 0), |
| 107 | + # resulting in planar projection from that angle. This is a bit more formal than projecting by |
| 108 | + # just discarding a component (the y component in our case). |
| 109 | + |
| 110 | + # Our projection orientation and point, there is nothing special about these values, they are |
| 111 | + # just look good for this example. |
| 112 | + projectionNormal: c4d.Vector = c4d.Vector(1, 1, 0).GetNormalized() |
| 113 | + projectionPoint: c4d.Vector = c4d.Vector(0) |
| 114 | + |
| 115 | + uvwTag: c4d.UVWTag = sphere.MakeVariableTag(c4d.Tuvw, sphere.GetPolygonCount()) |
| 116 | + points: typing.List[c4d.Vector] = sphere.GetAllPoints() |
| 117 | + |
| 118 | + radius: c4d.Vector = sphere.GetRad() |
| 119 | + poly: c4d.CPolygon |
| 120 | + for i, poly in enumerate(sphere.GetAllPolygons()): |
| 121 | + |
| 122 | + a: c4d.Vector = ProjectIntoPlane(points[poly.a], projectionPoint, projectionNormal) |
| 123 | + b: c4d.Vector = ProjectIntoPlane(points[poly.b], projectionPoint, projectionNormal) |
| 124 | + c: c4d.Vector = ProjectIntoPlane(points[poly.c], projectionPoint, projectionNormal) |
| 125 | + d: c4d.Vector = ProjectIntoPlane(points[poly.d], projectionPoint, projectionNormal) |
| 126 | + |
| 127 | + # We must still map the projected points to the unit square. What we do here is not quite |
| 128 | + # mathematically correct, as there is no guarantee that the projected points have the same |
| 129 | + # bounding box size as the original sphere, but eh, close enough for this example, we at |
| 130 | + # least map all values to [0, 1]. |
| 131 | + uvwTag.SetSlow(i, |
| 132 | + MapVector(a, -radius, radius), |
| 133 | + MapVector(b, -radius, radius), |
| 134 | + MapVector(c, -radius, radius), |
| 135 | + MapVector(d, -radius, radius)) |
| 136 | + |
| 137 | + # Lastly, we can use UVCommands to generate UVW data, here at the example of the cylinder object. |
| 138 | + # Doing this comes with the huge disadvantage that we must be in a certain GUI state, i.e., the |
| 139 | + # UV tools only work if the object is in the active document and the UV tools are in a certain |
| 140 | + # state. This makes it impossible to use the UV tools inside a generator object's GetVirtualObjects |
| 141 | + |
| 142 | + uvwTag: c4d.UVWTag = cylinder.MakeVariableTag(c4d.Tuvw, cylinder.GetPolygonCount()) |
| 143 | + |
| 144 | + # Boiler plate code for UV commands to work. |
| 145 | + doc: c4d.documents.BaseDocument = mxutils.CheckType(sphere.GetDocument()) |
| 146 | + doc.SetActiveObject(cylinder) |
| 147 | + |
| 148 | + oldMode: int = doc.GetMode() |
| 149 | + if doc.GetMode() not in [c4d.Muvpoints, c4d.Muvpolygons]: |
| 150 | + doc.SetMode(c4d.Muvpolygons) |
| 151 | + |
| 152 | + cmdTextureView: int = 170103 |
| 153 | + if not c4d.IsCommandChecked(cmdTextureView): |
| 154 | + c4d.CallCommand(cmdTextureView) |
| 155 | + c4d.modules.bodypaint.UpdateMeshUV(False) |
| 156 | + didOpenTextureView = True |
| 157 | + |
| 158 | + handle: c4d.modules.bodypaint.TempUVHandle = mxutils.CheckType( |
| 159 | + c4d.modules.bodypaint.GetActiveUVSet(doc, c4d.GETACTIVEUVSET_ALL)) |
| 160 | + |
| 161 | + # Retrieve the internal UVW data for the currently opened texture view and then invoke |
| 162 | + # the #UVCOMMAND_OPTIMALCUBICMAPPING command, mapping our cylinder object. |
| 163 | + uvw: list[dict] = mxutils.CheckType(handle.GetUVW()) |
| 164 | + settings: c4d.BaseContainer = c4d.BaseContainer() |
| 165 | + if not c4d.modules.bodypaint.CallUVCommand( |
| 166 | + handle.GetPoints(), handle.GetPointCount(), handle.GetPolys(), handle.GetPolyCount(), |
| 167 | + uvw, handle.GetPolySel(), handle.GetUVPointSel(), cylinder, handle.GetMode(), |
| 168 | + c4d.UVCOMMAND_OPTIMALCUBICMAPPING, settings): |
| 169 | + raise RuntimeError("CallUVCommand failed.") |
| 170 | + |
| 171 | + # Write the updated uvw data back and close the texture view we opened above. |
| 172 | + if not handle.SetUVWFromTextureView(uvw, True, True, True): |
| 173 | + raise RuntimeError("Failed to write Bodypaint uvw data back.") |
| 174 | + |
| 175 | + c4d.modules.bodypaint.FreeActiveUVSet(handle) |
| 176 | + doc.SetMode(oldMode) |
| 177 | + |
| 178 | + return None |
| 179 | + |
| 180 | +# The following code is boilerplate code to create a plane and sphere object, generate UVW data for |
| 181 | +# them, and apply a material to them. This is all boilerplate code and not the focus of this example. |
| 182 | + |
| 183 | +def BuildGeometry(doc: c4d.documents.BaseDocument) -> tuple[c4d.PolygonObject, c4d.PolygonObject]: |
| 184 | + """Constructs a plane and sphere polygon object. |
| 185 | + """ |
| 186 | + # Instantiate a plane and sphere generator. |
| 187 | + planeGen: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oplane), c4d.BaseObject) |
| 188 | + sphereGen: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Osphere), c4d.BaseObject) |
| 189 | + cylinderGen: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Ocylinder), c4d.BaseObject) |
| 190 | + |
| 191 | + # Insert the generators into a temporary document to build their caches. |
| 192 | + temp: c4d.documents.BaseDocument = c4d.documents.BaseDocument() |
| 193 | + temp.InsertObject(planeGen) |
| 194 | + temp.InsertObject(sphereGen) |
| 195 | + temp.InsertObject(cylinderGen) |
| 196 | + |
| 197 | + # Build the caches of the plane and sphere generators. |
| 198 | + if not temp.ExecutePasses(None, False, False, True, c4d.BUILDFLAGS_0): |
| 199 | + raise RuntimeError("Could not build the cache for plane and sphere objects.") |
| 200 | + |
| 201 | + # Retrieve the caches of the plane and sphere generators. |
| 202 | + planeCache: c4d.PolygonObject = mxutils.CheckType(planeGen.GetCache(), c4d.PolygonObject) |
| 203 | + sphereCache: c4d.PolygonObject = mxutils.CheckType(sphereGen.GetCache(), c4d.PolygonObject) |
| 204 | + cylinderCache: c4d.PolygonObject = mxutils.CheckType(cylinderGen.GetCache(), c4d.PolygonObject) |
| 205 | + |
| 206 | + # Clone the caches and remove the existing UVW tags from the clones. |
| 207 | + plane: c4d.PolygonObject = mxutils.CheckType(planeCache.GetClone(), c4d.PolygonObject) |
| 208 | + sphere: c4d.PolygonObject = mxutils.CheckType(sphereCache.GetClone(), c4d.PolygonObject) |
| 209 | + cylinder: c4d.PolygonObject = mxutils.CheckType(cylinderCache.GetClone(), c4d.PolygonObject) |
| 210 | + for node in [plane, sphere, cylinder]: |
| 211 | + uvwTag: c4d.UVWTag = node.GetTag(c4d.Tuvw) |
| 212 | + if uvwTag: |
| 213 | + uvwTag.Remove() |
| 214 | + |
| 215 | + # Set the global transform of each of them. |
| 216 | + plane.SetMg(c4d.Matrix(off=c4d.Vector(-300, 0, 0))) |
| 217 | + sphere.SetMg(c4d.Matrix(off=c4d.Vector(0, 0, 0))) |
| 218 | + cylinder.SetMg(c4d.Matrix(off=c4d.Vector(300, 0, 0))) |
| 219 | + |
| 220 | + # Insert the plane and sphere into the active document and return them. |
| 221 | + doc.InsertObject(plane) |
| 222 | + doc.InsertObject(sphere) |
| 223 | + doc.InsertObject(cylinder) |
| 224 | + |
| 225 | + return plane, sphere, cylinder |
| 226 | + |
| 227 | +def ApplyMaterials(geometries: tuple[c4d.PolygonObject]) -> None: |
| 228 | + """Applies a checkerboard material to the given #geometries. |
| 229 | + """ |
| 230 | + # Get the document from the geometries and at the same time ensure that the geometries are |
| 231 | + # at least three instances of PolygonObject. |
| 232 | + doc: c4d.documents.BaseDocument = mxutils.CheckIterable( |
| 233 | + geometries, c4d.PolygonObject, minCount=3)[0].GetDocument() |
| 234 | + |
| 235 | + # Enable the standard renderer in the document. |
| 236 | + renderData: c4d.BaseContainer = doc.GetActiveRenderData() |
| 237 | + renderData[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD |
| 238 | + |
| 239 | + # Create the checkerboard material and apply it to the geometries. |
| 240 | + material: c4d.BaseMaterial = mxutils.CheckType(c4d.BaseMaterial(c4d.Mmaterial), c4d.BaseMaterial) |
| 241 | + shader: c4d.BaseShader = mxutils.CheckType(c4d.BaseShader(c4d.Xcheckerboard), c4d.BaseShader) |
| 242 | + doc.InsertMaterial(material) |
| 243 | + |
| 244 | + material.InsertShader(shader) |
| 245 | + material[c4d.MATERIAL_COLOR_SHADER] = shader |
| 246 | + for geom in geometries: |
| 247 | + materialTag: c4d.BaseTag = geom.MakeTag(c4d.Ttexture) |
| 248 | + materialTag[c4d.TEXTURETAG_MATERIAL] = material |
| 249 | + materialTag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW |
| 250 | + |
| 251 | +def main(doc: c4d.documents.BaseDocument) -> None: |
| 252 | + """Runs the example. |
| 253 | + """ |
| 254 | + # Construct the plane, sphere, and cylinder geometry, then generate the UVW data, and apply a |
| 255 | + # material to the geometries, finally update the document with #EventAdd. Except for the |
| 256 | + # #GenerateUvwData call, this is all boilerplate code. |
| 257 | + geometries: tuple[c4d.PolygonObject] = BuildGeometry(doc) |
| 258 | + GenerateUvwData(geometries) |
| 259 | + ApplyMaterials(geometries) |
| 260 | + |
| 261 | + c4d.EventAdd() |
| 262 | + |
| 263 | +if __name__ == '__main__': |
| 264 | + c4d.CallCommand(13957) # Clear the console. |
| 265 | + # doc is a predefined module attribute as defined at the top of the file. |
| 266 | + main(doc) |
0 commit comments