Skip to content
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

Feat: Add distort shader #271

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

lewispeel
Copy link

@lewispeel lewispeel commented May 9, 2024

I recently created a shader In Lightning 2 to distort a texture inside itself and ported it to Lightning 3. It allows you to define new coordinates for top-left, top-right, bottom-right and bottom-left inside it's original dimensions.

Changes

  • Added DistortShader to WebGL shaders (based on https://www.shadertoy.com/view/lsVSWW).
  • Added Point interface to CommonTypes to make shader params typesafe.
  • Added an example in examples/tests/distort-shader that shows using pixel values and normalized values.
  • Updated CoreShaderManager to include DistortShader as an option.

How to test

  • Run pnpm start
  • In the browser, navigate to http://localhost:5173/?test=distort-shader

Usage

You can use explicit pixel values or normalized values e.g. { x: 0.5 } will be 50% of the texture width. To use normalized values, add normalized: true to the shader parameters.

const size = 500;

const parent = renderer.createNode({
  parent: testRoot,
});

// distorted using pixel values
const pixels = renderer.createNode({
  x: 100,
  y: 100,
  width: size,
  height: size,
  parent,
  src: `https://picsum.photos/${size}/${size}`,
  shader: renderer.createShader('DistortShader', {
    topLeft: { x: 20, y: 50 },
    topRight: { x: 400, y: 0 },
    bottomRight: { x: 350, y: 300 },
    bottomLeft: { x: 50, y: 350 },
  }),
});

// distorted using normalized values
const normalized = renderer.createNode({
  x: 700,
  y: 100,
  width: size,
  height: size,
  parent,
  src: `https://picsum.photos/${size}/${size}`,
  shader: renderer.createShader('DistortShader', {
    normalized: true,
    topLeft: { x: 0.1, y: 0.1 },
    topRight: { x: 0.75, y: 0.2 },
    bottomRight: { x: 0.95, y: 0.7 },
    bottomLeft: { x: 0.05, y: 0.9 },
  }),
});

Output

Examples using pixel values (left), normalized values (right). The dark rectangle are to illustrate where the original dimensions of the image textures.

image

topLeft: props.topLeft || { x: 0, y: 0 },
topRight: props.topRight || { x: 1920, y: 0 },
bottomRight: props.bottomRight || { x: 1920, y: 1080 },
bottomLeft: props.bottomLeft || { x: 0, y: 1080 },
Copy link
Collaborator

Choose a reason for hiding this comment

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

These defaults are a bit too specific. I think one thing you might be able to do to achieve this dynamically is to allow null as value for each of these keys and make null the default. Then in bindProps I think you could check for null and dynamically create the defaults using the $dimensions property which at that point is reliable. (Going to leave a separate comment in regards to your request for comments on potentially using normalized 0.0 -> 1.0 values)

Copy link
Author

@lewispeel lewispeel May 10, 2024

Choose a reason for hiding this comment

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

Done. I think that approach is nice. I was calculating the normalized values in the shader code but this works. To get this working using null I had to modify the Point class to accept number and null types. Allowing null feels a bit weird to me. I could keep it as number only and the default be zero, then if I see a zero size width or height value assume its been left as zero on purpose and use the full width/height? What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd regard the whole Point as a unit. The type of each corner in that cause would be Point | null. So if you need to provide an x, you also need to provide the y.

Copy link
Author

Choose a reason for hiding this comment

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

Ah that makes sense, I'll change it 👍

Copy link
Author

Choose a reason for hiding this comment

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

@frank-weindel
Copy link
Collaborator

@lewispeel Awesome work. Thanks for the contribution. Regarding your question about potentially using relative normalized values instead of absolute position values, I'd say allow both. I can see both methods coming in handy depending on the situation. You could base it off of a new boolean prop flag (normalized perhaps) and do all the necessary resolutions in bindProps().

Please let me know if you need assistance generating the certified snapshot needed for this. You need to run the visual regression test runner in --ci --capture mode.

@lewispeel
Copy link
Author

lewispeel commented May 10, 2024

@frank-weindel Thanks for the feedback! I've got it working with both pixel and normalized values. Something to note, see line 132 in DistortShader - there's a bug when the values create a rectangle.. not sure why this is but I've added a workaround and would appreciate some feedback.

// if the dimensions create a rectangle, nothing is rendered
// adding a small amount to one of the x values resolves this
if (topLeft[0] === bottomLeft[0] && topRight[0] === bottomRight[0]) {
topLeft[0] = +0.0001;
Copy link
Contributor

Choose a reason for hiding this comment

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

Small typo here, you're now assigning 0.0001 to top-left x position. You probably meant:
topLeft[0] += 0.0001

Copy link
Author

@lewispeel lewispeel May 13, 2024

Choose a reason for hiding this comment

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

Good catch, thanks @erikhaandrikman - I removed this block as the fix you suggested below means I don't need this anymore.

return a.x * b.y - a.y * b.x;
}

vec2 invBilinear(in vec2 p, in vec2 a, in vec2 b, in vec2 c, in vec2 d ){
Copy link
Contributor

Choose a reason for hiding this comment

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

To prevent the rendering issues when the axis are perpendicular ( like you mentioned in your comment ) you can replace in inverse bilinear function for:

vec2 invBilinear( in vec2 p, in vec2 a, in vec2 b, in vec2 c, in vec2 d )
{
    vec2 e = b-a;
    vec2 f = d-a;
    vec2 g = a-b+c-d;
    vec2 h = p-a;

    float k2 = xross( g, f );
    float k1 = xross( e, f ) + xross( h, g );
    float k0 = xross( h, e );

    float w = k1*k1 - 4.0*k0*k2;
    if( w<0.0 ) return vec2(-1.0);
    w = sqrt( w );

    // will fail for k0=0, which is only on the ba edge
    float v = 2.0*k0/(-k1 - w);
    if( v<0.0 || v>1.0 ) v = 2.0*k0/(-k1 + w);

    float u = (h.x - f.x*v)/(e.x + g.x*v);
    if( u<0.0 || u>1.0 || v<0.0 || v>1.0 ) return vec2(-1.0);
    return vec2( u, v );
}

Copy link
Author

Choose a reason for hiding this comment

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

Amazing! Thanks @erikhaandrikman - this is fixed.

@frank-weindel
Copy link
Collaborator

frank-weindel commented May 13, 2024

@lewispeel This needs a snapshot generated with:

pnpm test:visual --ci --capture

Check out the DOCKER.md file for more information on how to set things up for it.

@lewispeel
Copy link
Author

lewispeel commented May 13, 2024

@erikhaandrikman I've just spotted a problem.. this demo works fine on single nodes with no children, but if I create a node with child nodes (requiring rtt=true), the texture coords are flipped vertically... topLeft becomes bottomLeft topRight becomes bottomRight. Do you know any reason why this might be?

To test this, comment out lines 93-95 in tests/distort-shader.ts - it should apply the effect to only the top left but applies it to the bottom left. If you then comment out rtt:true on line 89 and 99-133 and the effect works again.

@lewispeel
Copy link
Author

@lewispeel This needs a snapshot generated with:

pnpm test:visual --ci --capture

Check out the DOCKER.md file for more information on how to set things up for it.

Thanks Frank, I'll wait for Erik to feed back on the issue above and I'll make sure to run the visual tests 👍

@erikhaandrikman
Copy link
Contributor

erikhaandrikman commented May 14, 2024

@erikhaandrikman I've just spotted a problem.. this demo works fine on single nodes with no children, but if I create a node with child nodes (requiring rtt=true), the texture coords are flipped vertically... topLeft becomes bottomLeft topRight becomes bottomRight. Do you know any reason why this might be?

To test this, comment out lines 93-95 in tests/distort-shader.ts - it should apply the effect to only the top left but applies it to the bottom left. If you then comment out rtt:true on line 89 and 99-133 and the effect works again.

probably due to the fact that the render texture is render flipped ( and coords being restored before pushing them to the buffer )

[texCoordY1, texCoordY2] = [texCoordY2, texCoordY1];

i can look into this

@erikhaandrikman
Copy link
Contributor

We're encountering some projection issues, primarily with text, when attempting to flip the rendering in render texture nodes. As a temporary solution, we've been rendering the contents to an extra childNode (with RTT enabled) and flipping the rendering on the Y-axis by setting scaleY: -1

const distorted = renderer.createNode({
    x: 700,
    y: 100,
    width: size,
    height: size,
    parent,
    src: `https://picsum.photos/${size}/${size}`,
    shader: renderer.createShader('DistortShader', {
      normalized: true,
      topLeft: { x: 0.1, y: 0.1 },
      topRight: { x: 0.75, y: 0.2 },
      bottomRight: { x: 0.95, y: 0.7 },
      bottomLeft: { x: 0.05, y: 0.9 },
    }),
  });

  // distorted children inside a distorted container (requires rtt=true)
  const nested = renderer.createNode({
    x: 1300,
    y: 100,
    width: size,
    height: size,
    parent,
    rtt: true,
  });

  const nestedInner = renderer.createNode({
    width: size,
    height: size,
    parent: nested,
    rtt: true,
    color:0xffffffff,
    shader: renderer.createShader('DistortShader', {
      normalized: true,
      topLeft: { x: 0.1, y: 0.1 },
      topRight: { x: 0.75, y: 0.2 },
      bottomRight: { x: 0.95, y: 0.7 },
      bottomLeft: { x: 0.05, y: 0.9 },
    }),
    scaleY: -1,
  });

  const nestedBg = renderer.createNode({
    width: size,
    height: size,
    color: 0x990000ff,
    parent: nestedInner
  });

  const nestedHolder = renderer.createNode({
    width: size,
    height: size,
    parent: nestedInner,
    scaleY:-1,
    color: 0x99000000
  });
  
  // a distorted image inside a distorted container
  const image = renderer.createNode({
    x: size / 2,
    y: size / 2,
    width: size / 2,
    height: size / 2,
    mount: 0.5,
    parent: nestedHolder,
    src: `https://picsum.photos/${size}/${size}`,
    shader: renderer.createShader('DistortShader', {
      normalized: true,
      topLeft: { x: 0.1, y: 0.1 },
      topRight: { x: 0.75, y: 0.2 },
      bottomRight: { x: 0.95, y: 0.7 },
      bottomLeft: { x: 0.05, y: 0.9 },
    }),
  });

@wouterlucas
Copy link
Contributor

@erikhaandrikman can we move this along with known limitations of RTT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants