Skip to content

Commit 22e2a8a

Browse files
committed
Python SDK: Added more example code for how to generate UVW data.
1 parent ae583c5 commit 22e2a8a

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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)

scripts/04_3d_concepts/modeling/geometry/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Deform caches can only be found on non-generator `PointObject` objects and they
2929
| geometry_caches_xxx.py | Explains the geometry model of the Cinema API in Cinema 4D. |
3030
| geometry_polygonobject_xxx.py | Explains the user-editable polygon object model of the Cinema API. |
3131
| geometry_splineobject_xxx.py | Explains the user-editable spline object model of the Cinema API. |
32+
| geometry_uvw_2025_xxx.py | Explains how to generate UVW data for polygon objects. |
3233
| operation_extrude_polygons_xxx.py | Demonstrates how to extend polygonal geometry at the example of extruding polygons. |
3334
| operation_flatten_polygons_xxx.py | Demonstrates how to deform points of a point object at the example of 'flattening' the selected polygons in a polygon object. |
3435
| operation_transfer_axis_xxx.py | Demonstrates how to 'transfer' the axis of a point object to another object while keeping its vertices in place. |

0 commit comments

Comments
 (0)