@ferdinand : Yeah I will because that is definitely not what I'm seeing
Manually login in in both commandline/c4dpy works. As that is what we are doing now on all render nodes when the licenses vanish yet again.
Thanks!
@ferdinand : Yeah I will because that is definitely not what I'm seeing
Manually login in in both commandline/c4dpy works. As that is what we are doing now on all render nodes when the licenses vanish yet again.
Thanks!
@ferdinand When I was talking about Commandline I mean the actual Cinema4D app/.exe, not the terminal/powershell.
I will give the command a go Today and see how it goes. Thanks for the help regardless that it was outside of scope
Well, with your command on MacOS I basically get a wall of text but it does finish. However when I then manually run the C4D commandline again I get the standard "Enter the license method:" window
When I run it in c4dpy it ends on "Welcome to the world of C4D and python (....)" and >>>.
Cheers,
Thanks for adding the SDK info and also thanks for the hints, I will investigate further. We normally log in to Maxon accounts via the commandline or via the login window in Cinema, so it should work I imagine.
@ferdinand Due to the busy schedule at work I've only now gotten around to getting to implement this and I'm a bit stuck.
You mentioned in your reply "and we will also expose it in the technical and user documentation (in fact a few things should be updated there)." but I can't find anything in the SDK documentation about "g_license", just one quick mention in c4dpy about passing username and password as arguments.
Hi @ferdinand : Thanks a ton for helping out even though technically this kind of support is outside the scope.
I will read up on the material you provided and implement the code example into our own code.
As always you've been very helpful
Cheers,
Joep
@ferdinand : I will, thanks again for investigating
Cheers,
Joep
@ferdinand : Hi Ferdinand, thanks for getting back to me on this.
As for the legal aspects (the following is my personal opinion), what do you mean exactly?
We pay for licenses and we don't want the inconvenience of having to manually go through all render clients when the licensing system fails us yet again.
These actions (inputting license information on all commandline clients in our farm) cost man hours which can be more effectively spent working for clients rather than working around a needlessly cumbersome system.
There is no effective difference between me manually inputting the information on a client and doing it in an automated fashion with a piece of code.
That being said, if I'm missing something else here please feel free to correct my thinking.
Cheers,
Joep
Hi guys,
Running into a bit of an issue with the commandline application on Windows/MacOS when running and "driving" it via Python (standalone, outside Cinema4D).
We're writing an application that automatically handles the license logins on the machines in our renderfarms. Every time there is a minor update to Cinema or the Maxon App, all our machines seem to lose their license which is a lot of manual labor to get working again.
We have the following code:
import subprocess
import os
from os.path import expanduser
import time
# Petermine the platform (Windows or MacOS)
is_windows = os.name == 'nt'
# Path to your C4D executable
if is_windows:
try:
c4d_executable = r'C:\Program Files\Maxon Cinema 4D 2023\Commandline.exe'
except:
print("Commandline executablecould not be found!")
subprocess.wait(5)
else:
try:
c4d_executable = '/Applications/Maxon Cinema 4D 2023/Commandline.app/Contents/MacOS/Commandline'
except:
print("Commandline executablecould not be found!")
subprocess.wait(5)
# Define the license information to provide
license_method = "2"
license_username = "XXXXXXXX"
license_password = "XXXXXXXX"
license_floating_commandline = '1'
# Define license files
# Get user home directory
home = expanduser("~")
print(home)
# Create a subprocess to run C4D
c4d_process = subprocess.Popen(
[c4d_executable],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
text=True
)
# Use a loop to read and print the output in real-time
def capture_and_print_output(process):
while True:
output_line = process.stdout.readline()
if not output_line:
break # No more output, exit the loop
print(output_line, end='') # Print the line without adding extra newline
# Check for error messages in the output
if "Error:" in output_line:
print("Error message detected:", output_line)
while True:
error_line = process.stderr.readline()
if not error_line:
break # No more error output, exit the loop
print("Error:", error_line, end='')
# Capture and print outputs before providing license information
capture_and_print_output(c4d_process) # Output before license input
# Wait for Commandline to prompt for input
output, _ = c4d_process.communicate(timeout=10)
print("Output:", output)
if "Please select:" in output:
print("Inside the license method loop")
# Provide the license information
time.sleep(5)
c4d_process.stdin.write(license_method + '\n')
c4d_process.stdin.flush()
print("sleeping")
time.sleep(2)
# Capture and print outputs after providing license method
capture_and_print_output(c4d_process)
# Send the Enter key
c4d_process.stdin.write('\n')
c4d_process.stdin.flush()
print("sleeping after Enter")
time.sleep(2)
# Capture and print outputs after providing license method
capture_and_print_output(c4d_process)
time.sleep(2)
# Capture and print outputs after providing license method
capture_and_print_output(c4d_process)
if "Account email address []: " in output:
print("Inside the email address loop")
# Provide the license information
c4d_process.stdin.write(license_username)
c4d_process.stdin.flush()
# Send the Enter key
c4d_process.stdin.write('\n')
c4d_process.stdin.flush()
# Capture and print outputs after providing email address
capture_and_print_output(c4d_process)
if "Password: " in output:
print("Inside the password loop")
# Provide the license information
c4d_process.stdin.write(license_password)
c4d_process.stdin.flush()
# Send the Enter key
c4d_process.stdin.write('\n')
c4d_process.stdin.flush()
# Capture and print outputs after providing password
capture_and_print_output(c4d_process)
if "Please choose one of the following options: " in output:
print("Inside the license type loop")
# Provide type of license
c4d_process.stdin.write(license_floating_commandline)
c4d_process.stdin.flush()
# Send the Enter key
c4d_process.stdin.write('\n')
c4d_process.stdin.flush()
# Capture and print outputs after providing license type
capture_and_print_output(c4d_process)
else:
print("No input detected!")
c4d_process.wait(5)
c4d_process.stdin.close()
c4d_process.stdout.close()
c4d_process.stderr.close()
c4d_process.terminate()
# You can now continue working with the C4D process or wait for it to finish
print("Licensing process ran succesfully")
c4d_process.wait(5)
# Close the subprocess and clean up
c4d_process.stdin.close()
c4d_process.stdout.close()
c4d_process.stderr.close()
c4d_process.terminate()
Basically what happens is that the commandline application doesn't seem to register the "replies" that Python gives it when it asks for specific license information inputs. I've tried a ton of different ways to go about it but none seem to work.
Tried with sleep, without sleep, "Please select: ", "Please select:", "Enter the license method:", "Enter the license method: ", with \n, without \n etc.
Is there some specific timing I need to adhere to or something else behind the scenes that I'm not aware of?
Kind regards
Hi @ferdinand , thanks a ton for the very extensive reply. I do understand where you are coming from decision/code wise and that it might indeed be intended behavior, but I also appreciate you flagging this behavior to be looked at!
I will look into the code you provided and see what I can make of it to work for our specific case. As with all the examples i receive on here it is also about learning itself, to become better.
I already found out that I can run my piece of code 2 times to catch the first set of instances, perhaps it is also possible to run that on a loop until the len() doesn't increase anymore It's a hack-job but it might be a good last resort.
To address the first comment about the post itself, my reasoning was that since it was about GetAllAssetsNew which we talked about last, I added it to my post to keep it all together.
But I can see how it deviated too much from the original question that started the topic, my apologies.
Cheers,
@ferdinand : I've been working for a while with the solution of GetAllAssetsNew now but Today I noticed an issue:
When a texture is used in multiple shaders, in the "Project Asset Inspector" it has a "+" before "Asset". However when I pull that texture with GetAllAssetsNew it seems that is only displaying 1 "owner" shader, even though in reality it's multiple.
When I then loop over all texture assets to convert the paths from local to global, the extra owners keep their local paths and it's only changed for one of the owner shaders.
In my example I have 4 images assinged to 5 shaders. When I pull a len() on the list of assets I get 4. Then when I convert the paths to global, one of the shaders get's left behind and in the Project Asset Inspector the "+" vanishes and I now see 5 textures instead of 4. Same goes if I then pull a len() on the list again, it now says 5.
How can I make sure that it makes paths global on all owners of the texture?
Cheers,
Joep
Code:
def globalizeTextures():
doc = c4d.documents.GetActiveDocument()
textures = []
c4d.documents.GetAllAssetsNew(doc, 0, "", flags=c4d.ASSETDATA_FLAG_TEXTURESONLY,
assetList=textures)
print("The numder of textures is: ", len(textures))
print("Textures are: ", textures)
# Extract filename from all textures and put that name into the owner (shader) using the bitmap
for texture in textures:
print(texture)
filePath = texture
file = os.path.split(filePath["filename"])
workPath = doc.GetDocumentPath()
globalPath = str(os.path.join(workPath, "tex"))
# Check whether the texture file is in the texture (sub)folder(s)
for texture in os.walk(globalPath):
i = 0
for filename in texture[2]:
# print("Texture in tex folder is: ", filename)
# Check if the filenames match between the file in the shader and the file on the
# network
if (file[1] == filename):
globalPath = str(os.path.join(str(globalPath),str(filename)))
owner = filePath["owner"]
print("Owner is: ",filePath["owner"])
returnShader(owner, globalPath)
else:
i += 1
if (i == len(texture[2])):
if not filePath["exists"]:
print("This file ", file[1], " does not exist!")
else:
print("File ", file[1], " not found in the tex folder of the project, use "
"'Localize texture paths' first to copy the file to the tex folder "
"of the project")
updateScene()
return True
edit: forked form https://developers.maxon.net/forum/topic/14732/ by Ferdinand.
To have your code not "messed up", you should markdown formating.
```
def foo: return 3.14
```
which would render as
def foo: return 3.14
@ferdinand : The GetAllAssetsNew is a far more elegant solution for the problem I am trying to solve and I've rewritten the code so that it works now. We use Corona render and Redshift so it's a bonus that this works with pretty much all materials.
Thank you very much for the help, I will put your information about the layer shader in my database for future reference as I'm sure it will become helpful in another project!
Kind regards,
Joep
Hi @ferdinand
First of all thank you very much for the extensive reply, I haven't had the time yet to put the code intro practice but I wanted to answer your question on what I wanted to do with my code first.
I haven't had much effective code since I can't get to the actual bitmap's filepath in the layer shader, but this is what I have, sorry for the ton of print statements but I need to figure out what the code was doing:
if (i.GetType() == 1011123):
print ("Layershader found!")
print("i inside layershader is ", i)
layerShader = i
layer = layerShader.GetFirstLayer()
while layer.GetNext():
print("Layer is: ", layer)
print("Layer name: ", layer.GetName(doc))
print("Layer type is: ", layer.GetType())
print("Layer parameter is: ", layer.GetParameter(0))
print("Next layer is: ", layer.GetNext())
nextLayer = layer.GetNext()
print("Layer name: ", nextLayer.GetName(doc))
print("Layer type is: ", nextLayer.GetType())
print("Layer parameter is: ", nextLayer.GetParameter(0))
print("Next layer is: ", nextLayer.GetNext())
print("-----------------------------------------------------")
if (layer.GetType() == 5833) or (layer.GetType() == 1036473):
print("Bitmap found in layer!")
file = os.path.split(shader(i))
print("file is: ", file[1])
if layer.GetNext():
layer = layer.GetNext()
else:
pass
else:
print("No bitmap found in layer!")
if layer.GetNext():
layer = layer.GetNext()
else:
pass
What I want to do in code is this:
So far I've tackled the first 3 things but I'm running into issues with the 4th as I can't read out the texture path from a layer in the layer shader it seems.
Hi guys,
Got a question regarding traversing a layer shader with Python.
For an internal plugin I need to go through shaders and localize/globalize the file paths in the bitmaps.
When I run into a layer shader I want to get into said shader and traverse all layers, checking if it's a bitmap and if so localize/globalize the path.
However, the SDK documentation is very sparse for the layer shader itself, specifically the "LayerShaderLayer.GetParameter(self, id)", it says to look at the C++ documentation for ID's, which I did.
For instance "For shaders (TypeShader): LAYER_S_PARAM_SHADER"
Neither:
2 comes back as "none" and the rest gives an attribute error.
0 also comes back as "none".
I imagine that the GetParameter is used to get the file path of a layer if it's a bitmap, if not, what should I use?
The LayerShaderLayer only seems to have: GetNext, GetType, GetName, GetPreview, GetParameter, SetParameter.
When I run a GetType on the layer, it gives me back "2" which is "TypeShader" and with GetName it returns the name of the Bitmap in that layer.
But how do I then get to the actual file path inside that layer to change it?
Kind regards,
Joep
Hi Ferdinand,
Let me reply on the company account as I'm not at home, thank you very much for this information.
This is a much more simplified and clean way to go about it indeed, I did spend some time on Google before composing my code and even asking here.
Thank you for answering even though it is out of the scope of this forum technically, much appreciated.
I have tons of C4D python code that works, so no questions about that so far
I imagine this will work just fine, going to adapt it into my script.
@ferdinand : Thank you for the pointers to the articles, and I fully understand that you do not have the time to explain the more complex parts of the Matrix concepts.
I will read the material you have provided and otherwise search for info on Linear Algebra to get a better undertstanding.
I think I understand it a bit better now, basically those 3 numbers per axis (X,Y,Z) basically tell you along which world axis they lie.
So the main columns are X,Y,Z and then each row is technically X,Y,Z as well.
X Y Z
X X X
Y Y Y
Z Z Z
With the help of Cairyn I've figured out that the issue is with the transforms of parents and grandparents, so I will rewrite my code to remove all of those as well and this should hopefully fix the issue.
Thank you very much for your help and should I run into further issues on this, I will post again in this thread!
Kind regards,
@ferdinand Thanks again for this extensive explanation, I will be sure to keep this at hand for reference.
I do indeed grasp the concepts of multiple layers of coordinate systems but my issue specifically with the Matrix manual is that when I look at for instance the second image, with the nice colored columns for each component (Or so it would seem to me).
Then when I see the 3rd image I see the number "1" jump colums which is the part that makes it confusing to me. I believe in the dabble I had with matrices that default "1" is the scale but I don't grasp why it doesn't just stay in one of the colored columns.
Another time when I read out the matrix of an object that had rotations on it, the matrix showed everything 0 apart from those 3x default 1 jumping columns.
So it appeared the matrix did not have any rotational values stored.
Again, this confused me.
To be open and honest here, I'm a visual artist that has started to do coding to make internal company tools and while I have an ok understanding of math some things like this I can't yet visualise for myself so I don't fully understand it yet.
Kind regards,
Joep
The parent doesn't have the transforms but the grandparent indeed does, which seems to be the issue indeed.
I've already got my code to move the points around to the old relative transformations so I'll re-use that one for these purposes.
https://developers.maxon.net/forum/topic/13840/object-size-not-updated-after-points-move
The Subdivision Surface object two up has rotation on it, below that is a null with only a scale of 1.3 and inside that null is my object.
So if I store the points in my object and then reset the rotation of the SubS and scale of the null, and then put them back, all should be good.
I'm making an in-house turntable tool to isolate out objects in complex scenes and I get to see all kinds of weird modelling actions from team members and externals that I need to catch and correct This is one of them.
But say I wanted to rotate the pivot point to offset parent(s) transformations, how would I go about doing that in code?
@ferdinand : Thanks a lot for replying in such an extensive manner, that code was really helpful in it's own way but for other purposes.
Your code actually rotates and moves my object, which I do not want. I want it to remain in exactly the same location and orientation but I want it's axis system to match up to that of the world.
It seems I wasn't detailed enough in my initial question:
The object has no rotational transforms (all are 0) but the Pivot ("Enable Axis" button) is incorrectly oriented. The objects' local coordinate system does not match up with the worlds' coordinate system in terms of orientation (X-axis, Y-axis, Z-axis).
Normally, as a human, one would press the "Enable Axis" button and manually rotate the objects'' Pivot point to match the orientation of the world axis. So that Y is up, etc.
How would one do this in Python code?
Currently the Y axis of the object is on the worlds' X axis, so if I call the GetRad() function, the width of the object is actually put in the Y component of the c4d.Vector, not in the X where it should belong.
Edit: I have read up on the matrix manual but it's confusing to me, but that is a different subject.
Kind regards,
Hi there,
I've got a question that I can't seem to solve myself and neither is Google being of much help.
I've got an object of which it's local axis are not aligned with the world axis.
In this case:
The y of the object is on the x of the scene
The x of the object is on the -z of the scene
The z of the object is on the -y of the scene
I'm trying to build a custom bounding box for an internal tool and as you can imagine, reading of GetRad along completely wrong axis will give me an incorrect bounding box.
So I will need to align the axis of the object to the world axis first and then do my GetRad.
Normally as human, one would rotate the pivot point of the object to align with the world, however I would like to do this in code, as this will be part of a recursive loop going through a list of objects.
I've tried fiddling around with the matrices but this did not result in anything.
Kind regards,
Well, that indeed fixed the issue, now the size gets properly updated!
Thank you very much for the help