В настоящее время у меня есть преобразование CSS matrix3d
, которое мне нравится, скажем, например:
transform: matrix3d(1.3453,0.1357,0.0,0.0003,0.2096,1.3453,0.0,0.0003,0.0,0.0,1.0,0.0,-100.0,-100.0,0.0,1.0);
transform-origin: 0 0;
Я использую холст WebGL2 и хотел бы, чтобы изображение, которое я на нем нарисовал, имело такое же преобразование.
Я считаю, что для этого мне понадобится вершинный шейдер, который возьмет матрицу и умножит ее:
#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
uniform vec2 u_resolution;
out vec2 v_texCoord;
uniform mat4 u_matrix;
void main() {
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = u_matrix*vec4(clipSpace * vec2(1, -1), 0, 1);
v_texCoord = a_texCoord;
}
Затем я могу передать матрицу шейдеру в моем коде JavaScript:
const matrixLocation = gl.getUniformLocation(program, "u_matrix");
gl.uniformMatrix4fv(matrixLocation, false, new Float32Array(matrix));
Сейчас я застрял в выяснении того, каким должно быть значение matrix
в моем коде. Я пытался прочитать о том, как устроено преобразование matrix3d
, и на основе этого перестроить матрицу, но безуспешно.
Как я могу использовать здесь преобразование matrix3d
в шейдере WebGL2?
Обновлено: полный рабочий пример добавлен по запросу в комментариях.
// Set up matrices
// Based on: matrix3d(1.3453,0.1357,0.0,0.0003,0.2096,1.3453,0.0,0.0003,0.0,0.0,1.0,0.0,-100.0,-100.0,0.0,1.0)
const cssMatrix = new Float32Array([1.3453, 0.1357, 0.0, 0.0003, 0.2096, 1.3453, 0.0, 0.0003, 0.0, 0.0, 1.0, 0.0, -100.0, -100.0, 0.0, 1.0]);
const identityMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
// identityMatrix works correctly while the cssMatrix does not
const matrixArray = cssMatrix;
// const matrixArray = identityMatrix;
// Set up shaders
const vertexShaderSource = `#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
uniform vec2 u_resolution;
uniform mat4 u_matrix;
out vec2 v_texCoord;
void main() {
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = u_matrix*vec4(clipSpace * vec2(1, -1), 0,1);
v_texCoord = a_texCoord;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float;
uniform sampler2D u_image;
in vec2 v_texCoord;
out vec4 outColor;
void main() {
outColor = texture(u_image, v_texCoord);
}`;
// Load the test image
const img = document.getElementById("image");
img.src = "https://i.imgur.com/NIt86ft.png";
img.onload = () => renderImg(img);
// Rest of this code is boilerplate based off of
// https://webgl2fundamentals.org/webgl/lessons/webgl-image-processing.html
function createProgram(
gl, shaders, opt_attribs, opt_locations, opt_errorCallback) {
const errFn = opt_errorCallback || console.error;
const program = gl.createProgram();
shaders.forEach(function(shader) {
gl.attachShader(program, shader);
});
if (opt_attribs) {
opt_attribs.forEach(function(attrib, ndx) {
gl.bindAttribLocation(
program,
opt_locations ? opt_locations[ndx] : ndx,
attrib);
});
}
gl.linkProgram(program);
// Check the link status
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
// something went wrong with the link
const lastError = gl.getProgramInfoLog(program);
errFn('Error in program linking:' + lastError);
gl.deleteProgram(program);
return null;
}
return program;
}
function loadShader(gl, shaderSource, shaderType, opt_errorCallback) {
const errFn = opt_errorCallback || console.error;
// Create the shader object
const shader = gl.createShader(shaderType);
// Load the shader source
gl.shaderSource(shader, shaderSource);
// Compile the shader
gl.compileShader(shader);
// Check the compile status
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
// Something went wrong during compilation; get the error
const lastError = gl.getShaderInfoLog(shader);
console.error('*** Error compiling shader \'' + shader + '\':' + lastError + `\n` + shaderSource.split('\n').map((l, i) => `${i + 1}: ${l}`).join('\n'));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgramFromSources(
gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) {
const shaders = [];
const defaultShaderType = [
'VERTEX_SHADER',
'FRAGMENT_SHADER',
];
for (let ii = 0; ii < shaderSources.length; ++ii) {
shaders.push(loadShader(
gl, shaderSources[ii], gl[defaultShaderType[ii]], opt_errorCallback));
}
return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
}
function resizeCanvasToDisplaySize(canvas, multiplier) {
multiplier = multiplier || 1;
const width = canvas.clientWidth * multiplier | 0;
const height = canvas.clientHeight * multiplier | 0;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
return true;
}
return false;
}
function renderImg(image) {
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");
if (!gl)
return;
var program = createProgramFromSources(gl, [vertexShaderSource, fragmentShaderSource]);
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
var imageLocation = gl.getUniformLocation(program, "u_image");
var matrixLocation = gl.getUniformLocation(program, "u_matrix");
// Create a vertex array object (attribute state)
var vao = gl.createVertexArray();
// and make it the one we're currently working with
gl.bindVertexArray(vao);
// Create a buffer and put a single pixel space rectangle in
// it (2 triangles)
var positionBuffer = gl.createBuffer();
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
// Turn on the attribute
gl.enableVertexAttribArray(texCoordAttributeLocation);
// Tell the attribute how to get data out of texCoordBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
texCoordAttributeLocation, size, type, normalize, stride, offset);
// Create a texture.
var texture = gl.createTexture();
// make unit 0 the active texture uint
// (ie, the unit all other texture commands will affect
gl.activeTexture(gl.TEXTURE0 + 0);
// Bind it to texture unit 0's 2D bind point
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we don't need mips and so we're not filtering
// and we don't repeat at the edges.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
var mipLevel = 0; // the largest mip
var internalFormat = gl.RGBA; // format we want in the texture
var srcFormat = gl.RGBA; // format of data we are supplying
var srcType = gl.UNSIGNED_BYTE; // type of data we are supplying
gl.texImage2D(gl.TEXTURE_2D,
mipLevel,
internalFormat,
srcFormat,
srcType,
image);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
setRectangle(gl, 0, 0, image.width, image.height);
resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
gl.uniform1i(imageLocation, 0);
gl.uniformMatrix4fv(matrixLocation, false, matrixArray);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
#image {
transform: matrix3d(1.3453, 0.1357, 0.0, 0.0003, 0.2096, 1.3453, 0.0, 0.0003, 0.0, 0.0, 1.0, 0.0, -100.0, -100.0, 0.0, 1.0);
transform-origin: 0 0;
}
img,
canvas {
border: 1px solid #000;
}
<img src = "" crossOrigin = "" id = "image"><br>
<canvas id = "canvas" width=320 height=240></canvas>
CSS-матрица3d — это обычная однородная матрица размером 4x4, элементы которой упорядочены по строкам, поэтому в вашем случае строка 1 — это 1.3453, 0.1357, 0.0, 0.0003
, строка 2 — 0.2096, 1.3453, 0.0, 0.0003
, строка 3 — 0.0, 0.0, 1.0, 0.0
и строка 4 — -100.0, -100.0, 0.0, 1.0
.
Преобразовать это в массив float32 довольно просто: вы можете либо работать со строкой CSS, измельчить ее, чтобы преобразовать числа в действительные числа, и «привести» их к массиву float32, либо воспользоваться преимуществом DOMMatrix объект и заставьте его выполнить преобразование за вас.
Например.
const matrixFromCSS = `matrix3d(.....)`;
const matrixAsFloat32Array = new Float32Array(
matrixFromCSS
.replace(`matrix3d(`,``)
.split(`,`)
.map(v => parseFloat(v)
)
);
или
const matrixFromCSS = `matrix3d(.....)`;
const matrixAsDOMMatrix = new DOMMatrix(matrixFromCSS);
const matrixAsFloat32Array = matrixAsDOMMatrix.toFloat32Array();
Тогда я настоятельно рекомендую вам написать небольшое доказательство концепции, которое вы можете включить в свой пост в виде работоспособного фрагмента — на самом деле для этого нужен только div с CSS-трансформацией, элемент холста и достаточно JS для настройки Canvas для WebGL, который копирует полученное вами CSS-преобразование getComputedStyle
Добавил в пост доказательство концепции. Он корректно работает с единичной матрицей, но не с CSS. Пример должен быть довольно длинным, поскольку для webgl2 требуется много шаблонов — я не думаю, что его можно сделать намного короче.
Я просто экспериментировал, а не искал в документации правильные математические вычисления, так что на самом деле это может не сработать, но для моего теста это работает. 😅
В этом коде есть множество допущений, например, предположение, что данные вершин находятся в пикселях (потому что это то, что у вас было), и поэтому это необходимо изменить, если изменится размер изображения.
Другая проблема, с которой вы столкнетесь, заключается в том, что 3d css выталкивает элемент из обычного прямоугольника, который элемент будет заполнять, но WebGL не может рисовать за пределами холста.
Я также удалил transform-origin:
, потому что это добавило бы математики. То же самое можно сказать и о perspective:
или любых других преобразованиях в CSS.
Одно отличие от кода в вашем вопросе. Этот код умножал проецируемые положения вершин на матрицу (после деления на разрешение, умножения на 2 и вычитания 1).
Этот код упростил шейдер, просто умножив на одну матрицу, без каких-либо других математических вычислений в шейдере, как описано в этой статье о матричной математике
// Set up matrices
// Based on: matrix3d(1.3453,0.1357,0.0,0.0003,0.2096,1.3453,0.0,0.0003,0.0,0.0,1.0,0.0,-100.0,-100.0,0.0,1.0)
const cssMatrix = new Float32Array([
1.3453, 0.1357, 0.0, 0.0003,
0.2096, 1.3453, 0.0, 0.0003,
0.0, 0.0, 1.0, 0.0,
-100.0, -100.0, 0.0, 1.0,
]);
const identityMatrix = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
// identityMatrix works correctly while the cssMatrix does not
const matrixArray = cssMatrix;
//const matrixArray = identityMatrix;
// Set up shaders
const vertexShaderSource = `#version 300 es
in vec4 a_position;
in vec2 a_texCoord;
uniform mat4 u_matrix;
out vec2 v_texCoord;
void main() {
gl_Position = u_matrix * a_position;
v_texCoord = a_texCoord;
}`;
const fragmentShaderSource = `#version 300 es
precision highp float;
uniform sampler2D u_image;
in vec2 v_texCoord;
out vec4 outColor;
void main() {
outColor = texture(u_image, v_texCoord);
}`;
// Load the test image
const img = document.getElementById("image");
img.src = "https://i.imgur.com/NIt86ft.png";
img.onload = () => renderImg(img);
// Rest of this code is boilerplate based off of
// https://webgl2fundamentals.org/webgl/lessons/webgl-image-processing.html
function createProgram(
gl, shaders, opt_attribs, opt_locations, opt_errorCallback) {
const errFn = opt_errorCallback || console.error;
const program = gl.createProgram();
shaders.forEach(function(shader) {
gl.attachShader(program, shader);
});
if (opt_attribs) {
opt_attribs.forEach(function(attrib, ndx) {
gl.bindAttribLocation(
program,
opt_locations ? opt_locations[ndx] : ndx,
attrib);
});
}
gl.linkProgram(program);
// Check the link status
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
// something went wrong with the link
const lastError = gl.getProgramInfoLog(program);
errFn('Error in program linking:' + lastError);
gl.deleteProgram(program);
return null;
}
return program;
}
function loadShader(gl, shaderSource, shaderType, opt_errorCallback) {
const errFn = opt_errorCallback || console.error;
// Create the shader object
const shader = gl.createShader(shaderType);
// Load the shader source
gl.shaderSource(shader, shaderSource);
// Compile the shader
gl.compileShader(shader);
// Check the compile status
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
// Something went wrong during compilation; get the error
const lastError = gl.getShaderInfoLog(shader);
console.error('*** Error compiling shader \'' + shader + '\':' + lastError + `\n` + shaderSource.split('\n').map((l, i) => `${i + 1}: ${l}`).join('\n'));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgramFromSources(
gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) {
const shaders = [];
const defaultShaderType = [
'VERTEX_SHADER',
'FRAGMENT_SHADER',
];
for (let ii = 0; ii < shaderSources.length; ++ii) {
shaders.push(loadShader(
gl, shaderSources[ii], gl[defaultShaderType[ii]], opt_errorCallback));
}
return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
}
function resizeCanvasToDisplaySize(canvas, multiplier) {
multiplier = multiplier || 1;
const width = canvas.clientWidth * multiplier | 0;
const height = canvas.clientHeight * multiplier | 0;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
return true;
}
return false;
}
function renderImg(image) {
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");
if (!gl)
return;
var program = createProgramFromSources(gl, [vertexShaderSource, fragmentShaderSource]);
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
var imageLocation = gl.getUniformLocation(program, "u_image");
var matrixLocation = gl.getUniformLocation(program, "u_matrix");
// Create a vertex array object (attribute state)
var vao = gl.createVertexArray();
// and make it the one we're currently working with
gl.bindVertexArray(vao);
// Create a buffer and put a single pixel space rectangle in
// it (2 triangles)
var positionBuffer = gl.createBuffer();
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
// Turn on the attribute
gl.enableVertexAttribArray(texCoordAttributeLocation);
// Tell the attribute how to get data out of texCoordBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
texCoordAttributeLocation, size, type, normalize, stride, offset);
// Create a texture.
var texture = gl.createTexture();
// make unit 0 the active texture uint
// (ie, the unit all other texture commands will affect
gl.activeTexture(gl.TEXTURE0 + 0);
// Bind it to texture unit 0's 2D bind point
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we don't need mips and so we're not filtering
// and we don't repeat at the edges.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
var mipLevel = 0; // the largest mip
var internalFormat = gl.RGBA; // format we want in the texture
var srcFormat = gl.RGBA; // format of data we are supplying
var srcType = gl.UNSIGNED_BYTE; // type of data we are supplying
gl.texImage2D(gl.TEXTURE_2D,
mipLevel,
internalFormat,
srcFormat,
srcType,
image);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
setRectangle(gl, 0, 0, image.width, image.height);
function render(time) {
time *= 0.001; // convert to seconds
resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
gl.uniform1i(imageLocation, 0);
// compute some CSSMatrix
const m = m4.perspective(
10 * Math.PI / 180,
canvas.clientWidth / canvas.clientHeight,
10, 500);
const cm = m4.lookAt(
[0, 0, 50], // camera
[0, 0, 0], // target
[0, 1, 0], // up
);
const view = m4.inverse(cm);
m4.multiply(m, view, m);
m4.zRotate(m, time, m);
m4.yRotate(m, Math.sin(time) * 0.25, m);
// Convert the CSSMatrix to WebGL?
// Note: There's an assumption here that the vertex data
// is in pixels which is a poor assumption IMO. It would be better,
// at least for the simple case, to use a unit square around the origin
const mat = m4.identity();
m4.orthographic(0, canvas.width, canvas.height, 0, -100, 100, mat);
m4.translate(mat, img.width / 2, img.height / 2, 0, mat);
m4.multiply(mat, m, mat);
m4.translate(mat, -img.width / 2, -img.height / 2, 0, mat);
gl.uniformMatrix4fv(matrixLocation, false, mat);
img.style.transform = `matrix3d(${[...m].join(',')})`;
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
#image {
}
img,
canvas {
border: 1px solid #000;
}
<img src = "" crossOrigin = "" id = "image"><br>
<canvas id = "canvas" width=320 height=240></canvas>
<script src = "https://webgl2fundamentals.org/webgl/resources/m4.js"></script>
Приведенный выше код не использует вашу матрицу, но если вы закомментируете весь код, который управляет m
, и просто измените его на const m = matrixArray
, вы увидите, что они совпадают.
Простое преобразование, похоже, не работает, по крайней мере, не так, как я пытаюсь это сделать в посте. Результаты сильно отличаются от преобразования CSS Matrix3d, и именно эту часть я сейчас пытаюсь выяснить в своем вопросе.