TheRaytracers / freecad-povray-render

FreeCAD workbench to render images of your model easily.
GNU Lesser General Public License v2.1
6 stars 1 forks source link

scriptability of the povray workbench #2

Open sdementen opened 3 years ago

sdementen commented 3 years ago

I am testing this workbench and it looks promising. I would like to automate what I am doing now through the UI by scripting it via python. Is it possible ? and if so, would you have some doc/examples to do so ? at the end, I would like to set via scripting: lights, camera (instead of depending of the current view), running the render, retrieving the rendered image and opening it in freecad (I am creating/generating automatically document/objects in freecad and I would like also to automate the rendering part).

TheRaytracers commented 3 years ago

Thanks for your interest on our workbench :)

Roughly I would do it the following way:

Importing Workbench Modules

For the following you need some of the modules of the workbench, so add the directory where the workbench installed to the python paths (unfortunately I found no better way to import these modules):

import os
import sys
# os.path.join(App.getUserAppDataDir(), "exporttopovray") can become something like this:
# "/home/username/.FreeCAD/exporttopovray"
sys.path.append(os.path.join(App.getUserAppDataDir(), "exporttopovray"))

Insert Lights

For inserting lights, two possible ways come into my mind:

  1. Just run the command to insert a light via python and then get the created object.

    Gui.runCommand('PointLightCommand',0)
    # get the created object
  2. Do the same thing as the light insert command does in the background; in my opinion it’s easier to get the created light with this version:

    from Lights import PointLight
    from Lights import ViewProviderPointLight
    
    light = App.ActiveDocument.addObject("Part::FeaturePython","PointLight")
    PointLight(light)
    ViewProviderPointLight(light.ViewObject)
    App.ActiveDocument.recompute()
    
    # now you can manipulate properties of the light:
    light.Placement.Base.x = 15

Of course you can replace PointLight with SpotLight and AreaLight (also the ViewProviders).

Don't forget to recompute the document (App.ActiveDocument.recompute()).

Edit Camera

For changing the camera, there are also two possible methods:

  1. This solution is a bit hacky and I would recommend the other method because you can run into problems due to other objects that may refer to constants set by the original camera, for instance the FreeCAD light that uses the position of the camera. The ExportToPovRay class has the method getCam() to get the POV-Ray code for the camera. You can derive from ExportToPovRay and overwrite this method:

    from Exporter import ExportToPovRay
    
    class EditedExporter(ExportToPovRay):
       def getCam(self):
           return """Your POV-Ray Camera Code"""
  2. Create a _user.inc file where you write your camera in as described here: PowerUser.md (There described for manual creation of the file, but the file can be created also by a python script of course, this has to be done before the export.) The idea behind the _user.inc file and how to use it exactly is also described in the power user file (further up)

Start Rendering

The workbench has two main parts: The dialog for the user, programmed in Dialog.py and the exporter that does everything considering POV-Ray in Exporter.py. The Dialog creates an object RenderSettings where all made settings are stored and pass it as an argument to the exporter. Now you skip the dialog and create the RenderSettings object by yourself and call the exporter. (In CONTRIBUTING.md there is a section Contribute to the Code with a chart illustrating the structure of the workbench components.)

  1. import the necessary RenderSettings module of the workbench (the exporter is already imported from above)
from helpDefs import RenderSettings
from Exporter import ExportToPovRay # remove this if you use your own, changed Exporter as described above
  1. create the working directory of the project (if you use the _user.inc file you have to create the directory before of course)

    directory = "/home/userName/Dokumente/PovRayScripting/"  # directory where the output files should be placed
    os.mkdir(directory) # create directory
  2. create an instance of the class RenderSettings

