Skip to content

avm2: Store PerspectiveProjection to DisplayObject #19670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/avm1/globals/bitmap_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ fn draw<'gc>(
Transform {
matrix,
color_transform,
perspective_projection: None,
},
smoothing,
blend_mode,
Expand Down
30 changes: 5 additions & 25 deletions core/src/avm2/globals/flash/geom/PerspectiveProjection.as
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package flash.geom {
import __ruffle__.stub_getter;
import __ruffle__.stub_setter;
import flash.display.DisplayObject;
import flash.geom.Matrix3D;
import flash.geom.Point;
Expand All @@ -18,35 +16,17 @@ package flash.geom {
public function PerspectiveProjection() {
}

public function get fieldOfView():Number {
stub_getter("flash.geom.PerspectiveProjection", "fieldOfView");
return this.fov;
}

public function set fieldOfView(value:Number) {
// TODO: This setter should update the associated displayObject when there is.
stub_setter("flash.geom.PerspectiveProjection", "fieldOfView");
public native function get fieldOfView():Number;

if (value <= 0 || 180 <= value) {
throw new ArgumentError("Error #2182: Invalid fieldOfView value. The value must be greater than 0 and less than 180.", 2182);
}

this.fov = value;
}
public native function set fieldOfView(value:Number);

public native function get focalLength():Number;

public native function set focalLength(value:Number);

public function get projectionCenter():Point {
stub_getter("flash.geom.PerspectiveProjection", "projectionCenter");
return this.center;
}
public function set projectionCenter(value:Point) {
// TODO: This setter should update the associated displayObject when there is.
stub_setter("flash.geom.PerspectiveProjection", "projectionCenter");
this.center = value;
}
public native function get projectionCenter():Point;

public native function set projectionCenter(value:Point);

public function toMatrix3D():Matrix3D {
var fl: Number = this.focalLength;
Expand Down
168 changes: 158 additions & 10 deletions core/src/avm2/globals/flash/geom/perspective_projection.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::f64::consts::PI;

use crate::avm2::error::argument_error;
use crate::avm2::globals::flash::geom::transform::object_to_perspective_projection;
use crate::avm2::globals::slots::flash_geom_perspective_projection as pp_slots;
use crate::avm2::globals::slots::flash_geom_point as point_slots;
use crate::avm2::{Activation, Error, Object, TObject, Value};
use crate::avm2_stub_setter;
use crate::display_object::TDisplayObject;
use crate::{avm2_stub_getter, avm2_stub_setter};

const DEG2RAD: f64 = PI / 180.0;

Expand All @@ -29,15 +31,9 @@ pub fn get_focal_length<'gc>(
this: Value<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_getter!(
activation,
"flash.geom.PerspectiveProjection",
"focalLength"
);

let this = this.as_object().unwrap();

let fov = this.get_slot(pp_slots::FOV).coerce_to_number(activation)?;
let fov = object_to_perspective_projection(this, activation)?.field_of_view;

let width = get_width(activation, this);
let focal_length = (width / 2.0) as f32 * f64::tan((PI - fov * DEG2RAD) / 2.0) as f32;
Expand All @@ -50,7 +46,7 @@ pub fn set_focal_length<'gc>(
this: Value<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// TODO: This setter should update the associated displayObject when there is.
// FIXME: Render with the given PerspectiveProjection.
avm2_stub_setter!(
activation,
"flash.geom.PerspectiveProjection",
Expand All @@ -59,7 +55,6 @@ pub fn set_focal_length<'gc>(
let this = this.as_object().unwrap();

let focal_length = args.get(0).unwrap().coerce_to_number(activation)?;

if focal_length <= 0.0 {
return Err(Error::AvmError(argument_error(
activation,
Expand All @@ -68,10 +63,163 @@ pub fn set_focal_length<'gc>(
)?));
}

sync_from_display_object(activation, this)?;

let width = get_width(activation, this);
let fov = f64::atan((width / 2.0) / focal_length) / DEG2RAD * 2.0;
this.set_slot(pp_slots::FOV, fov.into(), activation)?;

sync_to_display_object(activation, this)?;

Ok(Value::Undefined)
}

pub fn get_field_of_view<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Value<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_object().unwrap();

let perspective_projection = object_to_perspective_projection(this, activation)?;

Ok(perspective_projection.field_of_view.into())
}

pub fn set_field_of_view<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Value<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// FIXME: Render with the given PerspectiveProjection.
avm2_stub_setter!(
activation,
"flash.geom.PerspectiveProjection",
"fieldOfView"
);

let this = this.as_object().unwrap();

let fov = args.get(0).unwrap().coerce_to_number(activation)?;
if fov <= 0.0 || 180.0 <= fov {
return Err(Error::AvmError(argument_error(
activation,
"Error #2182: Invalid fieldOfView value. The value must be greater than 0 and less than 180.",
2182,
)?));
}

sync_from_display_object(activation, this)?;

this.set_slot(pp_slots::FOV, fov.into(), activation)?;

sync_to_display_object(activation, this)?;

Ok(Value::Undefined)
}

pub fn get_projection_center<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Value<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_object().unwrap();

let perspective_projection = object_to_perspective_projection(this, activation)?;
let (x, y) = perspective_projection.center;

activation
.avm2()
.classes()
.point
.construct(activation, &[x.into(), y.into()])
}

