Add new Filament Gradle plugin (#9694)

This commit is contained in:
Ben Doherty
2026-02-10 14:15:45 -05:00
committed by GitHub
parent 4622e88a6b
commit 27aa517c48
23 changed files with 773 additions and 390 deletions

120
android/buildSrc/README.md Normal file
View File

@@ -0,0 +1,120 @@
# Filament Tools Gradle Plugin
## About
The **Filament Tools Gradle Plugin** helps integrate Filament into your Android project. It
automates the use of Filament's command-line tools (`matc`, `cmgen`, and `filamesh`).
This plugin handles:
- **Material Compilation**: Compiles `.mat` material definitions into `.filamat` binaries.
- **IBL Generation**: Generates Image-Based Lighting assets from HDR environment maps.
- **Mesh Compilation**: Converts models into Filament's efficient `.filamesh` binary format. *Note:
This tool is no longer recommended; instead, use glTF and Filament's `gltfio` library for model
loading.*
The plugin hooks directly into the Android build lifecycle (via `preBuild`) and supports incremental
builds, so assets are only recompiled when source files change.
## Usage
Apply the plugin in your module's `build.gradle` file and configure the `filament` block. You can
specify inputs and outputs for any combination of materials, IBLs, or meshes. If a path is not
configured, the corresponding task will be disabled.
### Example Configuration
```groovy
plugins {
id 'filament-plugin'
}
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
iblInputFile = project.layout.projectDirectory.file("path/to/environment.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
meshInputFile = project.layout.projectDirectory.file("path/to/model.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")
}
```
### Configuration Details
- **materialInputDir**: The directory containing your source material definitions (`.mat` files).
- **materialOutputDir**: The directory where the compiled material files (`.filamat`) will be
generated.
- **iblInputFile**: The source high-dynamic-range image file (e.g., `.hdr` or `.exr`) used to
generate Image Based Lighting assets.
- **iblOutputDir**: The directory where the generated IBL assets (typically `.ktx` files) will be
placed.
- **meshInputFile**: The source mesh file (e.g., `.obj`) to be compiled.
- **meshOutputDir**: The directory where the compiled mesh file (`.filamesh`) will be generated.
Automatically adds tasks to your Android build to compile materials, generate an IBL, and compile a
mesh. The plugin hooks into `preBuild` to ensure assets are generated before the application is
built.
### Configuration Flags
You can control specific compilation options using Gradle properties (e.g., in `gradle.properties`
or via command line `-P`).
- **`com.google.android.filament.exclude-vulkan`** When set to `true`, the Vulkan backend is
excluded from the compiled materials. This can be useful to reduce the size of the generated
assets if your application does not target Vulkan. *Default: `false` (Vulkan is included)*
- **`com.google.android.filament.include-webgpu`** When set to `true`, the WebGPU backend is
included in the compiled materials. Use this if you intend to use the materials in a context
supporting WebGPU. *Default: `false`*
## Tools Configuration
The Filament Tools plugin requires some binary tools to be available: `matc`, `cmgen`, and
`filamesh`.
There are three ways to configure Filament tools:
1. **Point to a local path directly**
```groovy
filament {
matc {
path = "/path/to/matc"
}
cmgen {
path = "/path/to/cmgen"
}
filamesh {
path = "/path/to/filamesh"
}
}
```
2. **Point to a Maven artifact**
```groovy
filament {
matc {
// The minor version (the middle number) must match the Filament dependency's.
artifact = 'com.google.android.filament:matc:1.68.5'
}
cmgen {
artifact = 'com.google.android.filament:cmgen:1.68.5'
}
}
```
*Note that the `filamesh` artifact is not hosted on Maven Central, so it must be provided locally or
via another mechanism if needed.*
Gradle will automatically handle downloading the tool appropriate for your machine (MacOS/Linux/Windows) from Maven.
3. **Set the `com.google.android.filament.tools-dir` Gradle property**
This will override any other configuration. Gradle will attempt to locate the tools under
`<tools-dir>/bin` (e.g., `.../bin/matc`, `.../bin/cmgen`, `.../bin/filamesh`).

View File

@@ -4,13 +4,18 @@ plugins {
gradlePlugin {
plugins {
create("filament-tools-plugin") {
id = "filament-tools-plugin"
implementationClass = "FilamentToolsPlugin"
create("filament-plugin") {
id = "filament-plugin"
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}

View File

@@ -1,359 +0,0 @@
// This plugin accepts the following parameters:
//
// com.google.android.filament.tools-dir
// Path to the Filament distribution/install directory for desktop.
// This directory must contain bin/matc.
//
// com.google.android.filament.exclude-vulkan
// When set, support for Vulkan will be excluded.
//
// Example:
// ./gradlew -Pcom.google.android.filament.tools-dir=../../dist-release assembleDebug
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.incremental.InputFileDetails
import org.gradle.api.model.ObjectFactory
import org.gradle.internal.os.OperatingSystem
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.nio.file.Paths
import javax.inject.Inject
abstract class TaskWithBinary extends DefaultTask {
private final String binaryName
private Property<String> binaryPath = null
TaskWithBinary(String name) {
binaryName = name
}
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
@Input
Property<String> getBinary() {
if (binaryPath == null) {
def tool = ["/bin/${binaryName}.exe", "/bin/${binaryName}"]
def fullPath = tool.collect { path ->
def filamentToolsPath = providers
.gradleProperty("com.google.android.filament.tools-dir")
.forUseAtConfigurationTime().get()
def directory = objects.fileProperty()
.fileValue(new File(filamentToolsPath)).getAsFile().get()
Paths.get(directory.absolutePath, path).toFile()
}
binaryPath = objects.property(String.class)
binaryPath.set(
(OperatingSystem.current().isWindows() ? fullPath[0] : fullPath[1]).toString())
}
return binaryPath
}
}
class LogOutputStream extends ByteArrayOutputStream {
private final Logger logger
private final LogLevel level
LogOutputStream(Logger logger, LogLevel level) {
this.logger = logger
this.level = level
}
@Override
void flush() {
logger.log(level, toString())
reset()
}
}
// Custom task to compile material files using matc
// This task handles incremental builds
abstract class MaterialCompiler extends TaskWithBinary {
@Incremental
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
MaterialCompiler() {
super("matc")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamat' })
})
}
inputs.getFileChanges(inputDir).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling material " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def matcArgs = []
def exclude_vulkan = providers
.gradleProperty("com.google.android.filament.exclude-vulkan")
.forUseAtConfigurationTime().present
if (!exclude_vulkan) {
matcArgs += ['-a', 'vulkan']
}
def include_webgpu = providers
.gradleProperty("com.google.android.filament.include-webgpu")
.forUseAtConfigurationTime().present
if (include_webgpu) {
matcArgs += ['-a', 'webgpu', '--variant-filter=stereo']
}
def mat_no_opt = providers
.gradleProperty("com.google.android.filament.matnopt")
.forUseAtConfigurationTime().present
if (mat_no_opt) {
matcArgs += ['-g']
}
matcArgs += ['-a', 'opengl', '-p', 'mobile', '-o', getOutputFile(file), file]
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args matcArgs
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamat').get().asFile
}
}
// Custom task to process IBLs using cmgen
// This task handles incremental builds
abstract class IblGenerator extends TaskWithBinary {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
IblGenerator() {
super("cmgen")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Generating IBL " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def outputPath = outputDir.get().asFile
def commandArgs = cmgenArgs.getOrNull()
if (commandArgs == null) {
commandArgs =
'-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
commandArgs = commandArgs + " " + file
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(commandArgs.split())
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.') - 1]).get().asFile
}
}
// Custom task to compile mesh files using filamesh
// This task handles incremental builds
abstract class MeshCompiler extends TaskWithBinary {
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
MeshCompiler() {
super("filamesh")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamesh' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling mesh " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(file, getOutputFile(file))
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamesh').get().asFile
}
}
class FilamentToolsPluginExtension {
DirectoryProperty materialInputDir
DirectoryProperty materialOutputDir
String cmgenArgs
RegularFileProperty iblInputFile
DirectoryProperty iblOutputDir
RegularFileProperty meshInputFile
DirectoryProperty meshOutputDir
}
class FilamentToolsPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('filamentTools', FilamentToolsPluginExtension)
extension.materialInputDir = project.objects.directoryProperty()
extension.materialOutputDir = project.objects.directoryProperty()
extension.iblInputFile = project.objects.fileProperty()
extension.iblOutputDir = project.objects.directoryProperty()
extension.meshInputFile = project.objects.fileProperty()
extension.meshOutputDir = project.objects.directoryProperty()
project.tasks.register("filamentCompileMaterials", MaterialCompiler) {
enabled =
extension.materialInputDir.isPresent() &&
extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.tasks.register("filamentGenerateIbl", IblGenerator) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile = extension.iblInputFile.getOrNull()
outputDir = extension.iblOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentGenerateIbl"
project.tasks.register("filamentCompileMesh", MeshCompiler) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentCompileMesh"
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
class FilamentExtension {
final ToolsLocator tools
final DirectoryProperty materialInputDir
final DirectoryProperty materialOutputDir
final Property<String> cmgenArgs
final RegularFileProperty iblInputFile
final DirectoryProperty iblOutputDir
final RegularFileProperty meshInputFile
final DirectoryProperty meshOutputDir
FilamentExtension(Project project) {
this.tools = new ToolsLocator(project)
this.materialInputDir = project.objects.directoryProperty()
this.materialOutputDir = project.objects.directoryProperty()
this.cmgenArgs = project.objects.property(String)
this.iblInputFile = project.objects.fileProperty()
this.iblOutputDir = project.objects.directoryProperty()
this.meshInputFile = project.objects.fileProperty()
this.meshOutputDir = project.objects.directoryProperty()
}
void matc(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.matc)
}
void cmgen(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.cmgen)
}
void filamesh(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.filamesh)
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
class FilamentPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.pluginManager.apply("com.google.osdetector")
FilamentExtension extension = project.extensions.create("filament", FilamentExtension, project)
project.afterEvaluate {
extension.tools.resolve(project)
project.tasks.register("filamentCompileMaterials", MaterialCompileTask) {
enabled = extension.materialInputDir.isPresent() && extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
matcTool.from(extension.tools.matcToolFiles)
}
project.tasks.register("filamentGenerateIbl", IblGenerateTask) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile.set(extension.iblInputFile.getOrNull())
outputDir.set(extension.iblOutputDir.getOrNull())
cmgenTool.from(extension.tools.cmgenToolFiles)
}
project.tasks.register("filamentCompileMesh", MeshCompileTask) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
filameshTool.from(extension.tools.filameshToolFiles)
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.preBuild.dependsOn "filamentGenerateIbl"
project.preBuild.dependsOn "filamentCompileMesh"
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.api.tasks.incremental.InputFileDetails
import javax.inject.Inject
abstract class IblGenerateTask extends DefaultTask {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
abstract ConfigurableFileCollection getCmgenTool()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void execute(InputChanges inputs) {
if (cmgenTool.empty) {
throw new IllegalStateException(
"cmgen executable not configured. Please configure the 'cmgen' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File cmgen = getCmgenTool().singleFile
if (!cmgen.exists()) {
throw new IllegalStateException("cmgen executable does not exist: ${cmgen.absolutePath}")
}
if (!cmgen.canExecute()) {
cmgen.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching { include '*' })
}
}
inputs.getFileChanges(getInputFile()).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
computeOutputFile(file).delete()
} else {
println "Generating IBL: ${file.name}"
def outputPath = getOutputDir().get().asFile
def commandArgs = getCmgenArgs().getOrNull()
if (commandArgs == null) {
// Default args if not provided
commandArgs = '-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
def argsList = commandArgs.split(' ').toList()
argsList.add(file.absolutePath)
getExecOperations().exec { spec ->
spec.executable(cmgen)
spec.args(argsList)
}
}
}
}
File computeOutputFile(final File file) {
String name = file.name
int dotIndex = name.lastIndexOf('.')
String baseName = dotIndex > 0 ? name.substring(0, dotIndex) : name
return getOutputDir().file(baseName).get().asFile
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MaterialCompileTask extends DefaultTask {
@Incremental
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getMatcTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@Inject
abstract ProviderFactory getProviderFactory()
@TaskAction
void compile(InputChanges inputs) {
if (matcTool.empty) {
throw new IllegalStateException(
"matc executable not configured. Please configure the 'matc' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File matc = matcTool.singleFile
if (!matc.exists()) {
throw new IllegalStateException("matc executable does not exist: ${matc.absolutePath}")
}
if (!matc.canExecute()) {
matc.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamat'
})
}
}
def pf = getProviderFactory()
def excludeVulkanProperty = pf.gradleProperty("com.google.android.filament.exclude-vulkan")
def includeWebGpuProperty = pf.gradleProperty("com.google.android.filament.include-webgpu")
def matNoOptProperty = pf.gradleProperty("com.google.android.filament.matnopt")
def excludeVulkan = excludeVulkanProperty.orNull == "true"
def includeWebGpu = includeWebGpuProperty.orNull == "true"
def matNoOpt = matNoOptProperty.orNull == "true"
inputs.getFileChanges(getInputDir()).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling material: ${file.name}"
def args = []
if (!excludeVulkan) {
args += ['-a', 'vulkan']
}
if (includeWebGpu) {
args += ['-a', 'webgpu', '--variant-filter=stereo']
}
if (matNoOpt) {
args += ['-g']
}
args += [
'-a', 'opengl', '-p', 'mobile',
'-o', outputFile.absolutePath,
file.absolutePath
]
getExecOperations().exec { spec ->
spec.executable(matc)
spec.args(args)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamat").get().asFile
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MeshCompileTask extends DefaultTask {
@Incremental
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getFilameshTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void compile(InputChanges inputs) {
if (filameshTool.empty) {
throw new IllegalStateException(
"filamesh executable not configured. Please configure the 'filamesh' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File filamesh = filameshTool.singleFile
if (!filamesh.exists()) {
throw new IllegalStateException("filamesh executable does not exist: ${filamesh.absolutePath}")
}
if (!filamesh.canExecute()) {
filamesh.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamesh'
})
}
}
inputs.getFileChanges(inputFile).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling mesh: ${file.name}"
getExecOperations().exec { spec ->
spec.executable(filamesh)
spec.args(file.absolutePath, outputFile.absolutePath)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamesh").get().asFile
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Paths
class ToolsLocator {
static class ToolConfig {
String artifact
String path
FileCollection files
}
final ToolConfig matc = new ToolConfig()
final ToolConfig cmgen = new ToolConfig()
final ToolConfig filamesh = new ToolConfig()
private final Project project
ToolsLocator(Project project) {
this.project = project
}
void resolve(Project project) {
resolveTool(matc, "matc")
resolveTool(cmgen, "cmgen")
resolveTool(filamesh, "filamesh")
}
/**
* Resolves a specific tool by its name and sets the {@link ToolConfig#files} property of the
* provided {@link ToolConfig} object. It first attempts to locate the tool based on a Gradle
* property `com.google.android.filament.tools-dir` if present, otherwise it resolves the tool
* through a Gradle configuration.
*
* @param tool The {@link ToolConfig} object whose {@code files} property will be set.
* @param name The name of the tool (e.g., "matc", "cmgen").
*/
private void resolveTool(ToolConfig tool, String name) {
// Find the OS classifier, e.g. 'osx-aarch_64'.
def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set, we'll use it as the tool's base path.
def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent()) {
def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" :
"${toolsDir}/bin/${name}"
tool.files = project.files(path)
return
}
// If an explicit path for the tool is provided in ToolConfig
// (e.g. matc { path = 'path/to/tool' }), use it directly.
if (tool.path) {
tool.files = project.files(tool.path)
return
}
// Otherwise, if an artifact is provided
// (e.g. matc { artifact = 'com.google.android.filament:matc:1.68.5' }), resolve it.
if (tool.artifact) {
String depString = tool.artifact
// In Gradle, a configuration is a named, manageable group of dependencies.
// Resolve the tool artifact using a detached configuration. A detached configuration
// is a temporary, isolated configuration that is not part of the project's regular
// configuration hierarchy.
Configuration config = project.configurations.detachedConfiguration()
config.setTransitive(false) // We only want the tool itself, not its dependencies
def dep = project.dependencies.create("${depString}:${classifier}@exe")
config.dependencies.add(dep)
// A Gradle Configuration implements FileCollection. When treated as a FileCollection,
// it represents the resolved files of its dependencies.
tool.files = config
}
}
FileCollection getMatcToolFiles() {
return matc.files ?: project.files()
}
FileCollection getCmgenToolFiles() {
return cmgen.files ?: project.files()
}
FileCollection getFilameshToolFiles() {
return filamesh.files ?: project.files()
}
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
cmgenArgs = "-q --format=ktx --size=256 --extract-blur=0.1 --deploy=src/main/assets/envs/default_env"
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/lightroom_14b.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
meshInputFile = project.layout.projectDirectory.file("../../../third_party/models/shader_ball/shader_ball.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/studio_small_02_2k.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}