暗黑模式
坐标变换(2D)
教程3DWebGL
变换(Transforms)和仿射变换(Affine Transforms)
在计算机图形学中,变换(Transforms)是一种用于改变对象位置、方向、大小或形状的操作。它们是将对象从一个坐标空间变换到另一个坐标空间的数学操作。而仿射变换(Affine transformations)是一种特殊类型的变换,它保持了原始对象的直线性质和平行性质。仿射变换是一种线性变换,意味着它保持了原始对象的直线性质和平行性质。在仿射变换中,原始对象的平行线在变换后仍然是平行的。在计算机图形学中,仿射变换通常用于实现基本的图形操作,如对象的平移、旋转和缩放。它们也常用于构建更复杂的变换操作,如透视投影等。
仿射变换(Affine transformations) 是一种线性变换,它包括以下几种基本变换:
- 平移(Translation):沿着坐标轴移动对象的位置。
- 旋转(Rotation):围绕一个点或轴线旋转对象。
- 缩放(Scaling):增加或减少对象的大小。
- 剪切(Shearing):按比例移动对象的顶点,从而改变其形状。
这些变换可以单独应用,也可以组合在一起以实现更复杂的效果。
在二维空间中,仿射变换通常由一个 2x2 的矩阵和一个平移向量(通常表示为一个 2x1 的列向量)组成,表示为:
在三维空间中,仿射变换通常由一个 3x3 的矩阵和一个平移向量(表示为一个 3x1 的列向量)组成,表示为:
平移(Translation)
平移公式推导
Translation
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform vec2 u_translation; // 用于传入平移量
uniform float u_heightWidthRatio;
void main() {
gl_Position = vec4(a_position + u_translation, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_translation = gl.getUniformLocation(shaderProgram, "u_translation");
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
gl.uniform2f(shaderProgram_location_translation, v.x, v.y);
draw();
},
},
};
// @ts-ignore
dynamicImportScript("/js/utils/tweakpane.utils.js").then(() => {
// @ts-ignore
TweakPaneUtils.createTweakPane(paneConfigs, {
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
});
});
});
</script>
隐藏源代码
旋转(Rotation)
绕Z轴旋转公式推导