pub fn set_projection_center<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Value<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// FIXME: Render with the given PerspectiveProjection.
avm2_stub_setter!(
activation,
"flash.geom.PerspectiveProjection",
"projectionCenter"
);

let this = this.as_object().unwrap();

sync_from_display_object(activation, this)?;

let point = args.get(0).unwrap();
this.set_slot(pp_slots::CENTER, *point, activation)?;

sync_to_display_object(activation, this)?;

Ok(Value::Undefined)
}

fn sync_from_display_object<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
) -> Result<(), Error<'gc>> {
let Some(dobj) = this.get_slot(pp_slots::DISPLAY_OBJECT).as_object() else {
// Not associated with DO. Unnecessary to sync.
return Ok(());
};
let dobj = dobj.as_display_object().unwrap();
let dobj = dobj.base();

let Some(perspective_projection) = dobj.perspective_projection() else {
return Ok(());
};

this.set_slot(
pp_slots::FOV,
perspective_projection.field_of_view.into(),
activation,
)?;

this.set_slot(
pp_slots::CENTER,
activation.avm2().classes().point.construct(
activation,
&[
perspective_projection.center.0.into(),
perspective_projection.center.1.into(),
],
)?,
activation,
)?;

Ok(())
}

fn sync_to_display_object<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
) -> Result<(), Error<'gc>> {
let Some(dobj) = this.get_slot(pp_slots::DISPLAY_OBJECT).as_object() else {
// Not associated with DO. Unnecessary to sync.
return Ok(());
};
let dobj = dobj.as_display_object().unwrap();

let mut write = dobj.base_mut(activation.gc());
let Some(write) = write.perspective_projection_mut() else {
return Ok(());
};

let fov = this.get_slot(pp_slots::FOV).as_f64();
let (x, y) = {
let center = this.get_slot(pp_slots::CENTER).as_object().unwrap();
let x = center.get_slot(point_slots::X).as_f64();
let y = center.get_slot(point_slots::Y).as_f64();
(x, y)
};

write.field_of_view = fov;
write.center = (x, y);

Ok(())
}
83 changes: 46 additions & 37 deletions core/src/avm2/globals/flash/geom/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::avm2::globals::slots::flash_geom_color_transform as ct_slots;
use crate::avm2::globals::slots::flash_geom_matrix as matrix_slots;
use crate::avm2::globals::slots::flash_geom_matrix_3d as matrix3d_slots;
use crate::avm2::globals::slots::flash_geom_perspective_projection as pp_slots;
use crate::avm2::globals::slots::flash_geom_point as point_slots;
use crate::avm2::globals::slots::flash_geom_transform as transform_slots;
use crate::avm2::object::VectorObject;
use crate::avm2::parameters::ParametersExt;
Expand All @@ -11,6 +12,7 @@ use crate::display_object::TDisplayObject;
use crate::prelude::{DisplayObject, Matrix, Twips};
use crate::{avm2_stub_getter, avm2_stub_setter};
use ruffle_render::matrix3d::Matrix3D;
use ruffle_render::perspective_projection::PerspectiveProjection;
use ruffle_render::quality::StageQuality;
use swf::{ColorTransform, Fixed8, Rectangle};

