Blender to gltf converter as a gradle task (or powershell)

Based on what i tried to do here: JmeConvert tool

These add a gradle task to convert all .blend files in your assets folder to .blend.glb files (in folder)
It works by calling a powershell file as a gradle task.
The powershell file enumerates the folder and checks if the generated file needs updating.
This calls the blender.exe with a background task and the python script on each file.

Task in gradle:

task convertBlendFiles(type: Exec) {
    workingDir 'src/assets'
    
    //sorry windows only unless you install powershell on linux
    commandLine 'cmd', '/c', 'Powershell -File blend2gltf.ps1'
}

The blend2gltf.ps1 called in gradle:

<#
    .SYNOPSIS
        Converts all .blend files in the project assets folder to .blend.glb files
    .NOTES
        Author: [email protected]
#>
$ErrorActionPreference = "Stop"

$blenderPath = 'C:\Program Files\Blender Foundation\Blender 2.81\'
$blenderPythonFile = 'blend2gltf.py'

function Convert-gltf {
    param(
        [Parameter(Mandatory="true")] [string] $FilePath,
        [Parameter(Mandatory = "true")] [string] $BlenderPython
    )
    $generatedFile = @(Get-ChildItem -ErrorAction SilentlyContinue "$FilePath.glb")
    $blendFile = Get-ChildItem $FilePath
    if (($generatedFile.Length -gt 0) -and ($blendFile.LastWriteTime -lt $generatedFile[0].LastWriteTime)) {
        Write-Host "Ignored: $blendFile"
        return $false # already converted, so ignore
    }
    #run blender command and ignore output
    & "$blenderPath\blender" $FilePath --background --python $BlenderPython > $null
    Write-Host "Converted: $FilePath"
    return $true
}

$files = Get-ChildItem -Recurse -Include '*.blend'
Write-Host "$(($files).Length) .blend files found"

$results = $files | ForEach-Object { Convert-gltf $_ $blenderPythonFile } | Where-Object { $_ }
Write-Host "$(($results).Length) updated .blend.glb files"

And the called blend2gltf.py file:

import bpy
import sys

bpy.ops.export_scene.gltf(filepath=bpy.data.filepath)

I haven’t thought about it but can you call ProcessBuilder? Seems like you should be able to, not sure but then linux users could use it without installing anything.

Not sure about this either but gradle should be able to determine if your on windows or not so you could just call the proper command.

    private static final boolean IS_WINDOWS = System.getProperty("os.name")
            .toLowerCase().startsWith("windows");

Been doing this myself so was curious.

        //Specify which type of command to send                
        if (IS_WINDOWS) {
            //Windows shell
            cmd.add("cmd.exe");
            //Run Command and then terminate
            cmd.add("/c");
            //Shows shell instead of running in background
//            cmd.add("start");
            //Path to updater script from registry
            cmd.add(updaterPref.get("startscript", null));
        } else {
            //Bourne shell
            cmd.add("sh");
            //Read from string
            cmd.add("-c");
            //TODO Setup registry on linux
            //Path to updater start script from registry
            cmd.add("./Updater/bin/Updater");
        }

Edit: cmd is just an arraylist.

Yeah thats good to know, ive only got an old vm to test it on.

I it in the sense that I am calling powershell with the *.ps1 file.

You could however build all this logic in plain java/gradle, then it would be cross platform :slight_smile:

1 Like

Oh sure that’s probably better but this is me not knowing how to use gradle well enough.

If someone cares not to do it but enough to link me something - let me know as powershell is currently good enough for me.

So i had a go at this and came up with this, which seems to work:

def blenderCommand = '"C:\\Program Files\\Blender Foundation\\Blender 2.82\\blender"'
def blenderPythonFile = "$projectDir\\assets\\blend2gltf.py"
task convertAllBlendFiles(type: Exec) {
    ConfigurableFileTree files = fileTree(dir: 'assets', include: '**/*.blend')
    files.each { File file ->
        exec {
            File glbFile = new File(file.path + '.glb')
            if (!glbFile.exists() && file.lastModified() > glbFile.lastModified()) {
                commandLine 'cmd', '/c', blenderCommand + ' "'+file.path+'" ' + '--background ' + '--python '+ blenderPythonFile
            } else {
                commandLine 'cmd', '/c', 'echo ' + file.name + ' is older\n"'
            }
        }
    }
    commandLine 'cmd', '/c', 'echo 0'
}

Haven’t tested it on linux though.

1 Like

For Linux cmd won’t work, but you could also just execute the blenderCommand directly and don’t echo text but use println (but then I guess you need to inline the exec {} statement.

But it’s a good enough start for everyone :slight_smile:

The weird echo calls are because or an error: execCommand == null.
Its also an issue that the gradle type: Exec looked like it isn’t designed to run multiple commandline calls.

Yeah, something like

files.each { File file ->
   File glbFile = file(file.path + '.glb')
   if (!glbFile.exists() && file.lastModified() > glbFile.lastModified()) {
        exec {
                commandLine blenderCommand + ' "'+file.path+'" ' + '--background ' + '--python '+ blenderPythonFile
            }
        }
    }

could work there, but I am no expert either

I just tried this commandLine call

commandLine blenderCommand, file.path, '--background ', '--python', blenderPythonFile

But i get the complaint 'C:\Program' is not recognized as an internal or external command, so its not quoting it correctly for me. There isn’t enough examples of this in gradle that i can find :frowning: .

The end result:

def blenderCommand = "C:\\Program Files\\Blender Foundation\\Blender 2.82\\blender"
def blenderPythonFile = "$projectDir\\assets\\blend2gltf.py"

task convertAllBlendFiles {
    doFirst {
        ConfigurableFileTree files = fileTree(dir: 'assets', include: '**/*.blend')
        
        files.each { File file ->
            File glbFile = new File(file.path + '.glb')
            if (!glbFile.exists() && file.lastModified() > glbFile.lastModified()) {
                println 'Running: ' + file.path
                def ip = new ByteArrayOutputStream()
                exec {
                    standardOutput = ip
                    def command = '"' + blenderCommand + '" "'+file.path+'" ' + '--background ' + '--python "' + blenderPythonFile + '"'
                    commandLine 'cmd', '/s', '/c', '"'+command+'"'
                }
                //println ip
            } else {
                println file.name + ' ignoring: generated file is newer'
            }
        }
    }
}

But at least i don’t need the random echo commands anymore, thanks.
If any one gets this to run on linux, ill figure something out with the is_windows stuff and put it together.

That is strange, for background:
Usually there are two ways to define/execute a command
“shell=true”: You pass everything as one giant string and the OS does the escaping etc. This is considered harmful, specifically if the user could pass arbitrary values.

array of parameters (much like String[] args), where you’d pass every argument and stuff as one (that’s what you did above).

What you’d need is to specifiy Exec - Gradle DSL Version 6.8.1 and Exec - Gradle DSL Version 6.8.1 instead of commandLine so that it doesn’t use any shell.

But I don’t want to push you into doing that, it’s already awesome, just in case you are interessted in learning etc :slight_smile:

1 Like