Tungsten: normal mapping capabilities (#274)

* Ability to add code prior to prepareMaterial

* Add normal input on shading node

* Add constant float node

* Add multiply and subtract nodes

* Split math nodes out into MathNodes.kt
This commit is contained in:
Ben Doherty
2018-09-20 11:09:15 -07:00
committed by GitHub
parent d3e7103ee5
commit 596c94f6f0
10 changed files with 367 additions and 46 deletions

View File

@@ -56,9 +56,11 @@ open class Expression(
}
}
fun floatTypeName(dimensions: Int) = if (dimensions > 1) "float$dimensions" else "float"
private fun createFloatLiteral(dimensions: Int): String {
val zeroes = "0.0, ".repeat(maxOf(dimensions - 1, 0)) + "0.0"
val typeName = if (dimensions > 1) "float$dimensions" else "float"
val typeName = floatTypeName(dimensions)
return "$typeName($zeroes)"
}

View File

@@ -36,7 +36,19 @@ import org.jetbrains.annotations.Nullable;
*/
public final class GraphCompiler {
public enum CodeSection {
AFTER_PREPARE_MATERIAL,
BEFORE_PREPARE_MATERIAL
}
// Material function source text before the "prepareMaterial" call
private StringBuilder mMaterialFunctionPrologue = new StringBuilder();
// Material function source text after the "prepareMaterial" call
private StringBuilder mMaterialFunctionBodyBuilder = new StringBuilder();
private CodeSection mCurrentCodeSection = CodeSection.AFTER_PREPARE_MATERIAL;
private final List<String> mRequiredAttributes = new ArrayList<>();
private String mShadingModel = "unlit";
private final List<Parameter> mParameters = new ArrayList<>();
@@ -73,7 +85,7 @@ public final class GraphCompiler {
}
String fragmentSection = GraphFormatter.formatFragmentSection(mGlobalFunctions.values(),
mMaterialFunctionBodyBuilder.toString());
mMaterialFunctionPrologue.toString(), mMaterialFunctionBodyBuilder.toString());
String materialDefinition =
GraphFormatter.formatMaterialSection(mRequiredAttributes, mParameters,
mShadingModel) + fragmentSection;
@@ -181,7 +193,20 @@ public final class GraphCompiler {
if (!mShouldAppendCode) {
return;
}
mMaterialFunctionBodyBuilder.append(code);
if (mCurrentCodeSection == CodeSection.AFTER_PREPARE_MATERIAL) {
mMaterialFunctionBodyBuilder.append(code);
} else if (mCurrentCodeSection == CodeSection.BEFORE_PREPARE_MATERIAL) {
mMaterialFunctionPrologue.append(code);
}
}
/**
* Set the state of this GraphCompiler to output code to a specific section of the material
* function body: either before the call to prepareMaterial(), or after.
* The state persists until setCurrentCodeSection is called again.
*/
public void setCurrentCodeSection(CodeSection section) {
mCurrentCodeSection = section;
}
/**

View File

@@ -30,7 +30,7 @@ final class GraphFormatter {
private GraphFormatter() { }
static String formatFragmentSection(Collection<String> globalFunctions,
String materialFunctionBody) {
String materialFunctionBodyPrologue, String materialFunctionBody) {
StringBuilder functions = new StringBuilder();
for (String functionBody : globalFunctions) {
functions.append(indent(functionBody, FUNCTION_INDENT_AMOUNT));
@@ -39,7 +39,7 @@ final class GraphFormatter {
return "fragment {\n"
+ functions.toString()
+ " void material(inout MaterialInputs material) {\n"
+ formatMaterialFunctionBody(materialFunctionBody)
+ formatMaterialFunctionBody(materialFunctionBodyPrologue ,materialFunctionBody)
+ " }\n"
+ "}";
}
@@ -75,9 +75,10 @@ final class GraphFormatter {
return t.replaceAll("\n(?!$)", "\n" + spaces); // indent remaining non-empty lines
}
private static String formatMaterialFunctionBody(String code) {
return indent("prepareMaterial(material);\n", BODY_CODE_INDENT_AMOUNT)
+ indent(code, BODY_CODE_INDENT_AMOUNT);
private static String formatMaterialFunctionBody(String prologue, String epilogue) {
return indent(prologue, BODY_CODE_INDENT_AMOUNT)
+ indent("prepareMaterial(material);\n", BODY_CODE_INDENT_AMOUNT)
+ indent(epilogue, BODY_CODE_INDENT_AMOUNT);
}
private static String formatParameters(Collection<Parameter> parameters) {

View File

@@ -18,11 +18,15 @@ package com.google.android.filament.tungsten.compiler
import com.google.android.filament.tungsten.model.Node
import com.google.android.filament.tungsten.model.NodeId
import com.google.android.filament.tungsten.model.createAdderNode
import com.google.android.filament.tungsten.model.createAddNode
import com.google.android.filament.tungsten.model.createDivideNode
import com.google.android.filament.tungsten.model.createFloat2ConstantNode
import com.google.android.filament.tungsten.model.createFloat3ConstantNode
import com.google.android.filament.tungsten.model.createFloat3ParameterNode
import com.google.android.filament.tungsten.model.createFloatConstantNode
import com.google.android.filament.tungsten.model.createMultiplyNode
import com.google.android.filament.tungsten.model.createShaderNode
import com.google.android.filament.tungsten.model.createSubtractNode
import com.google.android.filament.tungsten.model.createTextureSampleNode
import com.google.android.filament.tungsten.model.serialization.INodeFactory
@@ -44,7 +48,11 @@ class NodeRegistry : INodeFactory {
init {
mNodes = listOf(
NodeEntry("Add", "adder", createAdderNode),
NodeEntry("Add", "add", createAddNode),
NodeEntry("Subtract", "subtract", createSubtractNode),
NodeEntry("Multiply", "multiply", createMultiplyNode),
NodeEntry("Divide", "divide", createDivideNode),
NodeEntry("Constant float", "floatConstant", createFloatConstantNode),
NodeEntry("Constant float2", "float2Constant", createFloat2ConstantNode),
NodeEntry("Constant float3", "float3Constant", createFloat3ConstantNode),
NodeEntry("Float3 parameter", "float3Parameter", createFloat3ParameterNode),

View File

@@ -28,6 +28,7 @@ data class Connection(
abstract class Slot
typealias NodeId = Int
typealias CompileFunction = (Node, GraphCompiler) -> Node
/**
* An immutable Node in the graph.
@@ -42,7 +43,7 @@ data class Node(
// A node's compile function generates code by calling methods on a GraphCompiler. The function
// can return a new node if compilation has changed properties of the node itself.
val compileFunction: (Node, GraphCompiler) -> Node = { n, _ -> n },
val compileFunction: CompileFunction = { n, _ -> n },
// Input and output slots of a node are represented by a String name.
val outputSlots: List<String> = emptyList(),

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2018 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.tungsten.model
import com.google.android.filament.tungsten.compiler.Expression
import com.google.android.filament.tungsten.compiler.Literal
import com.google.android.filament.tungsten.compiler.floatTypeName
import com.google.android.filament.tungsten.compiler.trim
data class ResolvedExpressions(
val a: Expression,
val b: Expression,
val resultDimensionality: Int
)
// A function that takes two expressions and prepares them for a math operation.
typealias ExpressionResolver = (Expression?, Expression?) -> ResolvedExpressions
typealias Operation = (Expression, Expression) -> String
/**
* Create a math node compile function with a given operation.
* Assumes two input slots, "a" and "b", and a single output, "out".
*/
private fun createMathCompileFunction(
expressionResolver: ExpressionResolver,
operation: Operation,
temporaryVariable: String
): CompileFunction = { node, compiler ->
val a = compiler.compileAndRetrieveExpression(node.getInputSlot("a"))
val b = compiler.compileAndRetrieveExpression(node.getInputSlot("b"))
val (aExpr, bExpr, resultDimensionality) = expressionResolver(a, b)
compiler.setExpressionForSlot(node.getInputSlot("a"), aExpr)
compiler.setExpressionForSlot(node.getInputSlot("b"), bExpr)
val temp = compiler.getNewTemporaryVariableName(temporaryVariable)
compiler.addCodeToMaterialFunctionBody(
"${floatTypeName(resultDimensionality)} $temp = ${operation(aExpr, bExpr)};\n")
val output = node.getOutputSlot("out")
compiler.setExpressionForSlot(output, Expression(temp, resultDimensionality))
node
}
/**
* Trims two expressions to be the same dimensionality, preferring the one with fewer dimensions.
* If one of the inputs is a single float, neither input is modified, allowing for a scalar
* operations.
*/
internal fun resolveInputExpressions(a: Expression?, b: Expression?): ResolvedExpressions {
val aExpr = a ?: Literal(4)
val bExpr = b ?: Literal(4)
val (trimmedA, trimmedB) = if (aExpr.dimensions > 1 && bExpr.dimensions > 1) {
trim(aExpr, bExpr)
} else {
Pair(aExpr, bExpr)
}
return ResolvedExpressions(trimmedA, trimmedB, maxOf(trimmedA.dimensions, trimmedB.dimensions))
}
private val subtractNodeCompile = createMathCompileFunction(
::resolveInputExpressions,
{ a, b -> "$a - $b" },
"subtract"
)
private val addNodeCompile = createMathCompileFunction(
::resolveInputExpressions,
{ a, b -> "$a + $b" },
"add"
)
private val multiplyNodeCompile = createMathCompileFunction(
::resolveInputExpressions,
{ a, b -> "$a * $b" },
"multiply"
)
private val divideNodeCompile = createMathCompileFunction(
::resolveInputExpressions,
{ a, b -> "$a / $b" },
"divide"
)
val createAddNode = fun(id: NodeId) =
Node(
id = id,
type = "add",
compileFunction = addNodeCompile,
inputSlots = listOf("a", "b"),
outputSlots = listOf("out")
)
val createSubtractNode = fun(id: NodeId) =
Node(
id = id,
type = "subtract",
compileFunction = subtractNodeCompile,
inputSlots = listOf("a", "b"),
outputSlots = listOf("out")
)
val createMultiplyNode = fun(id: NodeId) =
Node(
id = id,
type = "multiply",
compileFunction = multiplyNodeCompile,
inputSlots = listOf("a", "b"),
outputSlots = listOf("out")
)
val createDivideNode = fun(id: NodeId) =
Node(
id = id,
type = "divide",
compileFunction = divideNodeCompile,
inputSlots = listOf("a", "b"),
outputSlots = listOf("out")
)

