552 lines
18 KiB
Haxe
552 lines
18 KiB
Haxe
package iron.object;
|
|
|
|
#if lnx_cpu_particles
|
|
import iron.Scene;
|
|
import iron.data.Data;
|
|
import iron.data.ParticleData;
|
|
import iron.data.SceneFormat;
|
|
import iron.math.Quat;
|
|
import iron.math.Vec3;
|
|
import iron.math.Vec4;
|
|
import iron.object.MeshObject;
|
|
import iron.object.Object;
|
|
import iron.system.Time;
|
|
import iron.system.Tween;
|
|
import kha.FastFloat;
|
|
import kha.arrays.Int16Array;
|
|
import kha.arrays.Uint32Array;
|
|
|
|
class ParticleSystemCPU {
|
|
public var data: ParticleData;
|
|
public var speed: FastFloat = 1.0; // Not used yet. Added to go in hand with `ParticleSystemGPU`
|
|
var r: TParticleData;
|
|
|
|
// Format
|
|
final baseFrameRate: FastFloat = 24.0;
|
|
var frameRate: FastFloat = 24.0;
|
|
var type: Int = 0; // type: 0 - Emission, 1 - Hair
|
|
|
|
// Emission
|
|
var count: Int = 10; // count
|
|
var frameStart: FastFloat = 1; // frame_start
|
|
var frameEnd: FastFloat = 10.0; // frame_end
|
|
var lifetime: FastFloat = 24.0; // lifetime
|
|
var lifetimeRandom: FastFloat = 0.0; // lifetime_random
|
|
var emitFrom: Int = 1; // emit_from: 0 - Vert, 1 - Face, 2 - Volume // TODO: fully integrate Blender's properties
|
|
|
|
// Velocity
|
|
var velocity: Vec3 = new Vec3(0.0, 0.0, 1.0); // object_align_factor: Float32Array
|
|
var velocityRandom: FastFloat = 0.0; // factor_random
|
|
|
|
// Rotation
|
|
var rotation: Bool = false; // use_rotations
|
|
var orientationAxis: Int = 0; // rotation_mode: 0 - None, 1 - Normal, 2 - Normal-Tangent, 3 - Velocity/Hair, 4 - Global X, 5 - Global Y, 6 - Global Z, 7 - Object X, 8 - Object Y, 9 - Object Z
|
|
var rotationRandom: FastFloat = 0.0; // rotation_factor_random
|
|
var phase: FastFloat = 0.0; // phase_factor
|
|
var phaseRandom: FastFloat = 0.0; // phase_factor_random
|
|
var dynamicRotation: Bool = false; // use_dynamic_rotation
|
|
|
|
// Render
|
|
var instanceObject: String; // instance_object
|
|
var scale: FastFloat = 1.0; // particle_size
|
|
var scaleRandom: FastFloat = 0.0; // size_random
|
|
|
|
// Field weights
|
|
var gravity: Vec3 = new Vec3(0, 0, -9.8);
|
|
var gravityFactor: FastFloat = 1.0; // weight_gravity
|
|
var textureFactor: FastFloat = 1.0; // weight_texture
|
|
|
|
// Textures
|
|
var textureSlots: Map<String, Dynamic> = [];
|
|
|
|
// Lnx props
|
|
var autoStart: Bool = true; // auto_start
|
|
var localCoords: Bool = false; // local_coords
|
|
var loop: Bool = false; // loop
|
|
|
|
// Internal logic
|
|
var owner: MeshObject;
|
|
var lifetimeSeconds: FastFloat = 0.0;
|
|
var spawnRate: FastFloat = 0.0;
|
|
var spawnFactor: Int = 1;
|
|
var spawnedParticles: Int = 0;
|
|
var particleScale: FastFloat = 1.0;
|
|
var loopAnim: TAnim;
|
|
var spawnTime: FastFloat = 0;
|
|
|
|
var randQuat: Quat;
|
|
var phaseQuat: Quat;
|
|
|
|
// Tween scaling
|
|
var scaleElementsCount: Int = 0;
|
|
var scaleRampSizeFactor: FastFloat = 0;
|
|
var rampPositions: Array<FastFloat> = [];
|
|
var rampColors: Array<FastFloat> = [];
|
|
|
|
// Optimization
|
|
var particlePool: Array<Object> = [];
|
|
var particlePhysics: Map<Object, TParticlePhysics> = [];
|
|
|
|
public function new(sceneName: String, pref: TParticleReference, mo: MeshObject) {
|
|
Data.getParticle(sceneName, pref.particle, function (b: ParticleData) {
|
|
data = b;
|
|
r = data.raw;
|
|
owner = mo;
|
|
|
|
frameRate = r.fps;
|
|
type = r.type;
|
|
count = r.count;
|
|
frameStart = r.frame_start;
|
|
frameEnd = r.frame_end;
|
|
lifetime = r.lifetime;
|
|
lifetimeRandom = r.lifetime_random;
|
|
emitFrom = r.emit_from;
|
|
|
|
rotation = r.use_rotations;
|
|
orientationAxis = r.rotation_mode;
|
|
rotationRandom = r.rotation_factor_random;
|
|
phase = r.phase_factor;
|
|
phaseRandom = r.phase_factor_random;
|
|
dynamicRotation = r.use_dynamic_rotation;
|
|
|
|
instanceObject = r.instance_object;
|
|
|
|
scale = r.particle_size;
|
|
scaleRandom = r.size_random;
|
|
|
|
velocity = new Vec3(r.object_align_factor[0], r.object_align_factor[1], r.object_align_factor[2]).mult(frameRate / baseFrameRate).mult(1 / scale);
|
|
velocityRandom = r.factor_random * (frameRate / baseFrameRate);
|
|
|
|
if (Scene.active.raw.gravity != null) {
|
|
gravity = new Vec3(Scene.active.raw.gravity[0], Scene.active.raw.gravity[1], Scene.active.raw.gravity[2]).mult(frameRate / baseFrameRate).mult(1 / scale);
|
|
}
|
|
gravityFactor = r.weight_gravity * (frameRate / baseFrameRate);
|
|
textureFactor = r.weight_texture;
|
|
|
|
if (r.texture_slots != null) {
|
|
for (slot in Reflect.fields(r.texture_slots)) {
|
|
textureSlots[slot] = Reflect.field(r.texture_slots, slot);
|
|
}
|
|
}
|
|
|
|
autoStart = r.auto_start;
|
|
localCoords = r.local_coords;
|
|
loop = r.loop;
|
|
|
|
spawnRate = ((frameEnd - frameStart) / count) / frameRate;
|
|
lifetimeSeconds = lifetime / frameRate;
|
|
|
|
scaleElementsCount = getRampElementsLength();
|
|
scaleRampSizeFactor = getRampSizeFactor();
|
|
|
|
Scene.active.notifyOnInit(function () {
|
|
var i: Int;
|
|
for (i in 0...count) addToPool();
|
|
});
|
|
|
|
switch (type) {
|
|
case 0: // Emission
|
|
loopAnim = {
|
|
tick: function () {
|
|
spawnTime += Time.delta * Time.scale;
|
|
var expected: Int = Math.floor(spawnTime / spawnRate);
|
|
while (spawnedParticles < expected && spawnedParticles < count) {
|
|
spawnParticle();
|
|
spawnedParticles++;
|
|
}
|
|
updateParticles();
|
|
},
|
|
target: null,
|
|
props: null,
|
|
duration: loop ? lifetimeSeconds : lifetimeSeconds * 2,
|
|
done: function () {
|
|
if (loop) start();
|
|
}
|
|
}
|
|
case 1: // Hair
|
|
Scene.active.notifyOnInit(function () {
|
|
var i: Int;
|
|
for (i in 0...count) spawnParticle();
|
|
});
|
|
default:
|
|
}
|
|
|
|
Scene.active.notifyOnInit(function () {
|
|
if (autoStart) start();
|
|
});
|
|
});
|
|
}
|
|
|
|
public function start() {
|
|
if (type != 0) return;
|
|
spawnTime = 0;
|
|
spawnedParticles = 0;
|
|
Tween.to(loopAnim);
|
|
}
|
|
|
|
// TODO
|
|
public function pause() {
|
|
|
|
}
|
|
|
|
// TODO
|
|
public function resume() {
|
|
|
|
}
|
|
|
|
public function stop() {
|
|
if (type != 0) return;
|
|
spawnTime = 0;
|
|
spawnedParticles = 0;
|
|
Tween.stop(loopAnim);
|
|
|
|
for (particle => physics in particlePhysics) releaseParticle(particle);
|
|
particlePhysics.clear();
|
|
}
|
|
|
|
function addToPool() {
|
|
Scene.active.spawnObject(instanceObject, localCoords ? owner : null, function (o: Object) {
|
|
o.visible = false;
|
|
particlePool.push(o);
|
|
});
|
|
}
|
|
|
|
function getFreeParticle(): Object {
|
|
for (particle in particlePool) {
|
|
if (!particle.visible) {
|
|
particle.visible = true;
|
|
return particle;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function releaseParticle(o: Object) {
|
|
o.visible = false;
|
|
o.transform.loc = new Vec4();
|
|
o.transform.rot = new Quat();
|
|
o.transform.scale = new Vec4(1, 1, 1, 1);
|
|
}
|
|
|
|
function spawnParticle() {
|
|
var o: Object = getFreeParticle();
|
|
if (o == null) {
|
|
addToPool();
|
|
o = getFreeParticle();
|
|
}
|
|
|
|
owner.transform.buildMatrix();
|
|
|
|
var objectPos: Vec4 = new Vec4();
|
|
var objectRot: Quat = new Quat();
|
|
var objectScale: Vec4 = new Vec4();
|
|
owner.transform.world.decompose(objectPos, objectRot, objectScale);
|
|
|
|
o.visible = true;
|
|
|
|
var normFactor: FastFloat = 1 / 32767;
|
|
var scalePos: FastFloat = owner.data.scalePos;
|
|
var scalePosParticle: FastFloat = cast(o, MeshObject).data.scalePos;
|
|
|
|
// TODO: add all properties from Blender's UI
|
|
switch (emitFrom) {
|
|
case 0: // Vertices
|
|
var pa: TVertexArray = owner.data.geom.positions;
|
|
var i: Int = Std.int(Math.random() * (pa.values.length / pa.size));
|
|
var loc: Vec4 = new Vec4(pa.values[i * pa.size] * normFactor, pa.values[i * pa.size + 1] * normFactor, pa.values[i * pa.size + 2] * normFactor, 1);
|
|
|
|
if (!localCoords) {
|
|
loc.applyQuat(objectRot);
|
|
loc.add(objectPos);
|
|
}
|
|
o.transform.loc.setFrom(loc);
|
|
case 1: // Faces
|
|
var positions: Int16Array = owner.data.geom.positions.values;
|
|
var ia: Uint32Array = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)];
|
|
var faceIndex: Int = Std.random(Std.int(ia.length / 3));
|
|
|
|
var i0 = ia[faceIndex * 3 + 0];
|
|
var i1 = ia[faceIndex * 3 + 1];
|
|
var i2 = ia[faceIndex * 3 + 2];
|
|
|
|
var v0: Vec3 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]);
|
|
var v1: Vec3 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]);
|
|
var v2: Vec3 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]);
|
|
|
|
var pos: Vec3 = randomPointInTriangle(v0, v1, v2);
|
|
|
|
var loc: Vec4 = new Vec4(pos.x, pos.y, pos.z, 1).mult(normFactor);
|
|
if (!localCoords) {
|
|
loc.applyQuat(objectRot);
|
|
loc.add(objectPos);
|
|
}
|
|
|
|
o.transform.loc.setFrom(loc);
|
|
case 2: // Volume
|
|
var scaleFactorVolume: Vec4 = new Vec4().setFrom(owner.transform.dim);
|
|
scaleFactorVolume.mult(0.5);
|
|
var loc: Vec4 = new Vec4((Math.random() * 2.0 - 1.0) * scaleFactorVolume.x, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z, 1);
|
|
|
|
if (!localCoords) {
|
|
loc.applyQuat(objectRot);
|
|
loc.add(objectPos);
|
|
}
|
|
o.transform.loc.setFrom(loc);
|
|
}
|
|
|
|
particleScale = 1 - scaleRandom * Math.random();
|
|
var localFactor: Vec3 = localCoords ? new Vec3(objectScale.x, objectScale.y, objectScale.z) : new Vec3(1, 1, 1);
|
|
var sc: Vec4 = new Vec4(o.transform.scale.x / localFactor.x, o.transform.scale.y / localFactor.y, o.transform.scale.z / localFactor.z, 1.0).mult(scale).mult(particleScale);
|
|
var randomLifetime: FastFloat = lifetimeSeconds * (1 - Math.random() * lifetimeRandom);
|
|
|
|
if (scaleElementsCount != 0) {
|
|
rampPositions = getRampPositions();
|
|
rampColors = getRampColors();
|
|
} else {
|
|
o.transform.scale.setFrom(sc);
|
|
}
|
|
o.transform.buildMatrix();
|
|
|
|
var randomX: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom;
|
|
var randomY: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom;
|
|
var randomZ: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom;
|
|
var g: Vec3 = new Vec3();
|
|
|
|
var rotatedVelocity: Vec4 = new Vec4(velocity.x + randomX, velocity.y + randomY, velocity.z + randomZ, 1);
|
|
if (!localCoords) rotatedVelocity.applyQuat(objectRot);
|
|
|
|
if (rotation) {
|
|
// Rotation phase and randomness. Wrap values between -1 and 1.
|
|
randQuat = new Quat().fromEuler((Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom);
|
|
var phaseRand: FastFloat = (Math.random() * 2 - 1) * phaseRandom;
|
|
var phaseValue: FastFloat = phase + phaseRand;
|
|
while (phaseValue > 1) phaseValue -= 2;
|
|
while (phaseValue < -1) phaseValue += 2;
|
|
var dirQuat: Quat = new Quat();
|
|
phaseQuat = new Quat().fromEuler(0, phaseValue * Math.PI, 0);
|
|
|
|
switch (orientationAxis) {
|
|
case 0: // None
|
|
o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5);
|
|
case 1: // Normal
|
|
case 2: // Normal-Tangent
|
|
case 3: // Velocity/Hair
|
|
setVelocityHair(o, rotatedVelocity, randQuat, phaseQuat);
|
|
case 4: // Global X
|
|
o.transform.rot.fromEuler(0, 0, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat);
|
|
case 5: // Global Y
|
|
o.transform.rot.fromEuler(0, 0, 0).mult(phaseQuat).mult(randQuat);
|
|
case 6: // Global Z
|
|
o.transform.rot.fromEuler(0, -Math.PI * 0.5, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat);
|
|
case 7: // Object X
|
|
o.transform.rot.setFrom(objectRot);
|
|
dirQuat.fromEuler(0, 0, -Math.PI * 0.5);
|
|
o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat);
|
|
case 8: // Object Y
|
|
o.transform.rot.setFrom(objectRot);
|
|
o.transform.rot.mult(phaseQuat).mult(randQuat);
|
|
case 9: // Object Z
|
|
o.transform.rot.setFrom(objectRot);
|
|
dirQuat.fromEuler(0, -Math.PI * 0.5, 0).mult(new Quat().fromEuler(0, 0, -Math.PI * 0.5));
|
|
o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat);
|
|
default:
|
|
}
|
|
} else {
|
|
o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5);
|
|
}
|
|
|
|
var physics: TParticlePhysics = {
|
|
velocity: rotatedVelocity.clone(),
|
|
gravity: gravity.clone().mult(gravityFactor),
|
|
lifetime: randomLifetime,
|
|
age: 0.0,
|
|
hasScaleRamp: scaleElementsCount != 0,
|
|
baseScale: sc.clone(),
|
|
rampPositions: rampPositions.copy(),
|
|
rampColors: rampColors.copy(),
|
|
scaleRampSizeFactor: scaleRampSizeFactor
|
|
};
|
|
|
|
particlePhysics.set(o, physics);
|
|
o.transform.buildMatrix();
|
|
}
|
|
}
|
|
|
|
function setVelocityHair(object: Object, velocity: Vec4, randQuat: Quat, phaseQuat: Quat) {
|
|
var dir: Vec4 = velocity.clone().normalize();
|
|
var yaw: FastFloat = Math.atan2(-dir.x, dir.y);
|
|
var pitch: FastFloat = Math.asin(dir.z);
|
|
var targetRot: Quat = new Quat().fromEuler(pitch, 0, yaw);
|
|
|
|
targetRot.mult(randQuat);
|
|
object.transform.rot.setFrom(targetRot.mult(phaseQuat));
|
|
}
|
|
|
|
function updateParticles() {
|
|
for (particle => physics in particlePhysics) {
|
|
physics.age += Time.delta * Time.scale;
|
|
|
|
if (physics.age >= physics.lifetime) {
|
|
particlePhysics.remove(particle);
|
|
releaseParticle(particle);
|
|
continue;
|
|
}
|
|
|
|
physics.velocity.x += physics.gravity.x * Time.delta * Time.scale;
|
|
physics.velocity.y += physics.gravity.y * Time.delta * Time.scale;
|
|
physics.velocity.z += physics.gravity.z * Time.delta * Time.scale;
|
|
|
|
particle.transform.translate(
|
|
physics.velocity.x * Time.delta * Time.scale,
|
|
physics.velocity.y * Time.delta * Time.scale,
|
|
physics.velocity.z * Time.delta * Time.scale
|
|
);
|
|
|
|
if (rotation && dynamicRotation && orientationAxis == 3) setVelocityHair(particle, physics.velocity, randQuat, phaseQuat);
|
|
|
|
if (physics.hasScaleRamp && physics.rampPositions.length > 1) {
|
|
var normalizedAge: FastFloat = physics.age / physics.lifetime;
|
|
var scaleMultiplier: FastFloat = interpolateRampValue(normalizedAge, physics.rampPositions, physics.rampColors);
|
|
var finalScale: FastFloat = scale * (particleScale * (1 - physics.scaleRampSizeFactor) + scaleMultiplier * physics.scaleRampSizeFactor);
|
|
particle.transform.scale.setFrom(physics.baseScale.clone().mult(finalScale));
|
|
}
|
|
|
|
particle.transform.buildMatrix();
|
|
}
|
|
}
|
|
|
|
// Linear interpolation
|
|
function interpolateRampValue(normalizedAge: FastFloat, positions: Array<FastFloat>, colors: Array<FastFloat>): FastFloat {
|
|
if (positions.length == 0) return 1.0;
|
|
if (normalizedAge <= positions[0]) return colors[0];
|
|
if (normalizedAge >= positions[positions.length - 1]) return colors[colors.length - 1];
|
|
|
|
var i: Int;
|
|
for (i in 0...(positions.length - 1)) {
|
|
if (normalizedAge >= positions[i] && normalizedAge <= positions[i + 1]) {
|
|
var t: FastFloat = (normalizedAge - positions[i]) / (positions[i + 1] - positions[i]);
|
|
return colors[i] + t * (colors[i + 1] - colors[i]);
|
|
}
|
|
}
|
|
|
|
return colors[colors.length - 1];
|
|
}
|
|
|
|
function getRampSizeFactor(): FastFloat {
|
|
// Just using the first slot for now: 1 texture slot
|
|
// TODO: use all available slots ?
|
|
for (slot in textureSlots.keys()) {
|
|
var s: Dynamic = textureSlots[slot];
|
|
if (s != null && s.use_map_size) {
|
|
var sizeFactor: FastFloat = s.size_factor;
|
|
return sizeFactor * textureFactor;
|
|
}
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
function getRampElementsLength(): Int {
|
|
for (slot in textureSlots.keys()) {
|
|
var s: Dynamic = textureSlots[slot];
|
|
if (s == null) continue;
|
|
var tex: Dynamic = s.texture;
|
|
if (tex == null) continue;
|
|
if (tex.use_color_ramp) {
|
|
var ramp: Dynamic = tex.color_ramp;
|
|
if (ramp == null) continue;
|
|
var elems: Dynamic = ramp.elements;
|
|
if (elems == null) continue;
|
|
return elems.length;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getRampPositions(): Array<FastFloat> {
|
|
// Just using the first slot for now: 1 texture slot
|
|
// TODO: use all available slots ?
|
|
for (slot in textureSlots.keys()) {
|
|
var s: Dynamic = textureSlots[slot];
|
|
if (s == null) continue;
|
|
var tex: Dynamic = s.texture;
|
|
if (tex == null) continue;
|
|
if (tex.use_color_ramp) {
|
|
var ramp: Dynamic = tex.color_ramp;
|
|
if (ramp == null) continue;
|
|
var elems: Dynamic = ramp.elements;
|
|
if (elems == null) continue;
|
|
var positions: Array<FastFloat> = [];
|
|
for (i in 0...elems.length) {
|
|
positions.push(elems[i].position);
|
|
}
|
|
return positions;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function getRampColors(): Array<FastFloat> {
|
|
// Just using the first slot for now: 1 texture slot
|
|
// TODO: use all available slots ?
|
|
for (slot in textureSlots.keys()) {
|
|
var s: Dynamic = textureSlots[slot];
|
|
if (s == null) continue;
|
|
var tex: Dynamic = s.texture;
|
|
if (tex == null) continue;
|
|
if (tex.use_color_ramp) {
|
|
var ramp: Dynamic = tex.color_ramp;
|
|
if (ramp == null) continue;
|
|
var elems: Dynamic = ramp.elements;
|
|
if (elems == null) continue;
|
|
var colors: Array<FastFloat> = [];
|
|
for (i in 0...elems.length) {
|
|
colors.push(elems[i].color.b); // Just need R, G or B for black and white images. Using B as it can be interpreted as V with HSV
|
|
}
|
|
return colors;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
public function remove() {
|
|
for (particle in particlePool) particle.remove();
|
|
}
|
|
|
|
/**
|
|
Generates a random point in the triangle with vertex positions abc.
|
|
|
|
Please note that the given position vectors are changed in-place by this
|
|
function and can be considered garbage afterwards, so make sure to clone
|
|
them first if needed.
|
|
**/
|
|
public static inline function randomPointInTriangle(a: Vec3, b: Vec3, c: Vec3): Vec3 {
|
|
// Generate a random point in a square where (0, 0) <= (x, y) < (1, 1)
|
|
var x = Math.random();
|
|
var y = Math.random();
|
|
|
|
if (x + y > 1) {
|
|
// We're in the upper right triangle in the square, mirror to lower left
|
|
x = 1 - x;
|
|
y = 1 - y;
|
|
}
|
|
|
|
// Transform the point to the triangle abc
|
|
var u = b.sub(a);
|
|
var v = c.sub(a);
|
|
return a.add(u.mult(x).add(v.mult(y)));
|
|
}
|
|
|
|
typedef TParticlePhysics = {
|
|
var velocity: Vec4;
|
|
var gravity: Vec3;
|
|
var lifetime: Float;
|
|
var age: Float;
|
|
var hasScaleRamp: Bool;
|
|
var baseScale: Vec4;
|
|
var rampPositions: Array<FastFloat>;
|
|
var rampColors: Array<FastFloat>;
|
|
var scaleRampSizeFactor: FastFloat;
|
|
}
|
|
}
|
|
#end
|