renderSettings = RenderSettings(
    directory,
    "myProject",  # name of the project
    800,     # width of the created image in pixels
    600,     # height of the created image in pixels
    True,    # whether the FreeCAD Light should be exported or not (for your purpose probably False)
    False,   # whether you want to repair the rotation
    False,   # whether you want to export the view of the model in FreeCAD (for you probably not)
    { # the dictionary with all settings of the "Indirect Lighting" tab, here a possible example
        "radiosityName": -1, # "Radiosity_" + text of the combo box, if disabled -1
        "ambientTo0": True, # whether the "Set Ambient to 0" checkbox is checked
    }, 
    { # the dictionary with all settings of the "Environment" tab, here possible example. 
        "enabled": True,
        "option": "FreeCAD Background", # alternatively "HDRI Environment" if you want a HDRI environment
        "hdrPath": "",
        "transX": 0.0,
        "transY": 0.0,
        "transZ": 0.0,
        "rotX": 90.0,
        "rotY": 0.0,
        "rotZ": 0.0,
    }
)
  1. create the necessary files:

    with open(renderSettings.texIncPath, "a+"): pass # create texture inc file if necessary
    with open(renderSettings.errorPath, "a+"): pass # create error output file if necessary
    
    # create ini file with all needed settings
    iniContent = ""
    iniContent += "Input_File_Name='" + renderSettings.povName + "'\n"
    iniContent += "Output_File_Name='" + renderSettings.pngName + "'\n"
    iniContent += "Width=" + str(renderSettings.width) + "\n"
    iniContent += "Height=" + str(renderSettings.height) + "\n"
    iniContent += "Fatal_File='" + renderSettings.errorName + "'\n"
    # write ini content to file
    with open(renderSettings.iniPath, "w") as iniFile:
       iniFile.write(iniContent)
  2. pass the RenderSettings instance to the exporter:

# for the changed exporter
exporter = EditedExporter()
exporter.initExport(renderSettings)

# for the original exporter
exporter = ExportToPovRay()
exporter.initExport(renderSettings)

Avoid Opening of POV-Ray

The prevention of the opening of POV-Ray can become a problem if you are on Windows because POV-Ray has no option there to run in the background. On other OS, go into the settings of the workbench (via the FreeCAD Settings) and in the "Additional Rendering Parameters" remove the +P option and add -d.

If you’re on Windows, the -d option does nothing, the removal of +P avoids that the window with the rendered image keeps open. Furthermore I would recommend for Windows Users to change the setting "Mode of Starting POV-Ray" from the recommended one to the independent start from FreeCAD to avoid the blocking of the further proceed of your script.

Open Image in FreeCAD

This can be done via this command:

import ImageGui
ImageGui.open(renderSettings.pngPath)

I would put all together in a macro that you can execute (or include this in the macro that creates the models).


As you can see, these solutions are not optimal, but hopefully they work. While writing this reply here, I noticed a few issues in the exact structure of the workbench that I want to fix to make such automations easier in the future. Feel free to ask if you don’t understand everything or something doesn't work! I would be very happy if you give feedback on whether it works or not; if yes I would create a new entry in the documentation about this to make this easier accessible to all users :)

sdementen commented 3 years ago

What a detailed and carefully crafted answer, thank you a lot! I will test it as soon as I can and let you know the result

sdementen commented 3 years ago

hi @TheRaytracers ,

I have followed your instructions and it works like a charm ! The only issue is that the lights I have defined are not taken into account in the rendering. In the resulting POV file, looking for "light" I only see

// FreeCAD Light -------------------------------------
light_source { CamPosition color rgb <0.5, 0.5, 0.5> }

yet in the RenderSettings I used your "expLight=True".

Do I need to do something else to export my light objects?

sdementen commented 3 years ago

btw, to close automatically povray after the rendering, we can change the following code https://github.com/TheRaytracers/freecad-povray-render/blob/master/Exporter.py#L1496 to:

        #start povray
        if execMode == 0: #wait until finished
            subprocess.call([povExec, "/EXIT", "/RENDER", self.iniName])
            self.checkErrFile()
        else:
            subprocess.Popen([povExec, "/EXIT", "/RENDER", self.iniName])

Probably worth a setting/preference...

sdementen commented 3 years ago

When I add the lights with the UI, I see them in povray but not when I create them with python (with the code you provided)

sdementen commented 3 years ago

Continuing on this scripting journey, how can I assign a texture to a given part via python ?

TheRaytracers commented 3 years ago

The only issue is that the lights I have defined are not taken into account in the rendering. In the resulting POV file, looking for "light" I only see

