Skip to content

Instantly share code, notes, and snippets.

@PrimaryFeather
Last active October 4, 2020 10:03
Show Gist options
  • Save PrimaryFeather/94d8650c294406050d77cc901dfb2a64 to your computer and use it in GitHub Desktop.
Save PrimaryFeather/94d8650c294406050d77cc901dfb2a64 to your computer and use it in GitHub Desktop.
A "MeshStyle" for Starling that allows batching of multiple textures in one draw call.
// =================================================================================================
//
// Starling Framework
// Copyright Gamua GmbH. All Rights Reserved.
//
// This program is free software. You can redistribute and/or modify it
// in accordance with the terms of the accompanying license agreement.
//
// =================================================================================================
package starling.extensions
{
import flash.geom.Matrix;
import starling.display.Mesh;
import starling.rendering.MeshEffect;
import starling.rendering.RenderState;
import starling.rendering.VertexData;
import starling.rendering.VertexDataFormat;
import starling.styles.MeshStyle;
import starling.textures.Texture;
public class MultiTextureStyle extends MeshStyle
{
public static const VERTEX_FORMAT:VertexDataFormat =
MeshStyle.VERTEX_FORMAT.extend("texture:float1");
public static const MAX_NUM_TEXTURES:int = 4;
private var _textures:Vector.<Texture>;
private static var sTextureIndexMap:Array = [];
public function MultiTextureStyle()
{
_textures = new <Texture>[];
}
override public function createEffect():MeshEffect
{
return new MultiTextureEffect();
}
override public function updateEffect(effect:MeshEffect, state:RenderState):void
{
var targetEffect:MultiTextureEffect = effect as MultiTextureEffect;
var numTextures:int = _textures.length;
targetEffect.clearTextures();
super.updateEffect(effect, state);
for (var i:int=0; i<numTextures; ++i)
targetEffect.setTextureAt(i, _textures[i]);
}
override public function canBatchWith(meshStyle:MeshStyle):Boolean
{
var mtStyle:MultiTextureStyle = meshStyle as MultiTextureStyle;
if (mtStyle)
{
var i:int;
var numTexturesToAdd:int = _textures.length;
if (mtStyle.numTextures + numTexturesToAdd > MAX_NUM_TEXTURES)
{
var numSharedTextures:int = 0;
for (i=0; i<numTexturesToAdd; ++i)
if (mtStyle.getTextureIndex(_textures[i]) != -1)
numSharedTextures++;
return mtStyle.numTextures + numTexturesToAdd - numSharedTextures
<= MAX_NUM_TEXTURES;
}
else return true;
}
return false;
}
override public function batchVertexData(target:MeshStyle, targetVertexID:int = 0,
matrix:Matrix = null, vertexID:int = 0,
numVertices:int = -1):void
{
super.batchVertexData(target, targetVertexID, matrix, vertexID, numVertices);
var mtTarget:MultiTextureStyle = target as MultiTextureStyle;
if (mtTarget)
{
var targetVertexData:VertexData = mtTarget.vertexData;
var sourceVertexData:VertexData = this.vertexData;
var numTextures:int = _textures.length;
var sourceTexID:int, targetTexID:int;
var i:int;
if (numVertices < 0)
numVertices = vertexData.numVertices - vertexID;
if (targetVertexID == 0)
mtTarget._textures.length = 0;
for (i = 0; i < numTextures; ++i)
{
var texture:Texture = _textures[i];
var textureIndexOnTarget:int = mtTarget.getTextureIndex(texture);
if (textureIndexOnTarget == -1)
{
textureIndexOnTarget = mtTarget.numTextures;
mtTarget.setTextureAt(texture, textureIndexOnTarget);
}
sTextureIndexMap[i] = textureIndexOnTarget;
}
for (i = 0; i < numVertices; ++i)
{
// TODO
// we could make this more efficient by storing the regions in which certain
// textures are used in a separate data structure, instead of accessing the
// vertex data over and over.
if (numTextures == 0) sourceTexID = -1;
else sourceTexID = sourceVertexData.getFloat(vertexID + i, "texture");
if (sourceTexID == -1) targetTexID = -1;
else targetTexID = sTextureIndexMap[sourceTexID];
if (sourceTexID == -1 || sourceTexID != targetTexID)
targetVertexData.setFloat(targetVertexID + i, "texture", targetTexID);
}
sTextureIndexMap.length = 0;
}
}
override protected function onTargetAssigned(target:Mesh):void
{
_textures.length = 0;
if (target.texture) _textures[0] = target.texture;
}
override public function get vertexFormat():VertexDataFormat
{
return VERTEX_FORMAT;
}
override public function set texture(value:Texture):void
{
if (value) _textures[0] = value;
super.texture = value;
}
private function setTextureAt(texture:Texture, index:int):void
{
_textures[index] = texture;
}
private function getTextureIndex(texture:Texture):int
{
var numTextures:int = _textures.length;
for (var i:int=0; i<numTextures; ++i)
if (_textures[i].base == texture.base) return i;
return -1;
}
private function get numTextures():int { return _textures.length; }
}
}
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import starling.core.Starling;
import starling.rendering.MeshEffect;
import starling.rendering.Program;
import starling.rendering.VertexDataFormat;
import starling.styles.MultiTextureStyle;
import starling.textures.Texture;
import starling.utils.RenderUtil;
class MultiTextureEffect extends MeshEffect
{
private var _textures:Vector.<Texture>;
private static const sTextureIndices:Vector.<Number> = new <Number>[0, 1, 2, 3];
private static const sOnes:Vector.<Number> = new <Number>[1, 1, 1, 1];
public function MultiTextureEffect()
{
_textures = new <Texture>[];
}
override protected function createProgram():Program
{
var vertexShader:Array = [
"m44 op, va0, vc0", // 4x4 matrix transform to output clip-space
"mov v0, va1 ", // pass texture coordinates to fragment program
"mul v1, va2, vc4", // multiply alpha (vc4) with color (va2), pass to fp
"mov v2, va3 " // pass texture sampler index to fp
];
// fc0 = [0, 1, 2, 3]
// fc1 = [1, 1, 1, 1]
var isBaseline:Boolean = Starling.current.profile.indexOf("baseline") != -1;
var agalVersion:uint = isBaseline ? 1 : 2;
var fragmentShader:Array = isBaseline ?
createFragmentShaderForBaselineProfile(numTextures) :
createFragmentShaderForStandardProfile(numTextures);
return Program.fromSource(vertexShader.join("\n"), fragmentShader.join("\n"), agalVersion);
}
private function createFragmentShaderForBaselineProfile(numTextures:int):Array
{
// In baseline profile, if-statements are not available. Instead,
// we sample all textures and multiply all but the active one with "zero".
var fragmentShader:Array = [];
if (numTextures == 0)
fragmentShader.push("mov ft4, fc0.xxxx"); // init with zero
if (numTextures > 0)
fragmentShader.push(
tex("ft0", "v0", 0, _textures[0]),
"seq ft1, v2.x, fc0.x",
"mul ft4, ft1, ft0"
);
if (numTextures > 1)
fragmentShader.push(
tex("ft0", "v0", 1, _textures[1]),
"seq ft1, v2.x, fc0.y",
"mul ft0, ft1, ft0",
"add ft4, ft4, ft0"
);
if (numTextures > 2)
fragmentShader.push(
tex("ft0", "v0", 2, _textures[2]),
"seq ft1, v2.x, fc0.z",
"mul ft0, ft1, ft0",
"add ft4, ft4, ft0"
);
if (numTextures > 3)
fragmentShader.push(
tex("ft0", "v0", 3, _textures[3]),
"seq ft1, v2.x, fc0.w",
"mul ft0, ft1, ft0",
"add ft4, ft4, ft0"
);
fragmentShader.push(
"slt ft0, v2.x, fc0.x", // no texture => v2.x < 0
"add ft4, ft4, ft0",
"mul oc, ft4, v1" // multiply color with texel color
);
return fragmentShader;
}
private function createFragmentShaderForStandardProfile(numTextures:int):Array
{
// In standard profile, we can actually choose the correct texture via an if-operation,
// which should be more efficient (fewer texture look-ups).
var fragmentShader:Array = [
"mov ft0, fc1" // init with white (used when no texture is assigned)
];
if (numTextures > 0)
fragmentShader.push(
"ife v2.x, fc0.x",
tex("ft0", "v0", 0, _textures[0]),
"eif"
);
if (numTextures > 1)
fragmentShader.push(
"ife v2.x, fc0.y",
tex("ft0", "v0", 1, _textures[1]),
"eif"
);
if (numTextures > 2)
fragmentShader.push(
"ife v2.x, fc0.z",
tex("ft0", "v0", 2, _textures[2]),
"eif"
);
if (numTextures > 3)
fragmentShader.push(
"ife v2.x, fc0.w",
tex("ft0", "v0", 3, _textures[3]),
"eif"
);
fragmentShader.push("mul oc, ft0, v1"); // multiply color with texel color
return fragmentShader;
}
override protected function beforeDraw(context:Context3D):void
{
super.beforeDraw(context);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, sTextureIndices);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, sOnes);
vertexFormat.setVertexBufferAt(1, vertexBuffer, "texCoords");
vertexFormat.setVertexBufferAt(3, vertexBuffer, "texture");
for (var i:int=0; i<numTextures; ++i)
{
var texture:Texture = _textures[i];
RenderUtil.setSamplerStateAt(i, texture.mipMapping, textureSmoothing, textureRepeat);
context.setTextureAt(i, texture.base);
}
}
override protected function afterDraw(context:Context3D):void
{
for (var i:int=0; i<numTextures; ++i)
context.setTextureAt(i, null);
context.setVertexBufferAt(1, null);
context.setVertexBufferAt(3, null);
super.afterDraw(context);
}
override public function get vertexFormat():VertexDataFormat
{
return MultiTextureStyle.VERTEX_FORMAT;
}
override protected function get programVariantName():uint
{
var numTextures:int = _textures.length;
var bits:uint = 0;
for (var i:int=0; i<numTextures; ++i)
bits |= RenderUtil.getTextureVariantBits(_textures[i]) << (4 * i);
return bits;
}
override public function set texture(value:Texture):void
{
if (value) _textures[0] = value;
super.texture = value;
}
public function setTextureAt(index:int, texture:Texture):void
{
_textures[index] = texture;
}
public function clearTextures():void
{
_textures.length = 0;
}
public function get numTextures():int { return _textures.length; }
}
@PrimaryFeather
Copy link
Author

Ah, I totally forgot about this extension! Thanks, @johnridges — you are right, that seems extremely likely. I will modify the code to a "floating point-safe" way. After all, this mesh style will still be useful in some cases!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment