Как я могу нарисовать и проецировать видео из проигрывателя HTML5 на 3D-плоскость в WebGL?

Я использую Unity для создания своего приложения для WebGL и обнаружил, что мне нужно захватить видео из проигрывателя HTML и нарисовать его на плоскости в трехмерном пространстве.

Я знаю, что вы можете вызвать drawImage() на CanvasRenderingContext2D и передать ссылку на видеоплеер, и текущий кадр будет нарисован на холсте при запуске функции.

Ближайший 3D-эквивалент, который мне удалось найти для этой функции, — это WebGL2RenderingContext.texImage3D(). Однако я не совсем понимаю, как это работает, и когда я попытался протестировать его, я получил следующее исключение: Uncaught DOMException: The operation is insecure. Я использовал свой собственный, локальный видеофайл, поэтому он не может быть CORS, но я не знаю, что Это.

Вы можете увидеть тестовый проект в этом репозитории GitHub.


Я нашел аналогичный вопрос здесь, но, к сожалению, ответы показывают, как рисовать то, что я Предположим, это предварительно загруженные текстуры. Я не знаю, как получить их из видеоплеера и передать их, или достаточно ли это быстро, чтобы делать это в каждом кадре.


Чтобы дать некоторый контекст, я пытаюсь показать прямую трансляцию HLS внутри моего приложения Unity/WebGL. Я мог бы загрузить сегменты видео .ts (MPEG-2 Transport Stream) и поставить их в очередь в связный видеопоток, но встроенный видеопроигрыватель Unity не поддерживает этот формат.

В качестве решения я подумал, что могу захватить видео в проигрывателе HTML5 (используя hls.js при необходимости) и внедрять текстуру в приложение WebGL с помощью JavaScript в каждом кадре.

Unity позволяет вам запускать код JavaScript из своих сценариев C#, поэтому время, вероятно, не будет проблемой, равно как и получение мирового масштаба/местоположения целевой плоскости. Мне просто нужно написать функцию JavaScript, чтобы как-то нарисовать текстуру.

Вот мой текущий код:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL</title>
    <script src="https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js"></script>
    <style>
        body {
            background-color: aquamarine;
        }
    </style>
</head>
<body>
    <video muted autoplay width="480" height="270">
        <source src="./test.mp4" type="video/mp4" />
    </video>
    <br>
    <canvas width="1080" height="720"></canvas>
    <button onclick="takeScreenshot()">Capture</button>
    
    <script>
        function takeScreenshot() {
            var video = document.querySelector("video");
            var canvas = document.querySelector("canvas");

            var gl = canvas.getContext("webgl2");

            gl.texImage3D(
                gl.TEXTURE_3D,    // target (enum)
                0,                // level of detail
                gl.RGBA,          // internalFormat
                1920,             // width of texture
                1080,             // height of texture
                1,                // depth
                0,                // border
                gl.RGBA,          // format
                gl.UNSIGNED_BYTE, // type
                video,            // source
            );
        }
    </script>
</body>
</html>

person verified_tinker    schedule 17.06.2021    source источник
comment
В коде тестового проекта, поскольку вы помещаете html и видео в одно и то же место, вы должны установить свой источник как src="test.mp4", а не как src="./test.mp4". У меня это сработало, и ваш код в порядке (проверено в Chrome).   -  person VC.One    schedule 17.06.2021
comment
@VC.Странно. Я изменил его, как вы сказали, но он все еще не работает. Chrome выдает более подробное сообщение об ошибке: DOMException: Failed to execute 'texImage3D' on 'WebGL2RenderingContext': The video element contains cross-origin data, and may not be loaded. Но это явно не так! Изменить: размещение на localhost устранило эту проблему. Однако по-прежнему не работает: (index):30 WebGL: INVALID_OPERATION: texImage3D: no texture bound to target Вы тоже не сталкивались с этой проблемой?   -  person verified_tinker    schedule 18.06.2021
comment
Да мой плохой. Я был сонным и помню, как менял источник. Я думаю, что видел вывод тега видео и отсутствие предупреждений / всплывающих окон компилятора графического процессора и сонно предположил, что Здесь нет проблем, дело решено. Извини. Я подготовил для вас полный и проверяемый пример (отредактированный из одного из моих проектов)   -  person VC.One    schedule 18.06.2021
comment
PPS: проверьте код на локальном хосте или используйте онлайн-ссылку MP4, чтобы избежать проблем с безопасностью при рисовании.   -  person VC.One    schedule 18.06.2021


Ответы (1)


Вот пример кода для настройки объекта webGL, который может получать ваши видеопиксели. Надеюсь, это полезно для вас.

По сути, вы создаете форму прямоугольника или прямоугольника, используя два треугольника, а затем проецируете видео на этот прямоугольник.

    0-------1
    |       |
    3-------2

//# two sets of... connected 3-points of a triangle
var vertexIndices = [ 0,  1,  2,      0,  2,  3, ];

В приведенном ниже примере кода также создаются некоторые необходимые шейдеры и программы графического процессора. Поэкспериментируйте с ним

Если кто-то из программистов просто хочет использовать пиксельные эффекты графического процессора, напишите свой код эффектов в шейдере фрагментов.
(см. часть кода по адресу: //# example of basic colour effect).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL</title>
    
    <!--
    <script src="https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js"></script>
    -->
    
    <style> body {background-color: aquamarine; } </style>  
</head>

<body>
    <video id="myVideo" controls muted autoplay width="480" height="270">
    <source src="video.mp4" type="video/mp4" />
    </video>
    <br>
    
    <button id="capture" onclick="takeScreenshot()"> Capture </button>
    <br><br>
    
    <!--
    <canvas id="myCanvas" width="1080" height="720"></canvas>
    -->
    
     <canvas id="myCanvas" width="480" height="270"></canvas>
    
<!-- ########## Shader code ###### -->
<!-- ### Shader code here -->


<!-- Fragment shader program -->
<script id="shader-fs" type="x-shader/x-fragment">

//<!-- //## code for pixel effects goes here if needed -->

//# these two vars will access 
varying mediump vec2 vDirection;
uniform sampler2D uSampler;

void main(void) 
{
    //# get current video pixel's color (no FOR-loops needed like in JS Canvas)
    gl_FragColor = texture2D(uSampler, vec2(vDirection.x * 0.5 + 0.5, vDirection.y * 0.5 + 0.5));
    
    /*
    //# example of basic colour effect
    gl_FragColor.r = ( gl_FragColor.r * 1.15 );
    gl_FragColor.g = ( gl_FragColor.g * 0.8 );
    gl_FragColor.b = ( gl_FragColor.b * 0.45 );
    */
}

</script>


<!-- Vertex shader program -->
<script id="shader-vs" type="x-shader/x-vertex">

    attribute mediump vec2 aVertexPosition;
    varying mediump vec2 vDirection;

    void main( void ) 
    {
        gl_Position = vec4(aVertexPosition, 1.0, 1.0) * 2.0;
        vDirection = aVertexPosition;
    }
    


</script>



<!-- ### END Shader code... -->

    
<script>

//# WebGL setup

var video = document.getElementById('myVideo');

const glcanvas = document.getElementById('myCanvas');
const gl = ( ( glcanvas.getContext("webgl") ) || ( glcanvas.getContext("experimental-webgl") ) );

//# check if WebGL is available..
if (gl && gl instanceof WebGLRenderingContext) { console.log( "WebGL is available"); }
else { console.log( "WebGL is NOT available" ); } //# use regular JS canvas functions if this happens...


//# create and attach the shader program to the webGL context
var attributes, uniforms, program;

function attachShader( params ) 
{
    fragmentShader = getShaderByName(params.fragmentShaderName);
    vertexShader = getShaderByName(params.vertexShaderName);

    program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) 
    { alert("Unable to initialize the shader program: " + gl.getProgramInfoLog(program)); }

    gl.useProgram(program);

    // get the location of attributes and uniforms
    attributes = {};

    for (var i = 0; i < params.attributes.length; i++) 
    {
        var attributeName = params.attributes[i];
        attributes[attributeName] = gl.getAttribLocation(program, attributeName);
        gl.enableVertexAttribArray(attributes[attributeName]);
    }
        
    uniforms = {};

    for (i = 0; i < params.uniforms.length; i++) 
    {
        var uniformName = params.uniforms[i];
        uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
        
        gl.enableVertexAttribArray(attributes[uniformName]);
    }
    
}

function getShaderByName( id ) 
{
    var shaderScript = document.getElementById(id);

    var theSource = "";
    var currentChild = shaderScript.firstChild;

    while(currentChild) 
    {
        if (currentChild.nodeType === 3) { theSource += currentChild.textContent; }
        currentChild = currentChild.nextSibling;
    }

    var result;
    
    if (shaderScript.type === "x-shader/x-fragment") 
    { result = gl.createShader(gl.FRAGMENT_SHADER); } 
    else { result = gl.createShader(gl.VERTEX_SHADER); }
    
    gl.shaderSource(result, theSource);
    gl.compileShader(result);

    if (!gl.getShaderParameter(result, gl.COMPILE_STATUS)) 
    {
        alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(result));
        return null;
    }
    return result;
}

//# attach shader
attachShader({
fragmentShaderName: 'shader-fs',
vertexShaderName: 'shader-vs',
attributes: ['aVertexPosition'],
uniforms: ['someVal', 'uSampler'],
});

// some webGL initialization
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clearDepth(1.0);
gl.disable(gl.DEPTH_TEST);

positionsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionsBuffer);
var positions = [
  -1.0, -1.0,
   1.0, -1.0,
   1.0,  1.0,
  -1.0,  1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

var vertexColors = [0xff00ff88,0xffffffff];
    
var cBuffer = gl.createBuffer();

verticesIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, verticesIndexBuffer);

