10. WebGPU rotation transformation

The unit circle is a circle with a radius of 1.0.

The figure below is a unit circle. [ Note 1 ]
insert image description here

In the image above, when you drag the blue handle around the circle, the X and Y positions change, representing the point's position on the circle. And at the top, Y is 1 and X is 0. On the right, X is 1 and Y is 0.

If you remember basic third grade math, multiply something by 1 and it stays the same. So 123 * 1 = 123. Pretty basic, right? Well, a unit circle, a circle with a radius of 1.0 is also a form of 1. It's a spin of 1 . So something can be multiplied by this unit circle, in a way it's kind of like multiplying by 1.

We'll take the X and Y values ​​from an arbitrary point on the unit circle and multiply the vertex position by the values ​​from the previous example.

Below is the shader modification.

struct Uniforms {
    
    
  color: vec4f,
  resolution: vec2f,
  translation: vec2f,
  rotation: vec2f,
};
 
struct Vertex {
    
    
  @location(0) position: vec2f,
};
 
struct VSOutput {
    
    
  @builtin(position) position: vec4f,
};
 
@group(0) @binding(0) var<uniform> uni: Uniforms;
 
@vertex fn vs(vert: Vertex) -> VSOutput {
    
    
  var vsOut: VSOutput;
 
  // Rotate the position
  let rotatedPosition = vec2f(
    vert.position.x * uni.rotation.x - vert.position.y * uni.rotation.y, //here
    vert.position.x * uni.rotation.y + vert.position.y * uni.rotation.x  //here
  );
 
  // Add in the translation
 // let position = vert.position + uni.translation;
  let position = rotatedPosition + uni.translation;
 
  // convert the position from pixels to a 0.0 to 1.0 value
  let zeroToOne = position / uni.resolution;
 
  // convert from 0 <-> 1 to 0 <-> 2
  let zeroToTwo = zeroToOne * 2.0;
 
  // covert from 0 <-> 2 to -1 <-> +1 (clip space)
  let flippedClipSpace = zeroToTwo - 1.0;
 
  // flip Y
  let clipSpace = flippedClipSpace * vec2f(1, -1);
 
  vsOut.position = vec4f(clipSpace, 0.0, 1.0);
  return vsOut;
}

Updated JavaScript to increase uniform size.

  // color, resolution, translation
  //const uniformBufferSize = (4 + 2 + 2) * 4;
  // color, resolution, translation, rotation, padding
  const uniformBufferSize = (4 + 2 + 2 + 2) * 4 + 8; //here
  const uniformBuffer = device.createBuffer({
    
    
    label: 'uniforms',
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
 
  const uniformValues = new Float32Array(uniformBufferSize / 4);
 
  // offsets to the various uniform values in float32 indices
  const kColorOffset = 0;
  const kResolutionOffset = 4;
  const kTranslationOffset = 6;
  const kRotationOffset = 8; //here
 
  const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
  const resolutionValue = uniformValues.subarray(kResolutionOffset, kResolutionOffset + 2);
  const translationValue = uniformValues.subarray(kTranslationOffset, kTranslationOffset + 2);
  const rotationValue = uniformValues.subarray(kRotationOffset, kRotationOffset + 2); //here

We need UI for display. This article is not a tutorial on making a UI, so I'm only going to use one. First some HTML to give a placeholder for the unit circle

  <body>
    <canvas></canvas>
    <div id="circle"></div>
  </body>

and then some CSS for positioning

#circle {
    
    
  position: fixed;
  right: 0;
  bottom: 0;
  width: 300px;
  background-color: var(--bg-color);
}

And finally the JavaScript that uses it.

import UnitCircle from './resources/js/unit-circle.js'; //here
 
...
 
  const gui = new GUI();
  gui.onChange(render);
  gui.add(settings.translation, '0', 0, 1000).name('translation.x');
  gui.add(settings.translation, '1', 0, 1000).name('translation.y');
 
  const unitCircle = new UnitCircle(); //here
  document.querySelector('#circle').appendChild(unitCircle.domElement); //here
  unitCircle.onChange(render); //here
 
  function render() {
    
    
    ...
 
    // Set the uniform values in our JavaScript side Float32Array
    resolutionValue.set([canvas.width, canvas.height]);
    translationValue.set(settings.translation);
    rotationValue.set([unitCircle.x, unitCircle.y]); //here
 
    // upload the uniform values to the uniform buffer
    device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

Below is the displayed result. Drag the handle on the circle to rotate or the slider to translate.

insert image description here

Why does it work? Here is a mathematical explanation.

rotatedX = a_position.x * u_rotation.x - a_position.y * u_rotation.y;
rotatedY = a_position.x * u_rotation.y + a_position.y * u_rotation.x;

Suppose you have a rectangle and want to rotate it. Before starting to rotate, the upper right corner is at (3.0, -9.0). Let's take a point on the unit circle 30 degrees clockwise from 3 o'clock.
insert image description here

The position on the circle above is x = 0.87, y = 0.50

 3.0 * 0.87 - -9.0 * 0.50 =  7.1
 3.0 * 0.50 + -9.0 * 0.87 = -6.3

This is exactly where it rotates
insert image description here

Take the same 60 degrees clockwise
insert image description here

The positions on the circle are 0.87 and 0.50

 3.0 * 0.50 - -9.0 * 0.87 =  9.3
 3.0 * 0.87 + -9.0 * 0.50 = -1.9

You can see that when the point is rotated clockwise, the X value becomes larger and the Y value becomes smaller. If you continue beyond 90 degrees, X will start to get smaller again and Y will start to get larger. This pattern rotates alternately.

Points on the unit circle have another name. They are called sine and cosine. So for any given angle, the sine and cosine can be obtained like this.

function printSineAndCosineForAnAngle(angleInDegrees) {
    
    
  const angleInRadians = angleInDegrees * Math.PI / 180;
  const s = Math.sin(angleInRadians);
  const c = Math.cos(angleInRadians);
  console.log('s =', s, 'c =', c);
}

If you copy and paste the code into the JavaScript console and type printSineAndCosignForAngle(30), you'll see it print s = 0.50 c = 0.87 (note: I rounded the numbers)

If you put them together, you can rotate the vertex position to any angle you want. Just set the rotation to be the sine and cosine of the angle you want to rotate to.

  ...
  const angleInRadians = angleInDegrees * Math.PI / 180;
  rotation[0] = Math.cos(angleInRadians);
  rotation[1] = Math.sin(angleInRadians);

Change the code below to have only one rotation parameter.

  const degToRad = d => d * Math.PI / 180;
 
  const settings = {
    
    
    translation: [150, 100],
    rotation: degToRad(30),//here
  };
 
  const radToDegOptions = {
    
     min: -360, max: 360, step: 1, converters: GUI.converters.radToDeg };
 
  const gui = new GUI();
  gui.onChange(render);
  gui.add(settings.translation, '0', 0, 1000).name('translation.x');
  gui.add(settings.translation, '1', 0, 1000).name('translation.y');
  gui.add(settings, 'rotation', radToDegOptions);
 
 // const unitCircle = new UnitCircle();
 // document.querySelector('#circle').appendChild(unitCircle.domElement);
 // unitCircle.onChange(render);
 
  function render() {
    
    
    ...
 
    // Set the uniform values in our JavaScript side Float32Array
    resolutionValue.set([canvas.width, canvas.height]);
    translationValue.set(settings.translation); //here
    // rotationValue.set([unitCircle.x, unitCircle.y]);
    rotationValue.set([
        Math.cos(settings.rotation), //here
        Math.sin(settings.rotation), //here
    ]);

Drag the slider to translate or rotate.

insert image description here

I hope this is pretty intuitive. Next is a simpler one. Scale transform .

Note 1

What are radians?

Radians are the unit of measurement used for circles, rotations, and angles. Just like we can measure distances in inches, yards, meters, etc., we can measure angles in degrees or radians.

You probably know that the math for metric measurements is easier than that for imperial measurements. To go from inches to feet, we divide by 12. To go from inches to yards, we divide by 36. I don't know about you, but I can't divide by 36 in my head. It's much easier to use the metric system. To convert millimeters to centimeters, we divide by 10. To convert from millimeters to meters, we divide by 1000. I can mentally divide by 1000.

Radians are similar to degrees. Angles make math difficult. Radians make math easy. A circle has 360 degrees, but only 2π radians. So a full circle is 2π radians. A half circle is 1π radians. 1/4 turn, or 90 degrees, is 1/2π radian. So if you want to rotate something 90 degrees, just use Math.PI * 0.5 . If you want to rotate it 45 degrees, use Math.PI * 0.25 etc.

Almost any math involving angles, circles, or rotations is pretty straightforward if you start thinking about radians. So try it. Use radians instead of degrees, except for UI displays.

This unit circle has +Y down to match that our pixel space is also Y down. The normal clip space for WebGPU is +Y up . In the previous article, we have flipped the Y in the shader.

original address

Guess you like

Origin blog.csdn.net/xuejianxinokok/article/details/131183645