View File

@@ -19,36 +19,15 @@ package com.google.android.filament.tungsten.model
import com.google.android.filament.tungsten.compiler.GraphCompiler
import com.google.android.filament.tungsten.compiler.Expression
import com.google.android.filament.tungsten.compiler.Literal
import com.google.android.filament.tungsten.compiler.trim
import com.google.android.filament.tungsten.properties.ColorChooser
import com.google.android.filament.tungsten.properties.FloatSlider
import com.google.android.filament.tungsten.properties.MultipleChoice
import com.google.android.filament.tungsten.properties.TextureFileChooser
private val adderNodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
val outputSlot = node.getOutputSlot("result")
val a = compiler.compileAndRetrieveExpression(node.getInputSlot("a"))
val b = compiler.compileAndRetrieveExpression(node.getInputSlot("b"))
// Trim the expressions so that they're the same dimension.
val (aExpression, bExpression) = trim(a ?: Literal(4), b ?: Literal(4))
compiler.setExpressionForSlot(node.getInputSlot("a"), aExpression)
compiler.setExpressionForSlot(node.getInputSlot("b"), bExpression)
val temp = compiler.getNewTemporaryVariableName("adder")
val resultDimensions = aExpression.dimensions
compiler.addCodeToMaterialFunctionBody(
"float$resultDimensions $temp = $aExpression + $bExpression;\n")
compiler.setExpressionForSlot(outputSlot, Expression(temp, resultDimensions))
return node
}
private val UNLIT_INPUTS = listOf("baseColor", "emissive")
private val LIT_INPUTS = listOf("baseColor", "metallic", "roughness", "reflectance", "clearCoat",
"clearCoatRoughness", "anisotropy", "anisotropyDirection", "ambientOcclusion", "emissive")
private val LIT_INPUTS = listOf("normal", "baseColor", "metallic", "roughness", "reflectance",
"clearCoat", "clearCoatRoughness", "anisotropy", "anisotropyDirection", "ambientOcclusion",
"emissive")
private val inputSlotsForShadingModel = { shadingModel: String ->
when (shadingModel) {
@@ -63,6 +42,11 @@ private val shaderNodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
compiler.setShadingModel(shadingModel)
val compileMaterialInput = { name: String, dimensions: Int ->
// The "normal" input is special, and its code must go before the call to prepareMaterial().
if (name == "normal") {
compiler.setCurrentCodeSection(GraphCompiler.CodeSection.BEFORE_PREPARE_MATERIAL)
}
val inputSlot = node.getInputSlot(name)
val connectedExpression = compiler.compileAndRetrieveExpression(inputSlot)
val isConnected = connectedExpression != null
@@ -75,6 +59,8 @@ private val shaderNodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
compiler.addCodeToMaterialFunctionBody("material.$name = $conformedExpression;\n")
}
compiler.setExpressionForSlot(inputSlot, conformedExpression)
compiler.setCurrentCodeSection(GraphCompiler.CodeSection.AFTER_PREPARE_MATERIAL)
}
// Compile inputs common to all shading models.
@@ -95,6 +81,11 @@ private val shaderNodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
compileMaterialInput("ambientOcclusion", 1)
}
if (shadingModel == "lit") {
// "normal" must be compiled first. If it has any dependencies that are connected to other
// inputs, we need to ensure their code comes before prepareMaterial()
compileMaterialInput("normal", 3)
}
compileCommonInputs()
when (shadingModel) {
"lit" -> compileLitInputs()
@@ -127,6 +118,18 @@ private val float3ParameterNodeCompile = fun(node: Node, compiler: GraphCompiler
return node
}
private val constantFloatNodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
val value = (node.properties[0].value as FloatValue)
val outputVariable = compiler.getNewTemporaryVariableName("floatConstant")
compiler.addCodeToMaterialFunctionBody("float $outputVariable = ${value.v};\n")
val outputSlot = node.getOutputSlot("out")
compiler.setExpressionForSlot(outputSlot, Expression(outputVariable, 1))
return node
}
private val constantFloat2NodeCompile = fun(node: Node, compiler: GraphCompiler): Node {
val outputSlot = node.getOutputSlot("result")
@@ -175,16 +178,6 @@ val createTextureSampleNode = fun(id: NodeId) =
))
)
val createAdderNode = fun(id: NodeId): Node {
return Node(
id = id,
type = "adder",
compileFunction = adderNodeCompile,
inputSlots = listOf("a", "b"),
outputSlots = listOf("result")
)
}
val createFloat3ConstantNode = fun(id: NodeId): Node {
return Node(
id = id,
@@ -212,6 +205,19 @@ val createFloat3ParameterNode = fun(id: NodeId): Node {
)
}
val createFloatConstantNode = fun(id: NodeId) =
Node(
id = id,
type = "floatConstant",
compileFunction = constantFloatNodeCompile,
outputSlots = listOf("out"),
properties = listOf(Property(
name = "value",
value = FloatValue(),
editorFactory = ::FloatSlider
))
)
val createFloat2ConstantNode = fun(id: NodeId): Node {
return Node(
id = id,

View File

@@ -39,6 +39,20 @@ sealed class PropertyValue {
abstract fun deserialize(value: Any): PropertyValue
}
data class FloatValue(val v: Float = 0.0f) : PropertyValue() {
override fun applyToMaterialInstance(materialInstance: MaterialInstance, name: String) {
materialInstance.setParameter(name, v)
}
override fun serialize() = v
override fun deserialize(value: Any): PropertyValue {
if (value !is Number) return this
return FloatValue(value.toFloat())
}
}
data class Float3(val x: Float = 0.0f, val y: Float = 0.0f, val z: Float = 0.0f) : PropertyValue() {
override fun applyToMaterialInstance(materialInstance: MaterialInstance, name: String) {

View File

@@ -17,17 +17,21 @@
package com.google.android.filament.tungsten.properties
import com.google.android.filament.tungsten.model.Float3
import com.google.android.filament.tungsten.model.FloatValue
import com.google.android.filament.tungsten.model.PropertyValue
import com.google.android.filament.tungsten.model.StringValue
import com.google.android.filament.tungsten.model.TextureFile
import com.google.android.filament.tungsten.texture.TextureUtils
import java.awt.Color
import java.text.NumberFormat
import javax.swing.JButton
import javax.swing.JColorChooser
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.JFileChooser
import javax.swing.JFormattedTextField
import javax.swing.JPanel
import javax.swing.JSlider
import kotlin.math.roundToInt
abstract class PropertyEditor {
@@ -38,6 +42,71 @@ abstract class PropertyEditor {
abstract fun setValue(v: PropertyValue)
}
/**
* Set the value of a JSlider without causing any changeListeners to trigger.
*/
fun JSlider.quietlySetValue(v: Int) {
val changeListeners = this.changeListeners
for (changeListener in changeListeners) {
this.removeChangeListener(changeListener)
}
this.value = v
for (changeListener in changeListeners) {
this.addChangeListener(changeListener)
}
}
internal class FloatSlider(initialValue: FloatValue) : PropertyEditor() {
private val scaleFactor = 1000
override val component: JPanel = JPanel()
private val slider: JSlider
private val field: JFormattedTextField
private var currentValue: Float = initialValue.v
override fun setValue(v: PropertyValue) {
val value = v as FloatValue
currentValue = value.v
field.value = currentValue
slider.quietlySetValue((currentValue * scaleFactor).roundToInt())
}
init {
slider = JSlider(0, scaleFactor, (initialValue.v * scaleFactor).roundToInt())
slider.addChangeListener {
if (!slider.valueIsAdjusting) {
currentValue = slider.value.toFloat() / scaleFactor
updateValue()
}
}
val format = NumberFormat.getNumberInstance()
format.maximumFractionDigits = 5
field = JFormattedTextField(format)
field.columns = 10
field.value = initialValue.v
field.addActionListener {
val newValue = field.value as? Number
newValue?.let { v ->
currentValue = v.toFloat()
updateValue()
}
}
component.add(slider)
component.add(field)
}
private fun updateValue() {
slider.quietlySetValue((currentValue * scaleFactor).roundToInt())
field.value = currentValue
valueChanged(FloatValue(currentValue))
}
}
internal class ColorChooser(value: Float3) : PropertyEditor() {
override val component: JColorChooser

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 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.tungsten.model
import com.google.android.filament.tungsten.compiler.Literal
import org.junit.Assert.assertEquals
import org.junit.Test
class NodeTest {
@Test
fun `resolveInputExpressions keeps two float4s the same length`() {
val (a, b) = resolveInputExpressions(Literal(4), Literal(4))
assertEquals(4, a.dimensions)
assertEquals(4, b.dimensions)
}
@Test
fun `resolveInputExpressions keeps two floats the same length`() {
val (a, b) = resolveInputExpressions(Literal(1), Literal(1))
assertEquals(1, a.dimensions)
assertEquals(1, b.dimensions)
}
@Test
fun `resolveInputExpressions trims both a float3 and a float2 to a float2`() {
val (a, b) = resolveInputExpressions(Literal(3), Literal(2))
assertEquals(2, a.dimensions)
assertEquals(2, b.dimensions)
}
@Test
fun `resolveInputExpressions allows a float as input for scalar operations`() {
val (a, b) = resolveInputExpressions(Literal(3), Literal(1))
assertEquals(3, a.dimensions)
assertEquals(1, b.dimensions)
}
@Test
fun `resolveInputExpressions defaults to float4 when a single input is not present`() {
val (a, b) = resolveInputExpressions(null, Literal(1))
assertEquals(4, a.dimensions)
assertEquals(1, b.dimensions)
}
}