Translation
Rotation
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import TweakPaneUtils from "@public/js/utils/tweakpane.utils.js";
import * as TweakpaneRotationInputPlugin from "@public/js/utils/tweakpane-plugin-rotation.min";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform vec2 u_translation; // 用于传入平移量
uniform vec2 u_rotation; // 用于传入旋转量
uniform float u_heightWidthRatio;
void main() {
// Rotate the position
// x' = x*cosɵ - y*sinɵ
// y' = x*sinɵ + y*cosɵ
vec2 rotatedPosition = vec2(
a_position.x * u_rotation.y - a_position.y * u_rotation.x,
a_position.y * u_rotation.y + a_position.x * u_rotation.x);
gl_Position = vec4(rotatedPosition + u_translation, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_translation = gl.getUniformLocation(shaderProgram, "u_translation");
const shaderProgram_location_rotation = gl.getUniformLocation(shaderProgram, "u_rotation");
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
updateRotation(0);
draw();
function updateRotation(zAxisDeg: number) {
const angleInDegrees = zAxisDeg;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
const rotation = [Math.sin(angleInRadians), Math.cos(angleInRadians)];
gl.uniform2fv(shaderProgram_location_rotation, rotation);
}
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
gl.uniform2f(shaderProgram_location_translation, v.x, v.y);
draw();
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
updateRotation(v.z);
draw();
},
},
Reset() {
pane.reset();
},
};
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
TweakpaneRotationInputPlugin,
);
});
</script>
隐藏源代码
缩放(Scale)
缩放公式推导
Translation
Rotation
Scale
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
// import TweakPaneUtils from "@public/js/utils/tweakpane.utils.js";
// import * as TweakpaneRotationInputPlugin from "@public/js/utils/tweakpane-plugin-rotation.min";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform vec2 u_translation; // 用于传入平移量
uniform vec2 u_rotation; // 用于传入旋转量
uniform vec2 u_scale; // 用于传入缩放量
uniform float u_heightWidthRatio;
void main() {
// Scale
vec2 scaledPosition = a_position * u_scale;
// Rotate the position
// x' = x*cosɵ - y*sinɵ
// y' = x*sinɵ + y*cosɵ
vec2 rotatedPosition = vec2(
scaledPosition.x * u_rotation.y - scaledPosition.y * u_rotation.x,
scaledPosition.y * u_rotation.y + scaledPosition.x * u_rotation.x);
gl_Position = vec4(rotatedPosition + u_translation, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_translation = gl.getUniformLocation(shaderProgram, "u_translation");
const shaderProgram_location_rotation = gl.getUniformLocation(shaderProgram, "u_rotation");
const shaderProgram_location_scale = gl.getUniformLocation(shaderProgram, "u_scale");
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
updateRotation(0);
updateScale(1, 1);
draw();
function updateRotation(zAxisDeg: number) {
const angleInDegrees = zAxisDeg;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
const rotation = [Math.sin(angleInRadians), Math.cos(angleInRadians)];
gl.uniform2fv(shaderProgram_location_rotation, rotation);
}
function updateScale(x: number, y: number) {
gl.uniform2fv(shaderProgram_location_scale, [x, y]);
}
// @ts-ignore
dynamicImportScript(["/js/utils/tweakpane.utils.js", "/js/utils/tweakpane-plugin-rotation.min.js"]).then((res) => {
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
gl.uniform2f(shaderProgram_location_translation, v.x, v.y);
draw();
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
updateRotation(v.z);
draw();
},
},
Scale: {
x: { min: -2, max: 2, value: 1 },
y: { min: -2, max: 2, value: 1, inverted: true },
onChange: (v) => {
updateScale(v.x, v.y);
draw();
},
},
Reset() {
pane.reset();
},
};
const TweakPaneUtils = res[0].default ? res[0].default : res[0];
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
res[1],
);
});
});
</script>
隐藏源代码
二维矩阵
平移
平移矩阵推导
注意
代码中存储矩阵数据时,为了方便存储和获取矩阵的某一列,所以代码的实现和数学中矩阵的惯例是不一样的:
- 矩阵的数学表达:js
const some4x4TranslationMatrix_math = [ 1, 0, 0, tx, // row 1 0, 1, 0, ty, // row 2 0, 0, 1, tx, // row 3 0, 0, 0, 1, // row 4 ];
1
2
3
4
5
6 - 矩阵的代码实现:js
const some4x4TranslationMatrix_webgl = [ 1, 0, 0, 0, // row 1 = column 1 of math convention 0, 1, 0, 0, // row 2 = column 2 of math convention 0, 0, 1, 0, // row 3 = column 3 of math convention tx, ty, tz, 1, // row 4 = column 4 of math convention ];
1
2
3
4
5
6
使用矩阵实现平移:
Translation
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import { vec2, mat3 } from "@public/js/math/tiny-matrix";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform mat3 u_translationMatrix; // 用于传入平移矩阵
uniform float u_heightWidthRatio;
void main() {
gl_Position = vec4((u_translationMatrix*vec3(a_position, 1)).xy, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_translation = gl.getUniformLocation(shaderProgram, "u_translationMatrix");
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
const translationMatrix = new mat3(); // 单位矩阵
gl.uniformMatrix3fv(shaderProgram_location_translation, false, translationMatrix); // 初始化 u_translationMatrix 为单位矩阵
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
const translationMatrix = new mat3();
translationMatrix.translate(new vec2(v.x, v.y));
gl.uniformMatrix3fv(shaderProgram_location_translation, false, translationMatrix);
draw();
},
},
};
// @ts-ignore
dynamicImportScript("/js/utils/tweakpane.utils.js").then(() => {
// @ts-ignore
TweakPaneUtils.createTweakPane(paneConfigs, {
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
});
});
});
</script>
隐藏源代码
旋转
旋转矩阵推导
使用矩阵实现平移、旋转:
Translation
Rotation
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import { vec2, mat3 } from "@public/js/math/tiny-matrix";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform mat3 u_translationRotationMatrix; // 用于传入平移和旋转矩阵
uniform float u_heightWidthRatio;
void main() {
gl_Position = vec4((u_translationRotationMatrix*vec3(a_position, 1)).xy, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_translationRotation = gl.getUniformLocation(
shaderProgram,
"u_translationRotationMatrix",
);
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
const matrix = new mat3(); // 单位矩阵
gl.uniformMatrix3fv(shaderProgram_location_translationRotation, false, matrix); // 初始化 u_translationRotationMatrix 为单位矩阵
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
// @ts-ignore
dynamicImportScript(["/js/utils/tweakpane.utils.js", "/js/utils/tweakpane-plugin-rotation.min.js"]).then((res) => {
let lastTranslation: any = undefined;
let lastRotation: any = undefined;
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
const matrix = new mat3();
matrix.translate(new vec2(v.x, v.y)); // translation matrix
if (lastRotation) {
const angleInDegrees = lastRotation.z;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
matrix.rotate(angleInRadians); // translation rotation matrix
}
gl.uniformMatrix3fv(shaderProgram_location_translationRotation, false, matrix);
draw();
lastTranslation = v;
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
const matrix = new mat3();
if (lastTranslation) {
matrix.translate(new vec2(lastTranslation.x, lastTranslation.y)); // translation matrix
}
const angleInDegrees = v.z;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
matrix.rotate(angleInRadians); // translation rotation matrix
gl.uniformMatrix3fv(shaderProgram_location_translationRotation, false, matrix);
draw();
lastRotation = v;
},
},
Reset() {
pane.reset();
},
};
const TweakPaneUtils = res[0].default ? res[0].default : res[0];
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
res[1],
);
});
});
</script>
隐藏源代码
缩放
缩放矩阵推导
Translation
Rotation
Scale
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import { vec2, mat3 } from "@public/js/math/tiny-matrix";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
const heightWidthRatio= canvas3d.height / canvas3d.width;
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform mat3 u_worldMatrix; // 用于传入平移、旋转和缩放矩阵,即从模型空间转换到世界空间
uniform float u_heightWidthRatio;
void main() {
gl_Position = vec4((u_worldMatrix*vec3(a_position, 1)).xy, 0, 1); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_world = gl.getUniformLocation(shaderProgram, "u_worldMatrix");
const shaderProgram_location_heightWidthRatio = gl.getUniformLocation(shaderProgram, "u_heightWidthRatio");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
gl.uniform1f(shaderProgram_location_heightWidthRatio, heightWidthRatio);
const matrix = new mat3(); // 单位矩阵
gl.uniformMatrix3fv(shaderProgram_location_world, false, matrix); // 初始化 u_worldMatrix 为单位矩阵
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
function updateMatrix(translation: any, rotation: any, scale: any) {
const matrix = new mat3();
if (translation) {
matrix.translate(new vec2(translation.x, translation.y)); // translation matrix
}
if (rotation) {
const angleInDegrees = rotation.z;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
matrix.rotate(angleInRadians); // translation rotation matrix
}
if (scale) {
matrix.scale(new vec2(scale.x, scale.y)); // translation rotation scale matrix, i.e. world matrix
}
gl.uniformMatrix3fv(shaderProgram_location_world, false, matrix);
}
// @ts-ignore
dynamicImportScript(["/js/utils/tweakpane.utils.js", "/js/utils/tweakpane-plugin-rotation.min.js"]).then((res) => {
let lastTranslation: any = undefined;
let lastRotation: any = undefined;
let lastScale: any = undefined;
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
lastTranslation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
lastRotation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Scale: {
x: { min: -1, max: 2, value: 1 },
y: { min: -1, max: 2, value: 1, inverted: true },
onChange: (v) => {
lastScale = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Reset() {
pane.reset();
},
};
const TweakPaneUtils = res[0].default ? res[0].default : res[0];
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
res[1],
);
});
});
</script>
隐藏源代码
投影
glsl
#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
uniform float u_heightWidthRatio; // canvas.clientWidth / canvas.clientHeight
void main() {
gl_Position = vec4(a_position, 0.0, 1.0); // gl_Position: clipping space 中的坐标
gl_Position.x *= u_heightWidthRatio; // 修正为 NDC
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
观察上面修正 NDC 的方法,发现其实就是对 Y 轴进行的缩放变换(即投影变换),故此我们可以把这一步骤加到平移、旋转、缩放等仿射变换中。
投影矩阵推导
Translation
Rotation
Scale
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import { vec2, mat3 } from "@public/js/math/tiny-matrix";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform mat3 u_projectionWorldMatrix; // 用于传入投影、平移、旋转和缩放矩阵,即从模型空间转换到世界空间、并转换为设备像素坐标。
void main() {
gl_Position = vec4((u_projectionWorldMatrix*vec3(a_position, 1)).xy, 0, 1);
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_projectionWorld = gl.getUniformLocation(shaderProgram, "u_projectionWorldMatrix");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
updateMatrix(); // 初始化 u_projectionWorldMatrix
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (-0.5, 0.5) (0, 0.5)
* ◉----------------◉
* | · | ·
* | · | ·
* | · |(0,0,0) ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (-0.5, -0.5) (0, -0.5) (0.5, -0.5)
*/
// 顶点数据数组
const vertices: number[] = [
-0.5,
0.5, // 顶点0
-0.5,
-0.5, // 顶点1
0,
-0.5, // 顶点2
0,
0.5, // 顶点3
0.5,
-0.5, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
function updateMatrix(translation?: any, rotation?: any, scale?: any) {
const matrix = new mat3(); // // identity matrix
const heightWidthRatio = canvas3d.height / canvas3d.width;
matrix.scale(new vec2(heightWidthRatio, 1)); // projection matrix
if (translation) {
matrix.translate(new vec2(translation.x, translation.y)); // projection translation matrix
}
if (rotation) {
const angleInDegrees = rotation.z;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
matrix.rotate(angleInRadians); // projection translation rotation matrix
}
if (scale) {
matrix.scale(new vec2(scale.x, scale.y)); // projection translation rotation scale matrix, i.e. projection world matrix
}
gl.uniformMatrix3fv(shaderProgram_location_projectionWorld, false, matrix);
}
// @ts-ignore
dynamicImportScript(["/js/utils/tweakpane.utils.js", "/js/utils/tweakpane-plugin-rotation.min.js"]).then((res) => {
let lastTranslation: any = undefined;
let lastRotation: any = undefined;
let lastScale: any = undefined;
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0, inverted: true },
onChange: (v) => {
lastTranslation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
lastRotation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Scale: {
x: { min: -1, max: 2, value: 1 },
y: { min: -1, max: 2, value: 1, inverted: true },
onChange: (v) => {
lastScale = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Reset() {
pane.reset();
},
};
const TweakPaneUtils = res[0].default ? res[0].default : res[0];
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
res[1],
);
});
});
</script>
隐藏源代码
另一种坐标系
前面的例子中,采用的坐标系是以屏幕中心为原点、Y轴朝上为正数,一般在 2D 中是采用另一种坐标系:
- 屏幕左上角为原点
- Y轴朝下为正数
●————————————————————————> X轴
| 原点(0,0,0)
|
|
|
|
|
v Y轴
采用 2D 常用坐标系:
Translation
Rotation
Scale
vue
<template>
<div style="position: relative">
<canvas ref="refCanvas" style="width: 100%"> Your browser does not support the HTML5 canvas element. </canvas>
<div ref="refDivTweakpane" style="position: absolute; right: 0px; top: 0px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { compileShader, createProgram, resizeCanvasToDisplaySize } from "@public/js/webgl/utils";
import { vec2, mat3 } from "@public/js/math/tiny-matrix";
let refCanvas = ref<HTMLCanvasElement>();
let refDivTweakpane = ref<HTMLDivElement>();
onMounted(() => {
//#region 1)初始化画布和WebGL
const canvas3d = refCanvas.value!;
const gl = canvas3d.getContext("webgl2")!;
resizeCanvasToDisplaySize(canvas3d, window.devicePixelRatio);
//#endregion
//#region 2)设置画布基本参数
gl.clearColor(1, 1, 1, 1); // 设置画布背景为白色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布背景色,并用当前设置的背景色填充
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置视口铺满整个 canvas
//#endregion
//#region 3)创建着色器程序
const vertexShaderSource = `#version 300 es
in vec2 a_position; // 顶点的原始坐标(本地坐标、模型空间坐标)
in vec4 a_color; // 顶点颜色
out vec4 v_color;
uniform mat3 u_projectionWorldMatrix; // 用于传入投影、平移、旋转和缩放矩阵,即从模型空间转换到世界空间、并转换为设备像素坐标。
void main() {
gl_Position = vec4((u_projectionWorldMatrix*vec3(a_position, 1)).xy, 0, 1);
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float; // 设置精度
in vec4 v_color;
out vec4 o_fragColor;
void main() {
o_fragColor = v_color;
}`;
const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = createProgram(gl, vertexShader, fragmentShader);
const shaderProgram_location_position = gl.getAttribLocation(shaderProgram, "a_position");
const shaderProgram_location_color = gl.getAttribLocation(shaderProgram, "a_color");
const shaderProgram_location_projectionWorld = gl.getUniformLocation(shaderProgram, "u_projectionWorldMatrix");
gl.useProgram(shaderProgram); // 绑定着色器程序以供后续绘制时使用
updateMatrix(); // 初始化 u_projectionWorldMatrix
//#endregion
//#region 3)创建顶点和索引缓冲区
/**
* 一共 4 个顶点:顶点0、1、2、3
* 顶点0 顶点3
* (0, 0) (0.5, 0)
* ◉----------------◉
* | · | ·
* | · | ·
* | · | ·
* | · | ·
* | · | ·
* ◉----------------◉----------------◉
* 顶点1 顶点2 顶点4
* (0, 1) (0.5, 1) (1, 1)
*/
// 顶点数据数组
const vertices: number[] = [
0,
0, // 顶点0
0,
1, // 顶点1
0.5,
1, // 顶点2
0.5,
0, // 顶点3
1,
1, // 顶点4
];
// 索引数据数组,逆时针
const indices: number[] = [
0,
1,
2, // Primitive 1
0,
2,
3, // Primitive 2
3,
2,
4, // Primitive 3
];
const colors = [
1.0,
0.0,
0.0, // 顶点0:红
1.0,
0.0,
0.0, // 顶点1:红
1.0,
0.0,
0.0, // 顶点2:红
0.0,
1.0,
0.0, // 顶点3:绿
0.0,
0.0,
1.0, // 顶点4:蓝
];
const vertexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【顶点数据】,所以起名叫 vertexBuffer
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定此 vertexBuffer 到 ARRAY_BUFFER(顶点坐标,纹理坐标数据或顶点颜色数据),此处我们是把 vertexBuffer 绑定为顶点数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 vertexBuffer 上执行
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 把顶点数据(vertices)存放到当前的缓冲区里(即 vertexBuffer)
gl.enableVertexAttribArray(shaderProgram_location_position); // 激活 a_position 顶点属性
const size = 2; // 指定每个顶点是由几个数字组成的,本例是 2,例如 (-0.5, 0.5)
gl.vertexAttribPointer(shaderProgram_location_position, size, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(vertexBuffer)中读取顶点数据。
const indexBuffer = gl.createBuffer(); // 创建一个用于存储数据的缓冲区,我们要用它保存【索引数据】,所以起名叫 indexBuffer
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); // 绑定此 indexBuffer 到 ELEMENT_ARRAY_BUFFER(元素索引数据),此处我们是把 indexBuffer 绑定为索引数据来使用,并且后续 Buffer 相关的 WebGL API 调用都会在 indexBuffer 上执行
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); // 把索引数据(indices)存放到当前的缓冲区里(即 indexBuffer)
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.enableVertexAttribArray(shaderProgram_location_color); // 激活 a_color 顶点属性
const sizePerColor = 3;
gl.vertexAttribPointer(shaderProgram_location_color, sizePerColor, gl.FLOAT, false, 0, 0); // 告诉显卡从当前绑定的缓冲区(colorBuffer)中读取顶点数据。
//#endregion
//#region 4)调用 draw* 函数 → GPU 启动渲染管道绘制图形
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
// 当使用 IBOs 时,需使用 drawElements 而不是 drawArrays
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0); // 把 vertexBuffer 里的数据当做“一系列三角形”来绘制,顶点的顺序由 indices 控制
}
//#endregion
draw();
function updateMatrix(translation?: any, rotation?: any, scale?: any) {
const matrix = new mat3(); // // identity matrix
const heightWidthRatio = canvas3d.height / canvas3d.width;
// matrix.translate(new vec2(-1, 1));
// matrix.scale(new vec2(heightWidthRatio, -1));
matrix.projection(2 / heightWidthRatio, 2); // projection matrix,此步骤等同于上面2个步骤
if (translation) {
matrix.translate(new vec2(translation.x, translation.y)); // projection translation matrix
}
if (rotation) {
const angleInDegrees = rotation.z;
const angleInRadians = (angleInDegrees * Math.PI) / 180;
matrix.rotate(angleInRadians); // projection translation rotation matrix
}
if (scale) {
matrix.scale(new vec2(scale.x, scale.y)); // projection translation rotation scale matrix, i.e. projection world matrix
}
gl.uniformMatrix3fv(shaderProgram_location_projectionWorld, false, matrix);
}
// @ts-ignore
dynamicImportScript(["/js/utils/tweakpane.utils.js", "/js/utils/tweakpane-plugin-rotation.min.js"]).then((res) => {
let lastTranslation: any = undefined;
let lastRotation: any = undefined;
let lastScale: any = undefined;
const paneConfigs = {
Translation: {
x: { min: -1, max: 1, value: 0 },
y: { min: -1, max: 1, value: 0 },
onChange: (v) => {
lastTranslation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Rotation: {
x: { min: 0, max: 0, value: 0 },
y: { min: 0, max: 0, value: 0 },
z: 0,
view: "rotation",
rotationMode: "euler", // optional, 'quaternion' by default
order: "XYZ", // Extrinsic rotation order. optional, 'XYZ' by default
unit: "deg", // or 'rad' or 'turn'. optional, 'rad' by default
// picker: "inline", // or 'popup'. optional, 'popup' by default
// expanded: true, // optional, false by default
onChange: (v) => {
v.z = -v.z; // 此例子采用 2D 坐标
lastRotation = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Scale: {
x: { min: -1, max: 2, value: 1 },
y: { min: -1, max: 2, value: 1 },
onChange: (v) => {
lastScale = v;
updateMatrix(lastTranslation, lastRotation, lastScale);
draw();
},
},
Reset() {
pane.reset();
},
};
const TweakPaneUtils = res[0].default ? res[0].default : res[0];
const pane = TweakPaneUtils.createTweakPane(
paneConfigs,
{
title: "参数",
container: refDivTweakpane.value,
// expanded: false,
},
res[1],
);
});
});
</script>
隐藏源代码