rework radial gradients

Adds full support for COLRv1 radial gradients based on the two-point conical gradient algorithm at https://skia.org/docs/dev/design/conical/

Also adds robustness to degenerate cases in gradient encoding:
* Radial where p0 == p1 && r0 == r1 renders transparent solid
* Empty stops render as transparent solid
* Single stop renders as solid
This commit is contained in:
Chad Brokaw 2023-05-09 18:09:53 -04:00
parent ced6309a3b
commit b103a55301
13 changed files with 284 additions and 95 deletions

View file

@ -10,7 +10,8 @@
"pathtag": "${workspaceFolder}/shader/shared/pathtag.wgsl",
"ptcl": "${workspaceFolder}/shader/shared/ptcl.wgsl",
"segment": "${workspaceFolder}/shader/shared/segment.wgsl",
"tile": "${workspaceFolder}/shader/shared/tile.wgsl"
"tile": "${workspaceFolder}/shader/shared/tile.wgsl",
"transform": "${workspaceFolder}/shader/shared/transform.wgsl"
},
"wgsl-analyzer.diagnostics.nagaVersion": "main",
"wgsl-analyzer.preprocessor.shaderDefs": [

View file

@ -22,7 +22,7 @@ impl DrawTag {
pub const LINEAR_GRADIENT: Self = Self(0x114);
/// Radial gradient fill.
pub const RADIAL_GRADIENT: Self = Self(0x2dc);
pub const RADIAL_GRADIENT: Self = Self(0x29c);
/// Image fill.
pub const IMAGE: Self = Self(0x248);

View file

@ -3,7 +3,7 @@
use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform};
use peniko::{kurbo::Shape, BlendMode, BrushRef};
use peniko::{kurbo::Shape, BlendMode, BrushRef, Color};
#[cfg(feature = "full")]
use {
@ -281,10 +281,15 @@ impl Encoding {
alpha: f32,
extend: Extend,
) {
self.add_ramp(color_stops, alpha, extend);
self.draw_tags.push(DrawTag::LINEAR_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
match self.add_ramp(color_stops, alpha, extend) {
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
RampStops::One(color) => self.encode_color(DrawColor::new(color)),
_ => {
self.draw_tags.push(DrawTag::LINEAR_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
}
}
}
/// Encodes a radial gradient brush.
@ -296,10 +301,20 @@ impl Encoding {
alpha: f32,
extend: Extend,
) {
self.add_ramp(color_stops, alpha, extend);
self.draw_tags.push(DrawTag::RADIAL_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
// Match Skia's epsilon for radii comparison
const SKIA_EPSILON: f32 = 1.0 / (1 << 12) as f32;
if gradient.p0 == gradient.p1 && (gradient.r0 - gradient.r1).abs() < SKIA_EPSILON {
self.encode_color(DrawColor::new(Color::TRANSPARENT));
}
match self.add_ramp(color_stops, alpha, extend) {
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
RampStops::One(color) => self.encode_color(DrawColor::new(color)),
_ => {
self.draw_tags.push(DrawTag::RADIAL_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
}
}
}
/// Encodes an image brush.
@ -354,7 +369,7 @@ impl Encoding {
color_stops: impl Iterator<Item = ColorStop>,
alpha: f32,
extend: Extend,
) {
) -> RampStops {
let offset = self.draw_data.len();
let stops_start = self.resources.color_stops.len();
if alpha != 1.0 {
@ -364,14 +379,32 @@ impl Encoding {
} else {
self.resources.color_stops.extend(color_stops);
}
self.resources.patches.push(Patch::Ramp {
draw_data_offset: offset,
stops: stops_start..self.resources.color_stops.len(),
extend,
});
let stops_end = self.resources.color_stops.len();
match stops_end - stops_start {
0 => RampStops::Empty,
1 => RampStops::One(self.resources.color_stops.pop().unwrap().color),
_ => {
self.resources.patches.push(Patch::Ramp {
draw_data_offset: offset,
stops: stops_start..stops_end,
extend,
});
RampStops::Many
}
}
}
}
/// Result for adding a sequence of color stops.
enum RampStops {
/// Color stop sequence was empty.
Empty,
/// Contained a single color stop.
One(Color),
/// More than one color stop.
Many,
}
/// Encoded data for late bound resources.
#[cfg(feature = "full")]
#[derive(Clone, Default)]

View file

@ -31,6 +31,7 @@ pub fn test_scenes() -> SceneSet {
scene!(cardioid_and_friends),
scene!(animated_text: animated),
scene!(gradient_extend),
scene!(two_point_radial),
scene!(brush_transform: animated),
scene!(blend_grid),
scene!(conflation_artifacts),
@ -252,11 +253,13 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) {
fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) {
fn square(sb: &mut SceneBuilder, is_radial: bool, transform: Affine, extend: Extend) {
let colors = [Color::RED, Color::GREEN, Color::BLUE];
let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE];
let width = 300f64;
let height = 300f64;
let gradient: Brush = if is_radial {
Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32)
let center = (width * 0.5, height * 0.5);
let radius = (width * 0.25) as f32;
Gradient::new_two_point_radial(center, radius * 0.25, center, radius)
.with_stops(colors)
.with_extend(extend)
.into()
@ -294,10 +297,45 @@ fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) {
label,
);
}
}
fn two_point_radial(sb: &mut SceneBuilder, params: &mut SceneParams) {
let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE];
let extend = Extend::Reflect;
let gradient1 = Gradient::new_two_point_radial((150.0, 150.0), 50.0, (200.0, 150.0), 100.0)
.with_extend(extend)
.with_stops(colors);
let gradient2 = Gradient::new_two_point_radial((300.0, 150.0), 100.0, (150.0, 150.0), 50.0)
.with_extend(extend)
.with_stops(colors);
let gradient3 = Gradient::new_two_point_radial((300.0, 150.0), 50.0, (150.0, 150.0), 50.0)
.with_extend(extend)
.with_stops(colors);
sb.fill(
Fill::NonZero,
Affine::scale(1.5) * Affine::translate((50.0, 0.0)),
&gradient1,
None,
&Rect::new(0.0, 0.0, 400.0, 400.0),
);
sb.fill(
Fill::NonZero,
Affine::scale(1.5) * Affine::translate((50.0, 300.0)),
&gradient2,
None,
&Rect::new(0.0, 0.0, 400.0, 400.0),
);
sb.fill(
Fill::NonZero,
Affine::scale(1.5) * Affine::translate((50.0, 600.0)),
&gradient3,
None,
&Rect::new(0.0, 0.0, 400.0, 400.0),
);
let t = (params.time * 0.5).sin() * 0.5 + 0.5;
let x_delta: f64 = t * 2000.0 - 1000.0;
let gradient =
Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 200.0)
Gradient::new_two_point_radial((400.0, 500.0), 100.0, (101.0 + x_delta, 500.0), 0.0)
.with_extend(Extend::Reflect)
.with_stops([Color::GREEN, Color::WHITE, Color::RED]);
sb.fill(

View file

@ -376,7 +376,7 @@ fn main(
}
}
// DRAWTAG_FILL_RAD_GRADIENT
case 0x2dcu: {
case 0x29cu: {
let linewidth = bitcast<f32>(info_bin_data[di]);
if write_path(tile, linewidth) {
let index = scene[dd];

View file

@ -6,6 +6,7 @@
#import clip
#import drawtag
#import bbox
#import transform
@group(0) @binding(0)
var<uniform> config: Config;
@ -30,12 +31,6 @@ var<storage, read_write> clip_inp: array<ClipInp>;
let WG_SIZE = 256u;
// Possibly dedup?
struct Transform {
matrx: vec4<f32>,
translate: vec2<f32>,
}
fn read_transform(transform_base: u32, ix: u32) -> Transform {
let base = transform_base + ix * 6u;
let c0 = bitcast<f32>(scene[base]);
@ -110,18 +105,16 @@ fn main(
// let y1 = f32(bbox.y1);
// let bbox_f = vec4(x0, y0, x1, y1);
let fill_mode = u32(bbox.linewidth >= 0.0);
var matrx: vec4<f32>;
var translate: vec2<f32>;
var transform = Transform();
var linewidth = bbox.linewidth;
if linewidth >= 0.0 || tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT ||
tag_word == DRAWTAG_FILL_IMAGE
{
let transform = read_transform(config.transform_base, bbox.trans_ix);
matrx = transform.matrx;
translate = transform.translate;
transform = read_transform(config.transform_base, bbox.trans_ix);
}
if linewidth >= 0.0 {
// Note: doesn't deal with anisotropic case
let matrx = transform.matrx;
linewidth *= sqrt(abs(matrx.x * matrx.w - matrx.y * matrx.z));
}
switch tag_word {
@ -134,8 +127,8 @@ fn main(
info[di] = bitcast<u32>(linewidth);
var p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
var p1 = bitcast<vec2<f32>>(vec2(scene[dd + 3u], scene[dd + 4u]));
p0 = matrx.xy * p0.x + matrx.zw * p0.y + translate;
p1 = matrx.xy * p1.x + matrx.zw * p1.y + translate;
p0 = transform_apply(transform, p0);
p1 = transform_apply(transform, p1);
let dxy = p1 - p0;
let scale = 1.0 / dot(dxy, dxy);
let line_xy = dxy * scale;
@ -145,48 +138,100 @@ fn main(
info[di + 3u] = bitcast<u32>(line_c);
}
// DRAWTAG_FILL_RAD_GRADIENT
case 0x2dcu: {
case 0x29cu: {
// Two-point conical gradient implementation based
// on the algorithm at <https://skia.org/docs/dev/design/conical/>
// This epsilon matches what Skia uses
let GRADIENT_EPSILON = 1.0 / f32(1 << 12u);
info[di] = bitcast<u32>(linewidth);
var p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
var p1 = bitcast<vec2<f32>>(vec2(scene[dd + 3u], scene[dd + 4u]));
let r0 = bitcast<f32>(scene[dd + 5u]);
let r1 = bitcast<f32>(scene[dd + 6u]);
let inv_det = 1.0 / (matrx.x * matrx.w - matrx.y * matrx.z);
let inv_mat = inv_det * vec4(matrx.w, -matrx.y, -matrx.z, matrx.x);
let rr = r1 / (r1 - r0);
var rr1 = rr;
if r0 > 0.0 {
p0 = p0 + (p0 - p1) * rr * 0.5;
rr1 = 1.0;
var r0 = bitcast<f32>(scene[dd + 5u]);
var r1 = bitcast<f32>(scene[dd + 6u]);
let user_to_gradient = transform_inverse(transform);
// Output variables
var xform = Transform();
var focal_x = 0.0;
var radius = 0.0;
var kind = 0u;
var flags = 0u;
if abs(r0 - r1) <= GRADIENT_EPSILON {
// When the radii are the same, emit a strip gradient
kind = RAD_GRAD_KIND_STRIP;
let scaled = r0 / distance(p0, p1);
xform = transform_mul(
two_point_to_unit_line(p0, p1),
user_to_gradient
);
radius = scaled * scaled;
} else {
// Assume a two point conical gradient unless the centers
// are equal.
kind = RAD_GRAD_KIND_CONE;
if all(p0 == p1) {
kind = RAD_GRAD_KIND_CIRCULAR;
// Nudge p0 a bit to avoid denormals.
p0 += GRADIENT_EPSILON;
}
if r1 == 0.0 {
// If r1 == 0.0, swap the points and radii
flags |= RAD_GRAD_SWAPPED;
let tmp_p = p0;
p0 = p1;
p1 = tmp_p;
let tmp_r = r0;
r0 = r1;
r1 = tmp_r;
}
focal_x = r0 / (r0 - r1);
let one_minus_focal_x = 1.0 - focal_x;
let cf = one_minus_focal_x * p0 + focal_x * p1;
let abs_one_minus_focal_x = abs(one_minus_focal_x);
radius = r1 / (distance(cf, p1));
let user_to_unit_line = transform_mul(
two_point_to_unit_line(cf, p1),
user_to_gradient
);
var user_to_scaled = user_to_unit_line;
// When r == 1.0, focal point is on circle
if abs(radius - 1.0) <= GRADIENT_EPSILON {
kind = RAD_GRAD_KIND_FOCAL_ON_CIRCLE;
let scale = 0.5 * abs_one_minus_focal_x;
user_to_scaled = transform_mul(
Transform(vec4(scale, 0.0, 0.0, scale), vec2(0.0)),
user_to_unit_line
);
} else {
let a = radius * radius - 1.0;
let scale_x = radius / a * abs_one_minus_focal_x;
let scale_y = sqrt(abs(a)) / a * abs_one_minus_focal_x;
user_to_scaled = transform_mul(
Transform(vec4(scale_x, 0.0, 0.0, scale_y), vec2(0.0)),
user_to_unit_line
);
}
xform = user_to_scaled;
}
let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -translate - p0;
let center1 = p1 - p0;
let ra_inv = rr1 / (r1 * r1 - dot(center1, center1));
let c1 = center1 * ra_inv;
let ra = rr1 * ra_inv;
info[di + 1u] = bitcast<u32>(inv_mat.x);
info[di + 2u] = bitcast<u32>(inv_mat.y);
info[di + 3u] = bitcast<u32>(inv_mat.z);
info[di + 4u] = bitcast<u32>(inv_mat.w);
info[di + 5u] = bitcast<u32>(inv_tr.x);
info[di + 6u] = bitcast<u32>(inv_tr.y);
info[di + 7u] = bitcast<u32>(c1.x);
info[di + 8u] = bitcast<u32>(c1.y);
info[di + 9u] = bitcast<u32>(ra);
info[di + 10u] = bitcast<u32>(rr);
info[di + 1u] = bitcast<u32>(xform.matrx.x);
info[di + 2u] = bitcast<u32>(xform.matrx.y);
info[di + 3u] = bitcast<u32>(xform.matrx.z);
info[di + 4u] = bitcast<u32>(xform.matrx.w);
info[di + 5u] = bitcast<u32>(xform.translate.x);
info[di + 6u] = bitcast<u32>(xform.translate.y);
info[di + 7u] = bitcast<u32>(focal_x);
info[di + 8u] = bitcast<u32>(radius);
info[di + 9u] = bitcast<u32>((flags << 3u) | kind);
}
// DRAWTAG_FILL_IMAGE
case 0x248u: {
info[di] = bitcast<u32>(linewidth);
let inv_det = 1.0 / (matrx.x * matrx.w - matrx.y * matrx.z);
let inv_mat = inv_det * vec4(matrx.w, -matrx.y, -matrx.z, matrx.x);
let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -translate;
info[di + 1u] = bitcast<u32>(inv_mat.x);
info[di + 2u] = bitcast<u32>(inv_mat.y);
info[di + 3u] = bitcast<u32>(inv_mat.z);
info[di + 4u] = bitcast<u32>(inv_mat.w);
info[di + 5u] = bitcast<u32>(inv_tr.x);
info[di + 6u] = bitcast<u32>(inv_tr.y);
let inv = transform_inverse(transform);
info[di + 1u] = bitcast<u32>(inv.matrx.x);
info[di + 2u] = bitcast<u32>(inv.matrx.y);
info[di + 3u] = bitcast<u32>(inv.matrx.z);
info[di + 4u] = bitcast<u32>(inv.matrx.w);
info[di + 5u] = bitcast<u32>(inv.translate.x);
info[di + 6u] = bitcast<u32>(inv.translate.y);
info[di + 7u] = scene[dd];
info[di + 8u] = scene[dd + 1u];
}
@ -201,3 +246,17 @@ fn main(
clip_inp[m.clip_ix] = ClipInp(ix, i32(path_ix));
}
}
fn two_point_to_unit_line(p0: vec2<f32>, p1: vec2<f32>) -> Transform {
let tmp1 = from_poly2(p0, p1);
let inv = transform_inverse(tmp1);
let tmp2 = from_poly2(vec2(0.0), vec2(1.0, 0.0));
return transform_mul(tmp2, inv);
}
fn from_poly2(p0: vec2<f32>, p1: vec2<f32>) -> Transform {
return Transform(
vec4(p1.y - p0.y, p0.x - p1.x, p1.x - p0.x, p1.y - p0.y),
vec2(p0.x, p0.y)
);
}

View file

@ -82,10 +82,12 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad {
let m3 = bitcast<f32>(info[info_offset + 3u]);
let matrx = vec4(m0, m1, m2, m3);
let xlat = vec2(bitcast<f32>(info[info_offset + 4u]), bitcast<f32>(info[info_offset + 5u]));
let c1 = vec2(bitcast<f32>(info[info_offset + 6u]), bitcast<f32>(info[info_offset + 7u]));
let ra = bitcast<f32>(info[info_offset + 8u]);
let roff = bitcast<f32>(info[info_offset + 9u]);
return CmdRadGrad(index, extend_mode, matrx, xlat, c1, ra, roff);
let focal_x = bitcast<f32>(info[info_offset + 6u]);
let radius = bitcast<f32>(info[info_offset + 7u]);
let flags_kind = info[info_offset + 8u];
let flags = flags_kind >> 3u;
let kind = flags_kind & 0x7u;
return CmdRadGrad(index, extend_mode, matrx, xlat, focal_x, radius, kind, flags);
}
fn read_image(cmd_ix: u32) -> CmdImage {
@ -295,17 +297,45 @@ fn main(
// CMD_RAD_GRAD
case 7u: {
let rad = read_rad_grad(cmd_ix);
let rr = rad.rr;
let roff = rr - 1.0;
let focal_x = rad.focal_x;
let one_minus_focal_x = 1.0 - focal_x;
let radius = rad.radius;
let is_strip = rad.kind == RAD_GRAD_KIND_STRIP;
let is_circular = rad.kind == RAD_GRAD_KIND_CIRCULAR;
let is_focal_on_circle = rad.kind == RAD_GRAD_KIND_FOCAL_ON_CIRCLE;
let is_swapped = (rad.flags & RAD_GRAD_SWAPPED) != 0u;
let inv_r1 = select(1.0 / radius, 0.0, is_circular);
let root_f = select(1.0, -1.0, is_swapped || one_minus_focal_x < 0.0);
let t_base_scale = select(vec2(0.0, -1.0), vec2(1.0, 1.0), is_swapped);
let t_sign = sign(one_minus_focal_x);
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
let my_xy = vec2(xy.x + f32(i), xy.y);
// TODO: can hoist y, but for now stick to the GLSL version
let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat;
let ba = dot(xy_xformed, rad.c1);
let ca = rad.ra * dot(xy_xformed, xy_xformed);
let t = sqrt(ba * ba + ca) - ba;
if t >= 0.0 {
let x = i32(round(extend_mode(t * rr - roff, rad.extend_mode) * f32(GRADIENT_WIDTH - 1)));
let local_xy = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat;
let x = local_xy.x;
let y = local_xy.y;
let xx = x * x;
let yy = y * y;
let x_inv_r1 = x * inv_r1;
var t = 0.0;
var valid = true;
if is_strip {
let a = radius - yy;
t = sqrt(a) + x;
valid = a >= 0.0;
} else if is_focal_on_circle {
t = (xx + yy) / x;
valid = t >= 0.0;
} else if radius > 1.0 {
t = sqrt(xx + yy) - x_inv_r1;
} else {
let a = xx - yy;
t = root_f * sqrt(a) - x_inv_r1;
valid = a >= 0.0 && t >= 0.0;
}
if valid {
t = extend_mode(focal_x + t_sign * t, rad.extend_mode);
t = (t_base_scale.x - t) * t_base_scale.y;
let x = i32(round(t * f32(GRADIENT_WIDTH - 1)));
let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;

View file

@ -14,6 +14,7 @@
#import config
#import pathtag
#import cubic
#import transform
@group(0) @binding(0)
var<uniform> config: Config;
@ -36,7 +37,6 @@ struct AtomicPathBbox {
@group(0) @binding(3)
var<storage, read_write> path_bboxes: array<AtomicPathBbox>;
@group(0) @binding(4)
var<storage, read_write> cubics: array<Cubic>;
@ -85,11 +85,6 @@ fn read_i16_point(ix: u32) -> vec2<f32> {
return vec2(x, y);
}
struct Transform {
matrx: vec4<f32>,
translate: vec2<f32>,
}
fn read_transform(transform_base: u32, ix: u32) -> Transform {
let base = transform_base + ix * 6u;
let c0 = bitcast<f32>(scene[base]);
@ -103,10 +98,6 @@ fn read_transform(transform_base: u32, ix: u32) -> Transform {
return Transform(matrx, translate);
}
fn transform_apply(transform: Transform, p: vec2<f32>) -> vec2<f32> {
return transform.matrx.xy * p.x + transform.matrx.zw * p.y + transform.translate;
}
fn round_down(x: f32) -> i32 {
return i32(floor(x));
}

View file

@ -49,3 +49,12 @@ let N_TILE_Y = 16u;
let N_TILE = 256u;
let BLEND_STACK_SPLIT = 4u;
// Radial gradient kinds
let RAD_GRAD_KIND_CIRCULAR = 1u;
let RAD_GRAD_KIND_STRIP = 2u;
let RAD_GRAD_KIND_FOCAL_ON_CIRCLE = 3u;
let RAD_GRAD_KIND_CONE = 4u;
// Radial gradient flags
let RAD_GRAD_SWAPPED = 1u;

View file

@ -18,7 +18,7 @@ struct DrawMonoid {
let DRAWTAG_NOP = 0u;
let DRAWTAG_FILL_COLOR = 0x44u;
let DRAWTAG_FILL_LIN_GRADIENT = 0x114u;
let DRAWTAG_FILL_RAD_GRADIENT = 0x2dcu;
let DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
let DRAWTAG_FILL_IMAGE = 0x248u;
let DRAWTAG_BEGIN_CLIP = 0x9u;
let DRAWTAG_END_CLIP = 0x21u;

View file

@ -55,9 +55,10 @@ struct CmdRadGrad {
extend_mode: u32,
matrx: vec4<f32>,
xlat: vec2<f32>,
c1: vec2<f32>,
ra: f32,
rr: f32,
focal_x: f32,
radius: f32,
kind: u32,
flags: u32,
}
struct CmdImage {

View file

@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense
// Helpers for working with transforms.
struct Transform {
matrx: vec4<f32>,
translate: vec2<f32>,
}
fn transform_apply(transform: Transform, p: vec2<f32>) -> vec2<f32> {
return transform.matrx.xy * p.x + transform.matrx.zw * p.y + transform.translate;
}
fn transform_inverse(transform: Transform) -> Transform {
let inv_det = 1.0 / (transform.matrx.x * transform.matrx.w - transform.matrx.y * transform.matrx.z);
let inv_mat = inv_det * vec4(transform.matrx.w, -transform.matrx.y, -transform.matrx.z, transform.matrx.x);
let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -transform.translate;
return Transform(inv_mat, inv_tr);
}
fn transform_mul(a: Transform, b: Transform) -> Transform {
return Transform(
a.matrx.xyxy * b.matrx.xxzz + a.matrx.zwzw * b.matrx.yyww,
a.matrx.xy * b.translate.x + a.matrx.zw * b.translate.y + a.translate
);
}

View file

@ -318,4 +318,5 @@ const SHARED_SHADERS: &[(&str, &str)] = &[
shared_shader!("ptcl"),
shared_shader!("segment"),
shared_shader!("tile"),
shared_shader!("transform"),
];