UV Scrolling Material
I recently helped a friend out who was having some issues with alternativa being slow uploading a new texture multiple times to give an animated effect. It was mostly based of my movie material post. Anyway a lot of people use spritesheets in flash and in game development in general. So i helped setup a material that will sample different areas of a larger texture like a sprite sheet rather than constantly uploading a new texture to the gpu. Its pretty basic but you get the idea and can probably improve on it pretty easily. Here is the material
package alternativa.engine3d.materials {
import alternativa.engine3d.alternativa3d;
import alternativa.engine3d.core.Camera3D;
import alternativa.engine3d.core.DrawUnit;
import alternativa.engine3d.core.Light3D;
import alternativa.engine3d.core.Object3D;
import alternativa.engine3d.core.Renderer;
import alternativa.engine3d.core.VertexAttributes;
import alternativa.engine3d.materials.compiler.Linker;
import alternativa.engine3d.materials.compiler.Procedure;
import alternativa.engine3d.materials.compiler.VariableType;
import alternativa.engine3d.objects.Surface;
import alternativa.engine3d.resources.Geometry;
import alternativa.engine3d.resources.TextureResource;
import avmplus.getQualifiedClassName;
import flash.display3D.Context3D;
import flash.display3D.Context3DBlendFactor;
import flash.display3D.Context3DProgramType;
import flash.display3D.VertexBuffer3D;
import flash.utils.Dictionary;
import flash.utils.getDefinitionByName;
use namespace alternativa3d;
/**
* The material fills surface with bitmap image in light-independent manner. Can draw a Skin with no more than 41 Joints per surface. See Skin.divide() for more details.
*
* To be drawn with this material, geometry shoud have UV coordinates.
* @see alternativa.engine3d.objects.Skin#divide()
* @see alternativa.engine3d.core.VertexAttributes#TEXCOORDS
*/
public class UVScrollMaterial extends Material {
private static var caches:Dictionary = new Dictionary(true);
private var cachedContext3D:Context3D;
private var programsCache:Dictionary;
/**
* @private
* Procedure for diffuse map with alpha channel
*/
static alternativa3d const getDiffuseProcedure:Procedure = new Procedure([
"#v0=vUV",
"#s0=sDiffuse",
"#c0=cThresholdAlpha",
"#c1=cCustomUV",
"mov t1, v0",
"add t1.xy, t1.xy, c1.zw",
"div t1.xy, t1.xy, c1.xy",
"frc t1.xy, t1.xy",
"tex t0, t1, s0 <2d, linear,nearest, miplinear>",
"mul t0.w, t0.w, c0.w",
"mov o0, t0"
], "getDiffuseProcedure");
/**
* @private
* Procedure for diffuse with opacity map.
*/
static alternativa3d const getDiffuseOpacityProcedure:Procedure = new Procedure([
"#v0=vUV",
"#s0=sDiffuse",
"#s1=sOpacity",
"#c0=cThresholdAlpha",
"#c1=cCustomUV",
"mov t1, v0",
"add t1.xy, t1.xy, c1.zw",
"div t1.xy, t1.xy, c1.xy",
"frc t1.xy, t1.xy",
"tex t0, t1, s0 <2d, linear,nearest, miplinear>",
"tex t1, t1, s1 <2d, linear,nearest, miplinear>",
"mul t0.w, t1.x, c0.w",
"mov o0, t0"
], "getDiffuseOpacityProcedure");
/**
* @private
* Alpha-test check procedure.
*/
static alternativa3d const thresholdOpaqueAlphaProcedure:Procedure = new Procedure([
"#c0=cThresholdAlpha",
"sub t0.w, i0.w, c0.x",
"kil t0.w",
"mov o0, i0"
], "thresholdOpaqueAlphaProcedure");
/**
* @private
* Alpha-test check procedure.
*/
static alternativa3d const thresholdTransparentAlphaProcedure:Procedure = new Procedure([
"#c0=cThresholdAlpha",
"slt t0.w, i0.w, c0.x",
"mul i0.w, t0.w, i0.w",
"mov o0, i0"
], "thresholdTransparentAlphaProcedure");
/**
* @private
* Pass UV to the fragment shader procedure
*/
static alternativa3d const _passUVProcedure:Procedure = new Procedure(["#v0=vUV", "#a0=aUV", "mov v0, a0"], "passUVProcedure");
/**
* Diffuse map.
*/
public var diffuseMap:TextureResource;
/**
* Opacity map.
*/
public var opacityMap:TextureResource;
/**
* If true, perform transparent pass. Parts of surface, cumulative alpha value of which is below than alphaThreshold will be drawn within transparent pass.
* @see #alphaThreshold
*/
public var transparentPass:Boolean = true;
/**
* If true, perform opaque pass. Parts of surface, cumulative alpha value of which is greater or equal than alphaThreshold will be drawn within opaque pass.
* @see #alphaThreshold
*/
public var opaquePass:Boolean = true;
/**
* alphaThreshold defines starts from which value of alpha a fragment of the surface will get into transparent pass.
* @see #transparentPass
* @see #opaquePass
*/
public var alphaThreshold:Number = 0;
/**
* Transparency.
*/
public var alpha:Number = 1;
public var atlasWidth:Number = 0;
public var atlasHeight:Number = 0;
public var spriteWidth:Number = 16;
public var spriteHeight:Number = 16;
public var x:Number = 0;
public var y:Number = 1;
/**
* Creates a new TextureMaterial instance.
*
* @param diffuseMap Diffuse map.
* @param alpha Transparency.
*/
public function UVScrollMaterial(diffuseMap:TextureResource = null, opacityMap:TextureResource = null, alpha:Number = 1, atlasWidth:Number = 0, atlasHeight:Number = 0, spriteWidth:Number = 16, spriteHeight:Number = 16) {
this.diffuseMap = diffuseMap;
this.opacityMap = opacityMap;
this.alpha = alpha;
this.atlasWidth = atlasWidth;
this.atlasHeight = atlasHeight;
this.spriteWidth = spriteWidth;
this.spriteHeight = spriteHeight;
}
public function setTileXY(x:Number=0,y:Number=0):void
{
this.x = x;
this.y = y;
}
/**
* @private
*/
override alternativa3d function fillResources(resources:Dictionary, resourceType:Class):void {
super.fillResources(resources, resourceType);
if (diffuseMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(diffuseMap)) as Class, resourceType)) {
resources[diffuseMap] = true;
}
if (opacityMap != null && A3DUtils.checkParent(getDefinitionByName(getQualifiedClassName(opacityMap)) as Class, resourceType)) {
resources[opacityMap] = true;
}
}
/**
* @param object
* @param programs
* @param camera
* @param opacityMap
* @param alphaTest 0 - disabled, 1 - opaque, 2 - contours
* @return
*/
private function getProgram(object:Object3D, programs:Vector., camera:Camera3D, opacityMap:TextureResource, alphaTest:int):UVScrollMaterialProgram {
var key:int = (opacityMap != null ? 3 : 0) + alphaTest;
var program:UVScrollMaterialProgram = programs[key];
if (program == null) {
// Make program
// Vertex shader
var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX);
var positionVar:String = "aPosition";
vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE);
if (object.transformProcedure != null) {
positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker);
}
vertexLinker.addProcedure(_projectProcedure);
vertexLinker.setInputParams(_projectProcedure, positionVar);
vertexLinker.addProcedure(_passUVProcedure);
// Pixel shader
var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT);
var outProcedure:Procedure = (opacityMap != null ? getDiffuseOpacityProcedure : getDiffuseProcedure);
fragmentLinker.addProcedure(outProcedure);
if (alphaTest > 0) {
fragmentLinker.declareVariable("tColor");
fragmentLinker.setOutputParams(outProcedure, "tColor");
if (alphaTest == 1) {
fragmentLinker.addProcedure(thresholdOpaqueAlphaProcedure, "tColor");
} else {
fragmentLinker.addProcedure(thresholdTransparentAlphaProcedure, "tColor");
}
}
fragmentLinker.varyings = vertexLinker.varyings;
program = new UVScrollMaterialProgram(vertexLinker, fragmentLinker);
program.upload(camera.context3D);
programs[key] = program;
}
return program;
}
private function getDrawUnit(program:UVScrollMaterialProgram, camera:Camera3D, surface:Surface, geometry:Geometry, opacityMap:TextureResource):DrawUnit {
var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION);
var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]);
var object:Object3D = surface.object;
// Draw call
var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program);
// Streams
drawUnit.setVertexBufferAt(program.aPosition, positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]);
drawUnit.setVertexBufferAt(program.aUV, uvBuffer, geometry._attributesOffsets[VertexAttributes.TEXCOORDS[0]], VertexAttributes.FORMATS[VertexAttributes.TEXCOORDS[0]]);
//Constants
object.setTransformConstants(drawUnit, surface, program.vertexShader, camera);
drawUnit.setProjectionConstants(camera, program.cProjMatrix, object.localToCameraTransform);
drawUnit.setFragmentConstantsFromNumbers(program.cThresholdAlpha, alphaThreshold, 0, 0, alpha);
drawUnit.setFragmentConstantsFromNumbers(program.cCustomUV, 1 / (spriteWidth / atlasWidth), 1, x, y);
// Textures
drawUnit.setTextureAt(program.sDiffuse, diffuseMap._texture);
if (opacityMap != null) {
drawUnit.setTextureAt(program.sOpacity, opacityMap._texture);
}
return drawUnit;
}
/**
* @private
*/
override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, useShadow:Boolean, objectRenderPriority:int = -1):void {
var object:Object3D = surface.object;
// Buffers
var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION);
var uvBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.TEXCOORDS[0]);
// Check validity
if (positionBuffer == null || uvBuffer == null || diffuseMap == null || diffuseMap._texture == null || opacityMap != null && opacityMap._texture == null) return;
// Refresh program cache for this context
if (camera.context3D != cachedContext3D) {
cachedContext3D = camera.context3D;
programsCache = caches[cachedContext3D];
if (programsCache == null) {
programsCache = new Dictionary();
caches[cachedContext3D] = programsCache;
}
}
var optionsPrograms:Vector. = programsCache[object.transformProcedure];
if(optionsPrograms == null) {
optionsPrograms = new Vector.(6, true);
programsCache[object.transformProcedure] = optionsPrograms;
}
var program:UVScrollMaterialProgram;
var drawUnit:DrawUnit;
// Opaque pass
if (opaquePass && alphaThreshold <= alpha) {
if (alphaThreshold > 0) {
// Alpha test
// use opacityMap if it is presented
program = getProgram(object, optionsPrograms, camera, opacityMap, 1);
drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap);
} else {
// do not use opacityMap at all
program = getProgram(object, optionsPrograms, camera, null, 0);
drawUnit = getDrawUnit(program, camera, surface, geometry, null);
}
// Use z-buffer within DrawCall, draws without blending
camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE);
}
// Transparent pass
if (transparentPass && alphaThreshold > 0 && alpha > 0) {
// use opacityMap if it is presented
if (alphaThreshold <= alpha && !opaquePass) {
// Alpha threshold
program = getProgram(object, optionsPrograms, camera, opacityMap, 2);
drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap);
} else {
// There is no Alpha threshold or check z-buffer by previous pass
program = getProgram(object, optionsPrograms, camera, opacityMap, 0);
drawUnit = getDrawUnit(program, camera, surface, geometry, opacityMap);
}
// Do not use z-buffer, draws with blending
drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA;
drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA;
camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT);
}
}
/**
* @inheritDoc
*/
override public function clone():Material {
var res:UVScrollMaterial = new UVScrollMaterial(diffuseMap, opacityMap, alpha);
res.clonePropertiesFrom(this);
return res;
}
/**
* @inheritDoc
*/
override protected function clonePropertiesFrom(source:Material):void {
super.clonePropertiesFrom(source);
var tex:TextureMaterial = source as TextureMaterial;
diffuseMap = tex.diffuseMap;
opacityMap = tex.opacityMap;
opaquePass = tex.opaquePass;
transparentPass = tex.transparentPass;
alphaThreshold = tex.alphaThreshold;
alpha = tex.alpha;
}
}
}
import alternativa.engine3d.materials.ShaderProgram;
import alternativa.engine3d.materials.compiler.Linker;
import flash.display3D.Context3D;
class UVScrollMaterialProgram extends ShaderProgram {
public var aPosition:int = -1;
public var aUV:int = -1;
public var cProjMatrix:int = -1;
public var cThresholdAlpha:int = -1;
public var cCustomUV:int = -1;
public var sDiffuse:int = -1;
public var sOpacity:int = -1;
public function UVScrollMaterialProgram(vertex:Linker, fragment:Linker) {
super(vertex, fragment);
}
override public function upload(context3D:Context3D):void {
super.upload(context3D);
aPosition = vertexShader.findVariable("aPosition");
aUV = vertexShader.findVariable("aUV");
cProjMatrix = vertexShader.findVariable("cProjMatrix");
cThresholdAlpha = fragmentShader.findVariable("cThresholdAlpha");
sDiffuse = fragmentShader.findVariable("sDiffuse");
sOpacity = fragmentShader.findVariable("sOpacity");
cCustomUV = fragmentShader.findVariable("cCustomUV");
}
}
The usage is pretty simple, just setup the sheet and then setup the material passing in the sheet idth and height and the individual area size in this case 16x16. Then to switch area at runtime call setTileXY and the position.
private var material:UVScrollMaterial;
[Embed(source = "../assets/testsheet.png")] private var sheet:Class;
private var sheetbd:BitmapData = new sheet().bitmapData;
private var sheetres:BitmapTextureResource = new BitmapTextureResource(sheetbd);
material = new UVScrollMaterial(sheetres, null, 1, sheetbd.width, sheetbd.height, 16, 16);
material.setTileXY(x, y);
Hey David, i made a modification to this class to add some extra behaviour, i posted a blog post here: http://tracehello.wordpress.com/2014/05/17/uv-scrolling-material-for-alternativa3d/
let me know what you think!