// FreeCAD Light -------------------------------------
light_source { CamPosition color rgb <0.5, 0.5, 0.5> }

yet in the RenderSettings I used your "expLight=True".

Do I need to do something else to export my light objects?

Hm, on my system the lights are working. I'm using the following FreeCAD version:

OS: Linux Mint 20.1 (X-Cinnamon/cinnamon)
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.19.
Build type: Release
Branch: unknown
Hash: e8566f22bbeb0b7204e3c45519d0963e8881100b
Python version: 3.8.5
Qt version: 5.12.8
Coin version: 4.0.0
OCC version: 7.5.1
Locale: German/Germany (de_DE)

What system and FreeCAD version do you use?

I attached an example macro which inserts a light in the currently opened document in FreeCAD and starts POV-Ray. Could you try whether this example also doesn't work on your system?

The "expLight" option of the RenderSettings object has a somewhat misleading name, since it refers only to the FreeCAD light, not to the rest of the lights.

This is the macro. Don't forget to change the directory for the files before you run it :)

import os
import sys
# os.path.join(App.getUserAppDataDir(), "exporttopovray") can become something like this:
# "/home/username/.FreeCAD/exporttopovray"
sys.path.append(os.path.join(App.getUserAppDataDir(), "exporttopovray"))

from Lights import PointLight
from Lights import ViewProviderPointLight

light = App.ActiveDocument.addObject("Part::FeaturePython","PointLight")
PointLight(light)
ViewProviderPointLight(light.ViewObject)
App.ActiveDocument.recompute()

# now you can manipulate properties of the light:
light.Placement.Base.x = 15
light.Placement.Base.y = 15
light.Placement.Base.z = 15
light.Color = (1.0, 0.0, 0.0)

App.ActiveDocument.recompute()

from helpDefs import RenderSettings
from Exporter import ExportToPovRay

directory = "/home/USERNAME/Dokumente/PovRayScripting"  # directory where the output files should be placed
#os.mkdir(directory) # create directory

renderSettings = RenderSettings(
    directory,
    "myProject",  # name of the project
    800,     # width of the created image in pixels
    600,     # height of the created image in pixels
    True,    # whether the FreeCAD Light should be exported or not (for your purpose probably False)
    False,   # whether you want to repair the rotation
    False,   # whether you want to export the view of the model in FreeCAD (for you probably not)
    { # the dictionary with all settings of the "Indirect Lighting" tab, here a possible example
        "radiosityName": -1, # "Radiosity_" + text of the combo box, if disabled -1
        "ambientTo0": True, # whether the "Set Ambient to 0" checkbox is checked
    }, 
    { # the dictionary with all settings of the "Environment" tab, here possible example. 
        "enabled": True,
        "option": "FreeCAD Background", # alternatively "HDRI Environment" if you want a HDRI environment
        "hdrPath": "",
        "transX": 0.0,
        "transY": 0.0,
        "transZ": 0.0,
        "rotX": 90.0,
        "rotY": 0.0,
        "rotZ": 0.0,
    }
)

with open(renderSettings.texIncPath, "a+"): pass # create texture inc file if necessary
with open(renderSettings.errorPath, "a+"): pass # create error output file if necessary

# create ini file with all needed settings
iniContent = ""
iniContent += "Input_File_Name='" + renderSettings.povName + "'\n"
iniContent += "Output_File_Name='" + renderSettings.pngName + "'\n"
iniContent += "Width=" + str(renderSettings.width) + "\n"
iniContent += "Height=" + str(renderSettings.height) + "\n"
iniContent += "Fatal_File='" + renderSettings.errorName + "'\n"
# write ini content to file
with open(renderSettings.iniPath, "w") as iniFile:
    iniFile.write(iniContent)

# for the original exporter
exporter = ExportToPovRay()
exporter.initExport(renderSettings)
TheRaytracers commented 3 years ago

btw, to close automatically povray after the rendering, we can change the following code https://github.com/TheRaytracers/freecad-povray-render/blob/master/Exporter.py#L1496 to:

        #start povray
        if execMode == 0: #wait until finished
            subprocess.call([povExec, "/EXIT", "/RENDER", self.iniName])
            self.checkErrFile()
        else:
            subprocess.Popen([povExec, "/EXIT", "/RENDER", self.iniName])