Expand Down Expand Up @@ -248,6 +250,32 @@ fn object_to_matrix3d<'gc>(
Ok(Matrix3D { raw_data })
}

pub fn object_to_perspective_projection<'gc>(
object: Object<'gc>,
_activation: &mut Activation<'_, 'gc>,
) -> Result<PerspectiveProjection, Error<'gc>> {
if let Some(display_object) = object.get_slot(pp_slots::DISPLAY_OBJECT).as_object() {
return Ok(display_object
.as_display_object()
.unwrap()
.base()
.perspective_projection()
.copied()
.unwrap_or_default());
}

let fov = object.get_slot(pp_slots::FOV).as_f64();

let center = object.get_slot(pp_slots::CENTER).as_object().unwrap();
let x = center.get_slot(point_slots::X).as_f64();
let y = center.get_slot(point_slots::Y).as_f64();

Ok(PerspectiveProjection {
field_of_view: fov,
center: (x, y),
})
}

pub fn matrix_to_object<'gc>(
matrix: Matrix,
activation: &mut Activation<'_, 'gc>,
Expand Down Expand Up @@ -389,44 +417,25 @@ pub fn get_perspective_projection<'gc>(
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_object().unwrap();

avm2_stub_getter!(activation, "flash.geom.Transform", "perspectiveProjection");

let display_object = get_display_object(this);
let has_perspective_projection = if display_object.is_root() {
true
} else {
display_object.base().has_perspective_projection_stub()
};

if has_perspective_projection {
let result = activation
if get_display_object(this)
.base()
.perspective_projection()
.is_some()
{
let object = activation
.avm2()
.classes()
.perspectiveprojection
.construct(activation, &[])?;
.construct(activation, &[])?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, does this really return a new PerspectiveProjection each time the getter is called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, to make it clear, I added a test case testEq() to geom_transform just now.
From the same Transform, every call to the getter creates a new PerspectiveProjection.

.as_object()
.unwrap();

let object = result.as_object().unwrap();
object.set_slot(
pp_slots::DISPLAY_OBJECT,
this.get_slot(transform_slots::DISPLAY_OBJECT),
activation,
)?;

if display_object.is_root() && display_object.as_stage().is_none() {
// TODO: Move this special PerspectiveProjection assignment to `root` object initialization time.
// This should be assigned to `root.transform` from the beginning.

let (width, height) = activation.context.stage.stage_size();

let center = activation.avm2().classes().point.construct(
activation,
&[(width as f64 / 2.0).into(), (height as f64 / 2.0).into()],
)?;

object.set_slot(pp_slots::CENTER, center, activation)?;
}

Ok(result)
Ok(object.into())
} else {
Ok(Value::Null)
}
Expand All @@ -439,15 +448,15 @@ pub fn set_perspective_projection<'gc>(
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_object().unwrap();

// FIXME: Render with the given PerspectiveProjection.
avm2_stub_setter!(activation, "flash.geom.Transform", "perspectiveProjection");

let set = args
.get(0)
.map(|arg| arg.as_object().is_some())
.unwrap_or_default();
let display_object = get_display_object(this);
display_object
.base()
.set_has_perspective_projection_stub(set);
let perspective_projection = args
.try_get_object(activation, 0)
.map(|object| object_to_perspective_projection(object, activation))
.transpose()?;

get_display_object(this).set_perspective_projection(activation.gc(), perspective_projection);

Ok(Value::Undefined)
}
Loading