var vertexIndices = [ 0,  1,  2,      0,  2,  3, ];

gl.bufferData(  
                gl.ELEMENT_ARRAY_BUFFER,
                new Uint16Array(vertexIndices), gl.STATIC_DRAW
            );

texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

//# must be LINEAR to avoid subtle pixelation (double-check this... test other options like NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

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.bindTexture(gl.TEXTURE_2D, null);

// update the texture from the video
updateTexture = function() 
{
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    
    //# next line fails in Safari if input video is NOT from same domain/server as this html code
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB,
    gl.UNSIGNED_BYTE, video);
    gl.bindTexture(gl.TEXTURE_2D, null);
};

</script>

<script>


//# Vars for video frame grabbing when system/browser provides a new frame
var requestAnimationFrame = (window.requestAnimationFrame || window.mozRequestAnimationFrame ||
                            window.webkitRequestAnimationFrame || window.msRequestAnimationFrame);

var cancelAnimationFrame = (window.cancelAnimationFrame || window.mozCancelAnimationFrame);

///////////////////////////////////////////////


function takeScreenshot( ) 
{
    //# video is ready (can display pixels)
    if( video.readyState >= 3 )
    {
        updateTexture(); //# update pixels with current video frame's pixels...
        
        gl.useProgram(program); //# apply our program
    
        gl.bindBuffer(gl.ARRAY_BUFFER, positionsBuffer);
        gl.vertexAttribPointer(attributes['aVertexPosition'], 2, gl.FLOAT, false, 0, 0);
        
        //# Specify the texture to map onto the faces.
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.uniform1i(uniforms['uSampler'], 0);
        
        //# Draw GPU
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, verticesIndexBuffer);
        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
    }
    
    //# re-capture the next frame... basically the function loops itself
    //# consider adding event listener for video pause to set value as... cancelAnimationFrame( takeScreenshot ); 
    requestAnimationFrame( takeScreenshot ); 
    
}

//////////////////////////////////////

function takeScreenshot_old() 
{
    var gl = canvas.getContext("webgl2");

    gl.texImage3D(
        gl.TEXTURE_3D,    // target (enum)
        0,                // level of detail
        gl.RGBA,          // internalFormat
        1920,             // width of texture
        1080,             // height of texture
        1,                // depth
        0,                // border
        gl.RGBA,          // format
        gl.UNSIGNED_BYTE, // type
        video,            // source
    );
}


    </script>
</body>
</html>
person VC.One    schedule 18.06.2021
comment
Вау здорово. Гораздо больше, чем я ожидал. Как это отразится на проецировании в 3D-пространство? Я возился с переменной positions — добавлял третье измерение — и соответствующим образом корректировал код шейдера, но все, что я сделал, это нарисовал 1 треугольник вместо 2. Кроме того, в строке 130, возможно, вы имели в виду написать uniforms[uniformName] вместо attributes[uniformName]? - person verified_tinker; 21.06.2021
comment
Вы имеете в виду, например, что хотите, чтобы видео проецировалось на куб? Я никогда этого не пробовал. В моем проекте просто использовался 2D-бокс, потому что я делал тяжелые пиксельные эффекты для видео (в Canvas это занимало 4 секунды на каждый кадр, но на GPU это плавное воспроизведение). Посмотрите, дает ли это руководство советы по текстурированию в 3D-пространстве. . Для некубической формы см. другое их руководство. PS: Объясните проецирование в 3D-пространство, и я посмотрю, что можно попробовать. - person VC.One; 21.06.2021
comment
Спасибо, я проверю их. Как я уже сказал, у меня есть существующее приложение WebGL, экспортированное игровым движком Unity. Но видеоплеер Unity не поддерживает тот тип файлов, который мне нужен, поэтому я подумал, что могу использовать для этого плеер браузера, получить данные текстуры и каким-то образом передать их приложению. Я подумал, что могу создать в приложении четырехугольник, приклеенный к стене, из кода JS и передать ему текстуру видео. Или, может быть, если возможно, я мог бы создать доступное поле/переменную в памяти приложения, если WebGL поддерживает это, и писать в него из браузера и читать из приложения/игры. - person verified_tinker; 21.06.2021
comment
Я подумал, что могу создать в приложении четырехугольник, приклеенный к стене, из кода JS и передать ему текстуру видео. Лучшее объяснение: представьте себе телевизор, закрепленный на стене в 3D-комнате. Мне нужно воспроизвести видео на этом телевизоре. Комната уже существует, как и телевизор. Но мне нужно использовать проигрыватель браузера, чтобы понять данные в видеофайле, а затем каким-то образом передать текстуру каждого кадра видео на дисплей телевизора. Поскольку комната трехмерна, дисплей — четырехъядерный — должен иметь местоположение, масштаб и ориентацию в мировом пространстве. - person verified_tinker; 21.06.2021