Probably worth a setting/preference...

Thank you very much for this suggestion!
I implemented this into the code of the Preview class to make the preview (e.g of the texture tab) also for Windows users usable. Windows users can add this two options in the workbench settings if they want to avoid the opening of POV-Ray. Maybe I should write this somewhere into the documentation...

I changed the code here (https://github.com/TheRaytracers/freecad-povray-render/blob/master/Dialog.py#L1394) to

# get the OS
operatingSystem = platform.system()

# start povray
if operatingSystem == "Windows": # use other command line options for windows
    subprocess.call([povExec, "/EXIT", "/RENDER", "width=" + str(self.previewWidth),
                     "height=" + str(self.previewHeight), povName])
else:
    subprocess.call([povExec, "-d", "width=" + str(self.previewWidth),
                     "height=" + str(self.previewHeight), povName])
TheRaytracers commented 3 years ago

Continuing on this scripting journey, how can I assign a texture to a given part via python ?

Assigning textures via python is a bit trickier, because the texture tab has to interact also with POV-Ray to create the preview, the thumbnails, etc. and therefore the division between the UI and POV-Ray isn't that sharp.

The predefined textures are stored in predefined.xml. The dialog reads this file and creates Predefined objects for every texture. Then, for every FreeCAD object that can have a texture, a ListObject is created, that stores the settings (for instance the rotation, scale, etc.) on the Predefined object for the FreeCAD object. This list of ListObject objects is now used to create the content of the _textures.inc file.

In theory, you could create the texture tab, manipulate the ListObjects and write the texture inc file, but simply instantiating the TextureTab object can have negative side effects, e.g. that the thumbnails of the textures would be loaded every time, what would cost a lot of computing time.

I have to restructure the TextureTab class a bit to make it efficiently possible to script this, I will tell you as soon I'm finished; I hope I can do this in the next couple of days.


Alternatively you could write into the _user.inc file and set textures there as described in PowerUser.md but using the predefined textures wouldn't be possible via this way.

sdementen commented 3 years ago

Regarding the PointLight, I have noticed that the name of the object MUST start with the string "PointLight" otherwise the light is not considered. Could you confirm this ? and if so, why is there such constraint?

sdementen commented 3 years ago

For the textures, it was very easy to specify them in the textures.inc file => problem solved. btw, more of a povray issue, do you know why I can't include the colors_ral.inc file (but well metals.inc, colors.inc, etc) ?

TheRaytracers commented 3 years ago

Regarding the PointLight, I have noticed that the name of the object MUST start with the string "PointLight" otherwise the light is not considered. Could you confirm this ? and if so, why is there such constraint?

This constraint results from the code checking whether the object is a point light (or area light or spot light):

elif fcObj.TypeId == "Part::FeaturePython" and fcObj.Name.startswith("PointLight"):

So it checks for the TypeId of the object and the Name (not to be confused with the Label, which is the name you give an object when you rename it), which is unique in the entire document and starts with the string given to the addObject() method. Furthermore, the Name is readonly and cannot be changed by the user.

But the Label of an object doesn't have to start with "PointLight"; for this, there are no constraints.

When I coded this lines I found no better method for checking the "real" type of a Part::FeaturePython and therefore I used this way. But you're right, it's not optimal :/

TheRaytracers commented 3 years ago

For the textures, it was very easy to specify them in the textures.inc file => problem solved.

Great that this solution also works for you! :)

btw, more of a povray issue, do you know why I can't include the colors_ral.inc file (but well metals.inc, colors.inc, etc) ?

Yes, I can confirm this here on my system too. It seems that POV-Ray installs old include files and colors_ral.inc is a relatively new one. metals.inc and colors.inc are older and also included in older POV-Ray versions. Altough I have the most recent POV-Ray version installed, the include files are for POV-Ray 3.5 and not for 3.7. I would suggest that you download the most recent include folder from here and overwrite your original include folder on your system. For me, this solved the problem. (I had to download the entire POV-Ray repository to get the include directory as a whole, that's a bit annoying.)

On Linux, the original include folder is here: /usr/share/povray-3.7/include