avm2: Add PixelBender bytecode parsing to ShaderData
We now parse PixelBender bytecode, and populate the parameters from the bytecode on `ShaderData`. This is enough to progress Steamlands, which needs to access dynamically set properties on `ShaderData` Bytecode execution is not implemented yet.
This commit is contained in:
parent
999e2f5b71
commit
21429cc205
|
@ -138,6 +138,8 @@ pub struct SystemClasses<'gc> {
|
|||
pub cubetexture: ClassObject<'gc>,
|
||||
pub rectangletexture: ClassObject<'gc>,
|
||||
pub morphshape: ClassObject<'gc>,
|
||||
pub shaderinput: ClassObject<'gc>,
|
||||
pub shaderparameter: ClassObject<'gc>,
|
||||
}
|
||||
|
||||
impl<'gc> SystemClasses<'gc> {
|
||||
|
@ -250,6 +252,8 @@ impl<'gc> SystemClasses<'gc> {
|
|||
cubetexture: object,
|
||||
rectangletexture: object,
|
||||
morphshape: object,
|
||||
shaderinput: object,
|
||||
shaderparameter: object,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -667,6 +671,8 @@ fn load_playerglobal<'gc>(
|
|||
("flash.display", "LoaderInfo", loaderinfo),
|
||||
("flash.display", "MorphShape", morphshape),
|
||||
("flash.display", "MovieClip", movieclip),
|
||||
("flash.display", "ShaderInput", shaderinput),
|
||||
("flash.display", "ShaderParameter", shaderparameter),
|
||||
("flash.display", "Shape", shape),
|
||||
("flash.display", "SimpleButton", simplebutton),
|
||||
("flash.display", "Sprite", sprite),
|
||||
|
|
|
@ -10,6 +10,8 @@ pub mod loader;
|
|||
pub mod loader_info;
|
||||
pub mod morph_shape;
|
||||
pub mod movie_clip;
|
||||
pub mod shader_data;
|
||||
pub mod shader_parameter;
|
||||
pub mod shape;
|
||||
pub mod simple_button;
|
||||
pub mod sprite;
|
||||
|
|
|
@ -14,17 +14,14 @@ package flash.display {
|
|||
}
|
||||
|
||||
public function set byteCode(code:ByteArray):void {
|
||||
stub_setter("flash.display.Shader", "byteCode");
|
||||
this._data = new ShaderData(code);
|
||||
}
|
||||
|
||||
public function get data():ShaderData {
|
||||
stub_getter("flash.display.Shader", "data");
|
||||
return this._data;
|
||||
}
|
||||
|
||||
public function set data(value:ShaderData):void {
|
||||
stub_setter("flash.display.Shader", "data");
|
||||
this._data = value;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,10 @@ package flash.display {
|
|||
|
||||
public final dynamic class ShaderData {
|
||||
public function ShaderData(bytecode:ByteArray) {
|
||||
stub_constructor("flash.display.ShaderData");
|
||||
this.init(bytecode);
|
||||
}
|
||||
|
||||
private native function init(bytecode:ByteArray);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package flash.display {
|
||||
public final dynamic class ShaderInput {
|
||||
internal var _channels: int;
|
||||
internal var _height: int;
|
||||
internal var _index: int;
|
||||
internal var _object: Object;
|
||||
internal var _width: int;
|
||||
|
||||
public function get channels():int {
|
||||
return _channels;
|
||||
}
|
||||
|
||||
public function get height():int {
|
||||
return _height;
|
||||
}
|
||||
|
||||
public function get index():int {
|
||||
return _index;
|
||||
}
|
||||
|
||||
public function get width():int {
|
||||
return _width;
|
||||
}
|
||||
|
||||
public function get input():Object {
|
||||
return _object;
|
||||
}
|
||||
|
||||
public function set input(value:Object):void {
|
||||
// FIXME - validate
|
||||
_object = value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package flash.display {
|
||||
import __ruffle__.stub_method;
|
||||
import __ruffle__.stub_getter;
|
||||
import __ruffle__.stub_setter;
|
||||
import __ruffle__.stub_constructor;
|
||||
import flash.events.EventDispatcher;
|
||||
|
||||
public class ShaderJob extends EventDispatcher {
|
||||
|
||||
public function ShaderJob(shader:Shader = null, target:Object = null, width:int = 0, height:int = 0) {
|
||||
stub_constructor("flash.display.ShaderJob");
|
||||
}
|
||||
|
||||
public function cancel():void {
|
||||
stub_method("flash.display.ShaderJob", "cancel")
|
||||
}
|
||||
|
||||
public function start(waitForCompletion:Boolean = false):void {
|
||||
stub_method("flash.display.ShaderJob", "start")
|
||||
}
|
||||
|
||||
public function get height():int {
|
||||
stub_getter("flash.display.ShaderJob", "height");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function set height(value:int):void {
|
||||
stub_setter("flash.display.ShaderJob", "height");
|
||||
}
|
||||
|
||||
public function get progress():Number {
|
||||
stub_getter("flash.display.ShaderJob", "progress")
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function get shader():Shader {
|
||||
stub_getter("flash.display.ShaderJob", "shader");
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set shader(value:Shader):void {
|
||||
stub_setter("flash.display.ShaderJob", "shader");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package flash.display {
|
||||
public final dynamic class ShaderParameter {
|
||||
|
||||
internal var _index:int;
|
||||
internal var _type:String;
|
||||
internal var _value:Array;
|
||||
|
||||
public function get index():int {
|
||||
return this._index;
|
||||
}
|
||||
public function get value():Array {
|
||||
return this._value.concat();
|
||||
}
|
||||
public function set value(value:Array):void {
|
||||
// FIXME - perform validation
|
||||
this._value = value.concat();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
use crate::{
|
||||
avm2::{
|
||||
parameters::ParametersExt, string::AvmString, Activation, Error, Object, TObject, Value,
|
||||
},
|
||||
pixel_bender::{PixelBenderParam, PixelBenderParamQualifier},
|
||||
};
|
||||
|
||||
use super::shader_parameter::make_shader_parameter;
|
||||
|
||||
/// Implements `ShaderData.init`, which is called from the constructor
|
||||
pub fn init<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
this: Option<Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let mut this = this.unwrap();
|
||||
let bytecode = args.get_object(activation, 0, "bytecode")?;
|
||||
let bytecode = bytecode.as_bytearray().unwrap();
|
||||
let shader = crate::pixel_bender::parse_shader(bytecode.bytes());
|
||||
|
||||
for meta in shader.metadata {
|
||||
let name = AvmString::new_utf8(activation.context.gc_context, &meta.key);
|
||||
let value = meta.value.into_avm2_value(activation)?;
|
||||
this.set_public_property(name, value, activation)?;
|
||||
}
|
||||
this.set_public_property(
|
||||
"name",
|
||||
AvmString::new_utf8(activation.context.gc_context, &shader.name).into(),
|
||||
activation,
|
||||
)?;
|
||||
|
||||
for (index, param) in shader.params.into_iter().enumerate() {
|
||||
let name = match ¶m {
|
||||
PixelBenderParam::Normal {
|
||||
name, qualifier, ..
|
||||
} => {
|
||||
// Neither of these show up in Flash Player
|
||||
if name == "_OutCoord" || matches!(qualifier, PixelBenderParamQualifier::Output) {
|
||||
continue;
|
||||
}
|
||||
name
|
||||
}
|
||||
PixelBenderParam::Texture { name, .. } => name,
|
||||
};
|
||||
|
||||
let name = AvmString::new_utf8(activation.context.gc_context, name);
|
||||
let param_obj = make_shader_parameter(activation, param, index)?;
|
||||
this.set_public_property(name, param_obj, activation)?;
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
use crate::{
|
||||
avm2::{string::AvmString, Activation, Error, Multiname, TObject, Value},
|
||||
pixel_bender::PixelBenderParam,
|
||||
};
|
||||
|
||||
pub fn make_shader_parameter<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
param: PixelBenderParam,
|
||||
index: usize,
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let ns = activation.avm2().flash_display_internal;
|
||||
|
||||
match param {
|
||||
PixelBenderParam::Normal {
|
||||
name,
|
||||
param_type,
|
||||
metadata,
|
||||
..
|
||||
} => {
|
||||
let mut obj = activation
|
||||
.avm2()
|
||||
.classes()
|
||||
.shaderparameter
|
||||
.construct(activation, &[])?;
|
||||
let type_name =
|
||||
AvmString::new_utf8(activation.context.gc_context, ¶m_type.to_string());
|
||||
|
||||
obj.set_property(&Multiname::new(ns, "_index"), index.into(), activation)?;
|
||||
obj.set_property(&Multiname::new(ns, "_type"), type_name.into(), activation)?;
|
||||
for meta in metadata {
|
||||
let name = AvmString::new_utf8(activation.context.gc_context, &meta.key);
|
||||
let value = meta.value.clone().into_avm2_value(activation)?;
|
||||
obj.set_public_property(name, value, activation)?;
|
||||
}
|
||||
obj.set_public_property(
|
||||
"name",
|
||||
AvmString::new_utf8(activation.context.gc_context, name).into(),
|
||||
activation,
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
}
|
||||
PixelBenderParam::Texture { name, channels, .. } => {
|
||||
let mut obj = activation
|
||||
.avm2()
|
||||
.classes()
|
||||
.shaderinput
|
||||
.construct(activation, &[])?;
|
||||
obj.set_property(
|
||||
&Multiname::new(ns, "_channels"),
|
||||
channels.into(),
|
||||
activation,
|
||||
)?;
|
||||
obj.set_property(&Multiname::new(ns, "_index"), index.into(), activation)?;
|
||||
obj.set_public_property(
|
||||
"name",
|
||||
AvmString::new_utf8(activation.context.gc_context, name).into(),
|
||||
activation,
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -88,6 +88,9 @@ include "flash/display/PNGEncoderOptions.as"
|
|||
include "flash/display/Scene.as"
|
||||
include "flash/display/Shader.as"
|
||||
include "flash/display/ShaderData.as"
|
||||
include "flash/display/ShaderInput.as"
|
||||
include "flash/display/ShaderJob.as"
|
||||
include "flash/display/ShaderParameter.as"
|
||||
include "flash/display/ShaderParameterType.as"
|
||||
include "flash/display/ShaderPrecision.as"
|
||||
include "flash/display/Shape.as"
|
||||
|
|
|
@ -34,6 +34,7 @@ mod library;
|
|||
pub mod limits;
|
||||
pub mod loader;
|
||||
mod locale;
|
||||
mod pixel_bender;
|
||||
mod player;
|
||||
mod prelude;
|
||||
mod streams;
|
||||
|
|
|
@ -0,0 +1,504 @@
|
|||
//! Pixel bender bytecode parsing code.
|
||||
//! This is heavling based on https://github.com/jamesward/pbjas and https://github.com/HaxeFoundation/format/tree/master/format/pbj
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
|
||||
use num_traits::FromPrimitive;
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
avm2::{Activation, ArrayObject, ArrayStorage, Error, Value},
|
||||
ecma_conversions::f64_to_wrapping_i32,
|
||||
string::AvmString,
|
||||
};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PixelBenderType {
|
||||
TFloat(f32) = 0x1,
|
||||
TFloat2(f32, f32) = 0x2,
|
||||
TFloat3(f32, f32, f32) = 0x3,
|
||||
TFloat4(f32, f32, f32, f32) = 0x4,
|
||||
TFloat2x2([f32; 4]) = 0x5,
|
||||
TFloat3x3([f32; 9]) = 0x6,
|
||||
TFloat4x4([f32; 16]) = 0x7,
|
||||
TInt(i16) = 0x8,
|
||||
TInt2(i16, i16) = 0x9,
|
||||
TInt3(i16, i16, i16) = 0xA,
|
||||
TInt4(i16, i16, i16, i16) = 0xB,
|
||||
TString(String) = 0xC,
|
||||
}
|
||||
|
||||
impl PixelBenderType {
|
||||
pub fn into_avm2_value<'gc>(
|
||||
self,
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
// Flash appears to use a uint/int if the float has no fractional part
|
||||
let cv = |f: f32| -> Value<'gc> {
|
||||
if f.fract() == 0.0 {
|
||||
f64_to_wrapping_i32(f as f64).into()
|
||||
} else {
|
||||
f.into()
|
||||
}
|
||||
};
|
||||
let vals: Vec<Value<'gc>> = match self {
|
||||
PixelBenderType::TString(string) => {
|
||||
return Ok(AvmString::new_utf8(activation.context.gc_context, string).into());
|
||||
}
|
||||
PixelBenderType::TInt(i) => return Ok(i.into()),
|
||||
PixelBenderType::TFloat(f) => vec![cv(f)],
|
||||
PixelBenderType::TFloat2(f1, f2) => vec![cv(f1), cv(f2)],
|
||||
PixelBenderType::TFloat3(f1, f2, f3) => vec![cv(f1), cv(f2), cv(f3)],
|
||||
PixelBenderType::TFloat4(f1, f2, f3, f4) => vec![cv(f1), cv(f2), cv(f3), cv(f4)],
|
||||
PixelBenderType::TFloat2x2(floats) => floats.iter().map(|f| cv(*f)).collect(),
|
||||
PixelBenderType::TFloat3x3(floats) => floats.iter().map(|f| cv(*f)).collect(),
|
||||
PixelBenderType::TFloat4x4(floats) => floats.iter().map(|f| cv(*f)).collect(),
|
||||
PixelBenderType::TInt2(i1, i2) => vec![i1.into(), i2.into()],
|
||||
PixelBenderType::TInt3(i1, i2, i3) => vec![i1.into(), i2.into(), i3.into()],
|
||||
PixelBenderType::TInt4(i1, i2, i3, i4) => {
|
||||
vec![i1.into(), i2.into(), i3.into(), i4.into()]
|
||||
}
|
||||
};
|
||||
let storage = ArrayStorage::from_args(&vals);
|
||||
Ok(ArrayObject::from_storage(activation, storage)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME - come up with a way to reduce duplication here
|
||||
#[derive(num_derive::FromPrimitive, Debug, PartialEq)]
|
||||
pub enum PixelBenderTypeOpcode {
|
||||
TFloat = 0x1,
|
||||
TFloat2 = 0x2,
|
||||
TFloat3 = 0x3,
|
||||
TFloat4 = 0x4,
|
||||
TFloat2x2 = 0x5,
|
||||
TFloat3x3 = 0x6,
|
||||
TFloat4x4 = 0x7,
|
||||
TInt = 0x8,
|
||||
TInt2 = 0x9,
|
||||
TInt3 = 0xA,
|
||||
TInt4 = 0xB,
|
||||
TString = 0xC,
|
||||
}
|
||||
|
||||
#[derive(num_derive::FromPrimitive, Debug, PartialEq)]
|
||||
pub enum PixelBenderParamQualifier {
|
||||
Input = 1,
|
||||
Output = 2,
|
||||
}
|
||||
|
||||
impl Display for PixelBenderTypeOpcode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
PixelBenderTypeOpcode::TFloat => "float",
|
||||
PixelBenderTypeOpcode::TFloat2 => "float2",
|
||||
PixelBenderTypeOpcode::TFloat3 => "float3",
|
||||
PixelBenderTypeOpcode::TFloat4 => "float4",
|
||||
PixelBenderTypeOpcode::TFloat2x2 => "matrix2x2",
|
||||
PixelBenderTypeOpcode::TFloat3x3 => "matrix3x3",
|
||||
PixelBenderTypeOpcode::TFloat4x4 => "matrix4x4",
|
||||
PixelBenderTypeOpcode::TInt => "int",
|
||||
PixelBenderTypeOpcode::TInt2 => "int2",
|
||||
PixelBenderTypeOpcode::TInt3 => "int3",
|
||||
PixelBenderTypeOpcode::TInt4 => "int4",
|
||||
PixelBenderTypeOpcode::TString => "string",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(num_derive::FromPrimitive, Debug, PartialEq)]
|
||||
pub enum Opcode {
|
||||
Nop = 0x0,
|
||||
Add = 0x1,
|
||||
Sub = 0x2,
|
||||
Mul = 0x3,
|
||||
Rcp = 0x4,
|
||||
Div = 0x5,
|
||||
Atan2 = 0x6,
|
||||
Pow = 0x7,
|
||||
Mod = 0x8,
|
||||
Min = 0x9,
|
||||
Max = 0xA,
|
||||
Step = 0xB,
|
||||
Sin = 0xC,
|
||||
Cos = 0xD,
|
||||
Tan = 0xE,
|
||||
Asin = 0xF,
|
||||
Acos = 0x10,
|
||||
Atan = 0x11,
|
||||
Exp = 0x12,
|
||||
Exp2 = 0x13,
|
||||
Log = 0x14,
|
||||
Log2 = 0x15,
|
||||
Sqrt = 0x16,
|
||||
RSqrt = 0x17,
|
||||
Abs = 0x18,
|
||||
Sign = 0x19,
|
||||
Floor = 0x1A,
|
||||
Ceil = 0x1B,
|
||||
Fract = 0x1C,
|
||||
Mov = 0x1D,
|
||||
FloatToInt = 0x1E,
|
||||
IntToFloat = 0x1F,
|
||||
MatMatMul = 0x20,
|
||||
VecMatMul = 0x21,
|
||||
MatVecMul = 0x22,
|
||||
Normalize = 0x23,
|
||||
Length = 0x24,
|
||||
Distance = 0x25,
|
||||
DotProduct = 0x26,
|
||||
CrossProduct = 0x27,
|
||||
Equal = 0x28,
|
||||
NotEqual = 0x29,
|
||||
LessThan = 0x2A,
|
||||
LessThanEqual = 0x2B,
|
||||
LogicalNot = 0x2C,
|
||||
LogicalAnd = 0x2D,
|
||||
LogicalOr = 0x2E,
|
||||
LogicalXor = 0x2F,
|
||||
SampleNearest = 0x30,
|
||||
SampleLinear = 0x31,
|
||||
LoadIntOrFloat = 0x32,
|
||||
Loop = 0x33,
|
||||
If = 0x34,
|
||||
Else = 0x35,
|
||||
EndIf = 0x36,
|
||||
FloatToBool = 0x37,
|
||||
BoolToFloat = 0x38,
|
||||
IntToBool = 0x39,
|
||||
BoolToInt = 0x3A,
|
||||
VectorEqual = 0x3B,
|
||||
VectorNotEqual = 0x3C,
|
||||
BoolAny = 0x3D,
|
||||
BoolAll = 0x3E,
|
||||
PBJMeta1 = 0xA0,
|
||||
PBJParam = 0xA1,
|
||||
PBJMeta2 = 0xA2,
|
||||
PBJParamTexture = 0xA3,
|
||||
Name = 0xA4,
|
||||
Version = 0xA5,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Operation {
|
||||
Nop,
|
||||
Normal {
|
||||
opcode: Opcode,
|
||||
dst: u16,
|
||||
mask: u8,
|
||||
src: u32,
|
||||
other: u8,
|
||||
},
|
||||
LoadInt {
|
||||
dst: u16,
|
||||
mask: u8,
|
||||
val: i32,
|
||||
},
|
||||
LoadFloat {
|
||||
dst: u16,
|
||||
mask: u8,
|
||||
val: f32,
|
||||
},
|
||||
If {
|
||||
src: u32,
|
||||
},
|
||||
Else,
|
||||
EndIf,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PixelBenderShader {
|
||||
pub name: String,
|
||||
pub version: i32,
|
||||
pub params: Vec<PixelBenderParam>,
|
||||
pub metadata: Vec<PixelBenderMetadata>,
|
||||
pub operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PixelBenderParam {
|
||||
Normal {
|
||||
qualifier: PixelBenderParamQualifier,
|
||||
param_type: PixelBenderTypeOpcode,
|
||||
reg: u16,
|
||||
mask: u8,
|
||||
name: String,
|
||||
metadata: Vec<PixelBenderMetadata>,
|
||||
},
|
||||
Texture {
|
||||
index: u8,
|
||||
channels: u8,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PixelBenderMetadata {
|
||||
pub key: String,
|
||||
pub value: PixelBenderType,
|
||||
}
|
||||
|
||||
/// Parses PixelBender bytecode
|
||||
pub fn parse_shader(mut data: &[u8]) -> PixelBenderShader {
|
||||
let mut shader = PixelBenderShader {
|
||||
name: String::new(),
|
||||
version: 0,
|
||||
params: Vec::new(),
|
||||
metadata: Vec::new(),
|
||||
operations: Vec::new(),
|
||||
};
|
||||
let data = &mut data;
|
||||
let mut metadata = Vec::new();
|
||||
while !data.is_empty() {
|
||||
read_op(data, &mut shader, &mut metadata).unwrap();
|
||||
}
|
||||
// Any metadata left in the vec is associated with our final parameter.
|
||||
apply_metadata(&mut shader, &mut metadata);
|
||||
shader
|
||||
}
|
||||
|
||||
fn read_op<R: Read>(
|
||||
data: &mut R,
|
||||
shader: &mut PixelBenderShader,
|
||||
metadata: &mut Vec<PixelBenderMetadata>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let raw = data.read_u8()?;
|
||||
let opcode = Opcode::from_u8(raw).expect("Unknown opcode");
|
||||
match opcode {
|
||||
Opcode::Nop => {
|
||||
assert_eq!(data.read_u32::<LittleEndian>()?, 0);
|
||||
assert_eq!(data.read_u16::<LittleEndian>()?, 0);
|
||||
shader.operations.push(Operation::Nop);
|
||||
}
|
||||
Opcode::PBJMeta1 | Opcode::PBJMeta2 => {
|
||||
let meta_type = data.read_u8()?;
|
||||
let meta_key = read_string(data)?;
|
||||
let meta_value = read_value(data, PixelBenderTypeOpcode::from_u8(meta_type).unwrap())?;
|
||||
metadata.push(PixelBenderMetadata {
|
||||
key: meta_key,
|
||||
value: meta_value,
|
||||
});
|
||||
}
|
||||
Opcode::PBJParam => {
|
||||
let qualifier = data.read_u8()?;
|
||||
let param_type = data.read_u8()?;
|
||||
let reg = data.read_u16::<LittleEndian>()?;
|
||||
let mask = data.read_u8()?;
|
||||
let name = read_string(data)?;
|
||||
|
||||
let param_type = PixelBenderTypeOpcode::from_u8(param_type).unwrap_or_else(|| {
|
||||
panic!("Unexpected param type {param_type}");
|
||||
});
|
||||
let qualifier = PixelBenderParamQualifier::from_u8(qualifier)
|
||||
.unwrap_or_else(|| panic!("Unexpected param qualifier {qualifier:?}"));
|
||||
apply_metadata(shader, metadata);
|
||||
|
||||
shader.params.push(PixelBenderParam::Normal {
|
||||
qualifier,
|
||||
param_type,
|
||||
reg,
|
||||
mask,
|
||||
name,
|
||||
metadata: Vec::new(),
|
||||
})
|
||||
}
|
||||
Opcode::PBJParamTexture => {
|
||||
let index = data.read_u8()?;
|
||||
let channels = data.read_u8()?;
|
||||
let name = read_string(data)?;
|
||||
apply_metadata(shader, metadata);
|
||||
|
||||
shader.params.push(PixelBenderParam::Texture {
|
||||
index,
|
||||
channels,
|
||||
name,
|
||||
});
|
||||
}
|
||||
Opcode::Name => {
|
||||
let len = data.read_u16::<LittleEndian>()?;
|
||||
let mut string_bytes = vec![0; len as usize];
|
||||
data.read_exact(&mut string_bytes)?;
|
||||
shader.name = String::from_utf8(string_bytes)?;
|
||||
}
|
||||
Opcode::Version => {
|
||||
shader.version = data.read_i32::<LittleEndian>()?;
|
||||
}
|
||||
Opcode::If => {
|
||||
assert_eq!(read_uint24(data)?, 0);
|
||||
let src = read_uint24(data)?;
|
||||
assert_eq!(data.read_u8()?, 0);
|
||||
shader.operations.push(Operation::If { src });
|
||||
}
|
||||
Opcode::Else => {
|
||||
assert_eq!(data.read_u32::<LittleEndian>()?, 0);
|
||||
assert_eq!(read_uint24(data)?, 0);
|
||||
shader.operations.push(Operation::Else);
|
||||
}
|
||||
Opcode::EndIf => {
|
||||
assert_eq!(data.read_u32::<LittleEndian>()?, 0);
|
||||
assert_eq!(read_uint24(data)?, 0);
|
||||
shader.operations.push(Operation::EndIf);
|
||||
}
|
||||
Opcode::LoadIntOrFloat => {
|
||||
let dst = data.read_u16::<LittleEndian>()?;
|
||||
let mask = data.read_u8()?;
|
||||
assert_eq!(mask & 0xF, 0);
|
||||
if dst & 0x8000 != 0 {
|
||||
let val = data.read_i32::<LittleEndian>()?;
|
||||
shader.operations.push(Operation::LoadInt {
|
||||
dst: dst - 0x8000,
|
||||
mask,
|
||||
val,
|
||||
})
|
||||
} else {
|
||||
let val = read_float(data)?;
|
||||
shader
|
||||
.operations
|
||||
.push(Operation::LoadFloat { dst, mask, val })
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let dst = data.read_u16::<LittleEndian>()?;
|
||||
let mask = data.read_u8()?;
|
||||
let src = read_uint24(data)?;
|
||||
assert_eq!(data.read_u8()?, 0);
|
||||
shader.operations.push(Operation::Normal {
|
||||
opcode,
|
||||
dst,
|
||||
mask,
|
||||
src,
|
||||
other: 0,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_string<R: Read>(data: &mut R) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let mut string = String::new();
|
||||
let mut b = data.read_u8()?;
|
||||
while b != 0 {
|
||||
string.push(b as char);
|
||||
b = data.read_u8()?;
|
||||
}
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
fn read_float<R: Read>(data: &mut R) -> Result<f32, Box<dyn std::error::Error>> {
|
||||
Ok(data.read_f32::<BigEndian>()?)
|
||||
}
|
||||
|
||||
fn read_value<R: Read>(
|
||||
data: &mut R,
|
||||
opcode: PixelBenderTypeOpcode,
|
||||
) -> Result<PixelBenderType, Box<dyn std::error::Error>> {
|
||||
match opcode {
|
||||
PixelBenderTypeOpcode::TFloat => Ok(PixelBenderType::TFloat(read_float(data)?)),
|
||||
PixelBenderTypeOpcode::TFloat2 => Ok(PixelBenderType::TFloat2(
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TFloat3 => Ok(PixelBenderType::TFloat3(
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TFloat4 => Ok(PixelBenderType::TFloat4(
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TFloat2x2 => Ok(PixelBenderType::TFloat2x2([
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
read_float(data)?,
|
||||
])),
|
||||
PixelBenderTypeOpcode::TFloat3x3 => {
|
||||
let mut floats: [f32; 9] = [0.0; 9];
|
||||
for float in &mut floats {
|
||||
*float = read_float(data)?;
|
||||
}
|
||||
Ok(PixelBenderType::TFloat3x3(floats))
|
||||
}
|
||||
PixelBenderTypeOpcode::TFloat4x4 => {
|
||||
let mut floats: [f32; 16] = [0.0; 16];
|
||||
for float in &mut floats {
|
||||
*float = read_float(data)?;
|
||||
}
|
||||
Ok(PixelBenderType::TFloat4x4(floats))
|
||||
}
|
||||
PixelBenderTypeOpcode::TInt => Ok(PixelBenderType::TInt(data.read_i16::<LittleEndian>()?)),
|
||||
PixelBenderTypeOpcode::TInt2 => Ok(PixelBenderType::TInt2(
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TInt3 => Ok(PixelBenderType::TInt3(
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TInt4 => Ok(PixelBenderType::TInt4(
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
data.read_i16::<LittleEndian>()?,
|
||||
)),
|
||||
PixelBenderTypeOpcode::TString => Ok(PixelBenderType::TString(read_string(data)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_uint24<R: Read>(data: &mut R) -> Result<u32, Box<dyn std::error::Error>> {
|
||||
let mut src = data.read_u16::<LittleEndian>()? as u32;
|
||||
src += data.read_u8()? as u32;
|
||||
Ok(src)
|
||||
}
|
||||
|
||||
// The opcodes are laid out like this:
|
||||
//
|
||||
// ```
|
||||
// PBJMeta1 (for overall program)
|
||||
// PBJMeta1 (for overall program)
|
||||
// PBJParam (param 1)
|
||||
// ...
|
||||
// PBJMeta1 (for param 1)
|
||||
// PBJMeta1 (for param 1)
|
||||
// ...
|
||||
// PBJParam (param 2)
|
||||
// ,,,
|
||||
// PBJMeta2 (for param 2)
|
||||
// ```
|
||||
//
|
||||
// The metadata associated with parameter is determined by all of the metadata opcodes
|
||||
// that come after it and before the next parameter opcode. The metadata opcodes
|
||||
// that come before all params are associated with the overall program.
|
||||
|
||||
fn apply_metadata(shader: &mut PixelBenderShader, metadata: &mut Vec<PixelBenderMetadata>) {
|
||||
// Reset the accumulated metadata Vec - we will start accumulating metadata for the next param
|
||||
let metadata = std::mem::take(metadata);
|
||||
if shader.params.is_empty() {
|
||||
shader.metadata = metadata;
|
||||
} else {
|
||||
match shader.params.last_mut().unwrap() {
|
||||
PixelBenderParam::Normal { metadata: meta, .. } => {
|
||||
*meta = metadata;
|
||||
}
|
||||
param => {
|
||||
if !metadata.is_empty() {
|
||||
panic!("Tried to apply metadata to texture parameter {param:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
use crate::pixel_bender::{
|
||||
Opcode, Operation, PixelBenderMetadata, PixelBenderParam, PixelBenderParamQualifier,
|
||||
PixelBenderShader, PixelBenderType, PixelBenderTypeOpcode,
|
||||
};
|
||||
|
||||
use super::parse_shader;
|
||||
|
||||
#[test]
|
||||
fn simple_shader() {
|
||||
let shader = &[
|
||||
165, 1, 0, 0, 0, 164, 9, 0, 68, 111, 78, 111, 116, 104, 105, 110, 103, 160, 12, 110, 97,
|
||||
109, 101, 115, 112, 97, 99, 101, 0, 65, 100, 111, 98, 101, 58, 58, 69, 120, 97, 109, 112,
|
||||
108, 101, 0, 160, 12, 118, 101, 110, 100, 111, 114, 0, 65, 100, 111, 98, 101, 32, 101, 120,
|
||||
97, 109, 112, 108, 101, 115, 0, 160, 8, 118, 101, 114, 115, 105, 111, 110, 0, 1, 0, 160,
|
||||
12, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 65, 32, 115, 104, 97, 100,
|
||||
101, 114, 32, 116, 104, 97, 116, 32, 100, 111, 101, 115, 32, 110, 111, 116, 104, 105, 110,
|
||||
103, 44, 32, 98, 117, 116, 32, 100, 111, 101, 115, 32, 105, 116, 32, 119, 101, 108, 108,
|
||||
46, 0, 161, 1, 2, 0, 0, 12, 95, 79, 117, 116, 67, 111, 111, 114, 100, 0, 163, 0, 4, 115,
|
||||
114, 99, 0, 161, 2, 4, 1, 0, 15, 100, 115, 116, 0, 161, 1, 2, 0, 0, 3, 115, 105, 122, 101,
|
||||
0, 162, 12, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 84, 104, 101, 32, 115,
|
||||
105, 122, 101, 32, 111, 102, 32, 116, 104, 101, 32, 105, 109, 97, 103, 101, 32, 116, 111,
|
||||
32, 119, 104, 105, 99, 104, 32, 116, 104, 101, 32, 107, 101, 114, 110, 101, 108, 32, 105,
|
||||
115, 32, 97, 112, 112, 108, 105, 101, 100, 0, 162, 2, 109, 105, 110, 86, 97, 108, 117, 101,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 162, 2, 109, 97, 120, 86, 97, 108, 117, 101, 0, 66, 200, 0, 0,
|
||||
66, 200, 0, 0, 162, 2, 100, 101, 102, 97, 117, 108, 116, 86, 97, 108, 117, 101, 0, 66, 72,
|
||||
0, 0, 66, 72, 0, 0, 161, 1, 1, 2, 0, 8, 114, 97, 100, 105, 117, 115, 0, 162, 12, 100, 101,
|
||||
115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 84, 104, 101, 32, 114, 97, 100, 105, 117,
|
||||
115, 32, 111, 102, 32, 116, 104, 101, 32, 101, 102, 102, 101, 99, 116, 0, 162, 1, 109, 105,
|
||||
110, 86, 97, 108, 117, 101, 0, 0, 0, 0, 0, 162, 1, 109, 97, 120, 86, 97, 108, 117, 101, 0,
|
||||
66, 72, 0, 0, 162, 1, 100, 101, 102, 97, 117, 108, 116, 86, 97, 108, 117, 101, 0, 65, 200,
|
||||
0, 0, 4, 2, 0, 64, 2, 0, 0, 0, 3, 2, 0, 64, 2, 0, 0, 0, 4, 2, 0, 49, 0, 0, 176, 0, 3, 2, 0,
|
||||
49, 0, 0, 176, 0, 29, 3, 0, 193, 2, 0, 80, 0, 3, 3, 0, 193, 2, 0, 176, 0, 29, 2, 0, 97, 3,
|
||||
0, 16, 0, 48, 3, 0, 241, 0, 0, 16, 0, 50, 4, 0, 128, 66, 200, 0, 0, 50, 4, 0, 64, 0, 0, 0,
|
||||
0, 50, 4, 0, 32, 66, 200, 0, 0, 50, 4, 0, 16, 63, 128, 0, 0, 29, 5, 0, 243, 3, 0, 27, 0, 1,
|
||||
5, 0, 243, 4, 0, 27, 0, 29, 1, 0, 243, 5, 0, 27, 0,
|
||||
];
|
||||
|
||||
let expected = PixelBenderShader {
|
||||
name: "DoNothing".to_string(),
|
||||
version: 1,
|
||||
params: vec![
|
||||
PixelBenderParam::Normal {
|
||||
qualifier: PixelBenderParamQualifier::Input,
|
||||
param_type: PixelBenderTypeOpcode::TFloat2,
|
||||
reg: 0,
|
||||
mask: 12,
|
||||
name: "_OutCoord".to_string(),
|
||||
metadata: vec![],
|
||||
},
|
||||
PixelBenderParam::Texture {
|
||||
index: 0,
|
||||
channels: 4,
|
||||
name: "src".to_string(),
|
||||
},
|
||||
PixelBenderParam::Normal {
|
||||
qualifier: PixelBenderParamQualifier::Output,
|
||||
param_type: PixelBenderTypeOpcode::TFloat4,
|
||||
reg: 1,
|
||||
mask: 15,
|
||||
name: "dst".to_string(),
|
||||
metadata: vec![],
|
||||
},
|
||||
PixelBenderParam::Normal {
|
||||
qualifier: PixelBenderParamQualifier::Input,
|
||||
param_type: PixelBenderTypeOpcode::TFloat2,
|
||||
reg: 0,
|
||||
mask: 3,
|
||||
name: "size".to_string(),
|
||||
metadata: vec![
|
||||
PixelBenderMetadata {
|
||||
key: "description".to_string(),
|
||||
value: PixelBenderType::TString(
|
||||
"The size of the image to which the kernel is applied".to_string(),
|
||||
),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "minValue".to_string(),
|
||||
value: PixelBenderType::TFloat2(0.0, 0.0),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "maxValue".to_string(),
|
||||
value: PixelBenderType::TFloat2(100.0, 100.0),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "defaultValue".to_string(),
|
||||
value: PixelBenderType::TFloat2(50.0, 50.0),
|
||||
},
|
||||
],
|
||||
},
|
||||
PixelBenderParam::Normal {
|
||||
qualifier: PixelBenderParamQualifier::Input,
|
||||
param_type: PixelBenderTypeOpcode::TFloat,
|
||||
reg: 2,
|
||||
mask: 8,
|
||||
name: "radius".to_string(),
|
||||
metadata: vec![
|
||||
PixelBenderMetadata {
|
||||
key: "description".to_string(),
|
||||
value: PixelBenderType::TString("The radius of the effect".to_string()),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "minValue".to_string(),
|
||||
value: PixelBenderType::TFloat(0.0),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "maxValue".to_string(),
|
||||
value: PixelBenderType::TFloat(50.0),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "defaultValue".to_string(),
|
||||
value: PixelBenderType::TFloat(25.0),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
metadata: vec![
|
||||
PixelBenderMetadata {
|
||||
key: "namespace".to_string(),
|
||||
value: PixelBenderType::TString("Adobe::Example".to_string()),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "vendor".to_string(),
|
||||
value: PixelBenderType::TString("Adobe examples".to_string()),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "version".to_string(),
|
||||
value: PixelBenderType::TInt(1),
|
||||
},
|
||||
PixelBenderMetadata {
|
||||
key: "description".to_string(),
|
||||
value: PixelBenderType::TString(
|
||||
"A shader that does nothing, but does it well.".to_string(),
|
||||
),
|
||||
},
|
||||
],
|
||||
operations: vec![
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Rcp,
|
||||
dst: 2,
|
||||
mask: 64,
|
||||
src: 2,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mul,
|
||||
dst: 2,
|
||||
mask: 64,
|
||||
src: 2,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Rcp,
|
||||
dst: 2,
|
||||
mask: 49,
|
||||
src: 176,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mul,
|
||||
dst: 2,
|
||||
mask: 49,
|
||||
src: 176,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mov,
|
||||
dst: 3,
|
||||
mask: 193,
|
||||
src: 82,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mul,
|
||||
dst: 3,
|
||||
mask: 193,
|
||||
src: 178,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mov,
|
||||
dst: 2,
|
||||
mask: 97,
|
||||
src: 19,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::SampleNearest,
|
||||
dst: 3,
|
||||
mask: 241,
|
||||
src: 16,
|
||||
other: 0,
|
||||
},
|
||||
Operation::LoadFloat {
|
||||
dst: 4,
|
||||
mask: 128,
|
||||
val: 100.0,
|
||||
},
|
||||
Operation::LoadFloat {
|
||||
dst: 4,
|
||||
mask: 64,
|
||||
val: 0.0,
|
||||
},
|
||||
Operation::LoadFloat {
|
||||
dst: 4,
|
||||
mask: 32,
|
||||
val: 100.0,
|
||||
},
|
||||
Operation::LoadFloat {
|
||||
dst: 4,
|
||||
mask: 16,
|
||||
val: 1.0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mov,
|
||||
dst: 5,
|
||||
mask: 243,
|
||||
src: 30,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Add,
|
||||
dst: 5,
|
||||
mask: 243,
|
||||
src: 31,
|
||||
other: 0,
|
||||
},
|
||||
Operation::Normal {
|
||||
opcode: Opcode::Mov,
|
||||
dst: 1,
|
||||
mask: 243,
|
||||
src: 32,
|
||||
other: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let shader = parse_shader(shader);
|
||||
assert_eq!(shader, expected, "Shader parsed incorrectly!");
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package {
|
||||
import flash.display.ShaderData;
|
||||
import flash.utils.ByteArray;
|
||||
import flash.display.Shader;
|
||||
import flash.utils.getQualifiedClassName;
|
||||
|
||||
public class Test {
|
||||
|
||||
[Embed(source = "shader.pbj", mimeType="application/octet-stream")]
|
||||
public static var SHADER_BYTES: Class;
|
||||
|
||||
public function Test() {
|
||||
var shader: ByteArray = new SHADER_BYTES();
|
||||
var data = new ShaderData(shader);
|
||||
trace(data);
|
||||
dumpObject(data);
|
||||
}
|
||||
|
||||
private function dumpObject(obj: Object, prefix: String = "") {
|
||||
var keys = [];
|
||||
for (var k in obj) {
|
||||
keys.push(k)
|
||||
}
|
||||
keys.sort();
|
||||
for each (var key in keys) {
|
||||
trace(prefix + key + ": " + obj[key] + " (" + getQualifiedClassName(obj[key])+ ")");
|
||||
dumpObject(obj[key], prefix + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
[object ShaderData]
|
||||
description: A shader that does nothing, but does it well. (String)
|
||||
name: DoNothing (String)
|
||||
namespace: Adobe::Example (String)
|
||||
radius: [object ShaderParameter] (flash.display::ShaderParameter)
|
||||
defaultValue: 25 (Array)
|
||||
0: 25 (int)
|
||||
description: The radius of the effect (String)
|
||||
maxValue: 50 (Array)
|
||||
0: 50 (int)
|
||||
minValue: 0 (Array)
|
||||
0: 0 (int)
|
||||
name: radius (String)
|
||||
size: [object ShaderParameter] (flash.display::ShaderParameter)
|
||||
defaultValue: 50,50 (Array)
|
||||
0: 50 (int)
|
||||
1: 50 (int)
|
||||
description: The size of the image to which the kernel is applied (String)
|
||||
maxValue: 100,100.5 (Array)
|
||||
0: 100 (int)
|
||||
1: 100.5 (Number)
|
||||
minValue: 0,0 (Array)
|
||||
0: 0 (int)
|
||||
1: 0 (int)
|
||||
name: size (String)
|
||||
src: [object ShaderInput] (flash.display::ShaderInput)
|
||||
name: src (String)
|
||||
vendor: Adobe examples (String)
|
||||
version: 1 (int)
|
Binary file not shown.
|
@ -0,0 +1,38 @@
|
|||
<languageVersion : 1.0;>
|
||||
|
||||
kernel DoNothing
|
||||
<
|
||||
namespace: "Adobe::Example";
|
||||
vendor: "Adobe examples";
|
||||
version: 1;
|
||||
description: "A shader that does nothing, but does it well.";
|
||||
>
|
||||
{
|
||||
|
||||
input image4 src;
|
||||
|
||||
|
||||
output pixel4 dst;
|
||||
|
||||
parameter float2 size
|
||||
<
|
||||
description: "The size of the image to which the kernel is applied";
|
||||
minValue: float2(0.0, 0.0);
|
||||
maxValue: float2(100.0, 100.5);
|
||||
defaultValue: float2(50.0, 50.0);
|
||||
>;
|
||||
|
||||
parameter float radius
|
||||
<
|
||||
description: "The radius of the effect";
|
||||
minValue: 0.0;
|
||||
maxValue: 50.0;
|
||||
defaultValue: 25.0;
|
||||
>;
|
||||
|
||||
void evaluatePixel()
|
||||
{
|
||||
float2 one = (radius / radius) * (size / size);
|
||||
dst = sampleNearest(src, outCoord()) + float4(100.0, 0.0, 100.0, 1.0);
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
num_frames = 1
|
Loading…
Reference in New Issue