4 votes

Comment afficher un fragment shader GLSL dans un canvas WebGL dans angular (9) ?

C'est la première fois que je travaille sur un projet construit avec angular, donc je suis encore en train de m'habituer à un bon nombre de pratiques spécifiques à ce dernier et à WebPack.

Je cherche à charger un fragment shader personnalisé (.frag/.glsl) dans un canevas plein écran pour l'utiliser comme arrière-plan d'un composant. Pour les maquettes précédentes ou d'autres projets n'utilisant pas angular, j'ai réussi à le faire facilement avec l'aide de bibliothèques comme GlslCanvas J'ai réussi à mettre en place un quadri-écran et les uniformes de base, mais maintenant j'ai du mal à comprendre les erreurs qui me sont envoyées lorsque j'essaie de construire mon application angulaire.

Après de nombreuses heures de navigation, j'ai trouvé comment importer avec succès mon code de shaders, en utilisant Chargeur de shaders GLSL et en ajoutant une configuration webpack personnalisée via @angular-devkit/build-angular et @angular-builder/custom-webpacks :

# my-custom-webpack.config.js

module.exports = {
    module: {
        rules: [{
            test: /\.(frag|vert|glsl)$/,
            use: [
                { 
                loader: 'glsl-shader-loader',
                options: {}  
                }
            ]
        }]
    }
}

J'ai également appris que je devais empêcher typescript de se plaindre de l'existence de modules non-ts lors de l'importation en définissant les déclarations nécessaires, c'est-à-dire :

# my-declarations.d.ts

declare module '*.glsl';
declare module '*.frag';
declare module '*.vert';

À ce stade, le code du fragment shader est correctement importé (du moins je le pense) et je peux l'enregistrer ou l'imprimer (avec la commande {{ myShaderCode }} par exemple) :

# glsl-bg.component.ts

import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import frag from './myShader.frag';
import * as GlslCanvas from 'glslCanvas'

@Component({
  selector: 'app-glsl-bg',
  templateUrl: './glsl-bg.component.html',
  styleUrls: ['./glsl-bg.component.css']
})
export class GlslBgComponent implements OnInit {

  @ViewChild('bgCanvas', {static: true})
  public bgCanvas: ElementRef<HTMLCanvasElement>;

  myShaderCode = frag;

  constructor() { }

  ngOnInit() {
    console.log(this.myShaderCode);
  }

}

Mais voici où je suis bloqué : J'ai essayé d'utiliser diverses bibliothèques de lumière (pas de gros truc comme three.js) sans succès pour exécuter le code shader dans le canevas.

Lors de la construction avec glslCanvas, la compilation est réussie, mais rien ne s'affiche dans le canevas et j'obtiens ceci dans la console :

ERROR TypeError: glslCanvas__WEBPACK_IMPORTED_MODULE_2__ is not a constructor

Alors que si j'utilise glsl-canvas-js (un portage ts du premier), il n'arrive pas à compiler, me donnant ce log :

ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './buffers' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './common' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './context' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './iterable' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './logger' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './subscriber' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './textures' in '[...]\node_modules\glsl-canvas-js\dist'
ERROR in ./node_modules/glsl-canvas-js/dist/glsl-canvas.js
Module not found: Error: Can't resolve './uniforms' in '[...]\node_modules\glsl-canvas-js\dist'

* [...] = full paths removed for simplification

Toute aide ou indication serait grandement appréciée !

0voto

tfrei Points 139

Je ne suis pas un développeur Angular, mais voici un réglage Typescript/Webpack de https://darvin.dev Modèle Webpack Boilerplate :

Chargeur GLSL pour Webpack :

// https://github.com/unic/darvin-webpack-boilerplate/blob/master/webpack/settings/addon-glsl/index.js
// setting with raw-loader and glslify
rules: [
  {
    test: /\.(glsl|frag|vert)$/,
    exclude: /node_modules/,
    use: [
      'raw-loader',
      {
        loader: 'glslify-loader',
        options: {
          transform: [
            ['glslify-hex', { 'option-1': true, 'option-2': 42 }]
          ]
        }
      }
    ]
  }
]

Je recommanderais de créer une instance webgl en dehors des frameworks réactifs et de l'appeler via pubsub, ainsi vous n'aurez pas de polling supplémentaire. Voici un exemple de helper avec une gestion performante du temps, vous le trouverez dans Darvin 2.0 en tant qu'exemple de démonstration.

/**
 * @author tobias.frei@unic.com
 *
 * @module glsl uniform demo
 * 
 * https://github.com/unic/darvin-webpack-boilerplate/blob/master/.cli/.preview/.demo/.templates/.njk/templates/modules/m03-background/index.ts
 */

const Tweakpane = require('tweakpane');

// @ts-ignore
import vertexWobble from '@scripts/glsl/demo.glsl.vert';
// @ts-ignore
import fragmentWobble from '@scripts/glsl/demo.glsl.frag';

// Parameter object
let PARAMS: any;

const DEFINE_FPS = 35;
const DEFINE_RES = 800;
const DEFINE_RES2 = 800;
const deviceRatio = 1;

const resX = DEFINE_RES * deviceRatio,
      resY = DEFINE_RES2 * deviceRatio,
      verts = [-1, 1, -1, -1, 1, -1, 1, 1];

let canvas: HTMLCanvasElement,
    gl: WebGLRenderingContext,
    fpsInterval: number,
    twodContext: CanvasRenderingContext2D,
    now: DOMHighResTimeStamp,
    then: DOMHighResTimeStamp,
    elapsed: DOMHighResTimeStamp,
    resFrame1: Promise<string>;

const imageDatas: ImageData[] = [],
      textures: any[] = [],
      textureLocationDarvin: WebGLUniformLocation[] | any[] = [];

// webgl uniforms
let pos: any,
    program: WebGLProgram,
    buffer: any,
    ut: WebGLUniformLocation | null,
    ures: WebGLUniformLocation | null,
    ucenter: WebGLUniformLocation | null,
    ushake: WebGLUniformLocation | null,
    upulse: WebGLUniformLocation | null,
    ublink: WebGLUniformLocation | null,
    ulight: WebGLUniformLocation | null;

const createShader = (type: number, source: string) => {
    const shader = gl.createShader(type);

    if (!shader || !source) {
      console.error('> cannot create shader');
      return;
    }

    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!success) {
      gl.deleteShader(shader);
      return false;
    }
    return shader;
  },

  createProgram = (vertexShaderString: string, fragmentShaderString: string) => {
    // Setup Vertext/Fragment Shader functions
    const vertexShader = createShader(gl.VERTEX_SHADER, vertexShaderString);
    const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentShaderString);

    // Setup Program and Attach Shader functions
    const newProgram: WebGLProgram | null = gl.createProgram();

    if (newProgram && vertexShader && fragmentShader) {
      gl.attachShader(newProgram, vertexShader);
      gl.attachShader(newProgram, fragmentShader);
      gl.linkProgram(newProgram);
    } else {
      console.error('#dv> webgl program error');
    }

    return newProgram;
  },

  createGraphics = (vertexShader: string | null, fragmentShader: string | null) => {
    if (!vertexShader || !fragmentShader) {
      console.error('> shader missing');
      return;
    }

    createTextureObject(imageDatas);

    // Create the Program //
    const newProgram = createProgram(vertexShader, fragmentShader);
    if (!newProgram) {
      console.error('#dv> webgl create graphics error');
      return;
    }
    program = newProgram;

    // Create and Bind buffer //
    buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array(verts),
      gl.STATIC_DRAW
    );

    pos = gl.getAttribLocation(program, 'pos');

    gl.vertexAttribPointer(
      pos,
      2,              // size: 2 components per iteration
      gl.FLOAT,       // type: the data is 32bit floats
      false,           // normalize: don't normalize the data
      0,              // stride: 0 = move forward size * sizeof(type) each iteration to get the next position
      0               // start at the beginning of the buffer
    );

    gl.enableVertexAttribArray(pos);

    importProgram();
  },

  updateUniforms = (time: DOMHighResTimeStamp): Promise<string> => {
    return new Promise(resolve => {
      gl.useProgram(program);

      importUniforms(time);

      gl.drawArrays(
        gl.TRIANGLE_FAN, // primitiveType
        0,                    // Offset
        4                     // Count
      );

      resolve('resolved');
    });
  },

  importProgram = () => {
    if (program) {
      ut = gl.getUniformLocation(program, 'time');
      ures = gl.getUniformLocation(program, 'resolution');
      ucenter = gl.getUniformLocation(program, 'center');
      ushake = gl.getUniformLocation(program, 'shake');
      upulse = gl.getUniformLocation(program, 'pulse');
      ublink = gl.getUniformLocation(program, 'blink');
      ulight = gl.getUniformLocation(program, 'light');
    }
    imageDatas.forEach((_imgData, i) => {
      const temp = gl.getUniformLocation(program, 'uTexture' + i);
      if (temp) {
        textureLocationDarvin.push(temp);
      }
    });
  },

  importUniforms = (time: DOMHighResTimeStamp) => {
    gl.uniform1f(ut, time / 1000);
    gl.uniform2f(ucenter, ((((window.innerWidth) / 2)) / (resX / 100) / 100) * deviceRatio, ( (((window.innerHeight) / 2)) / (resY / 100) / 100) * deviceRatio);
    gl.uniform2f(ures, resX, resY);

    gl.uniform1i(ushake, PARAMS.shake);
    gl.uniform1i(upulse, PARAMS.pulse);
    gl.uniform1i(ublink, PARAMS.blink);
    gl.uniform1f(ulight, PARAMS.light);

   // Set each texture unit to use a particular texture.
   textureLocationDarvin.forEach((textureLocation, i) => {
      gl.uniform1i(textureLocation, i);  // texture unit 0
      gl.activeTexture(gl['TEXTURE' + i]);
      gl.bindTexture(gl.TEXTURE_2D, textures[i]);
    });
  },

  resizeCanvasToDisplaySize = (): boolean => {
    const glCanvas = <HTMLCanvasElement>gl.canvas;
    const width = glCanvas.clientWidth * deviceRatio;
    const height = glCanvas.clientHeight * deviceRatio;
    const needResize = glCanvas.width !== width ||
      glCanvas.height !== height;
    if (needResize) {
      glCanvas.width = width;
      glCanvas.height = height;
    }
    return needResize;
  },

  startRenderLoop = (fps: number) => {
    fpsInterval = 1000 / fps;
    then = Date.now();
    renderLoop(then);
  },

  renderLoop = async (time: DOMHighResTimeStamp) => {
    requestAnimationFrame(renderLoop);
    // calc elapsed time since last loop
    now = Date.now();
    elapsed = now - then;

    if (elapsed > fpsInterval) {
      // Get ready for next frame by setting then=now, but also adjust for your
      // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
      then = now - (elapsed % fpsInterval);

      // begin call and store promise without waiting
      resFrame1 = updateUniforms(time);

      // @ts-ignore
      const actualFrame = [await resFrame1];
    }
  },

  startShaderItems = ({vertex, fragment}: any) => {
    createGraphics(vertex, fragment);
  },

  initCanvas = () => {
    resizeCanvasToDisplaySize();
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    startRenderLoop(DEFINE_FPS);
  },

  resize = () => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        resizeCanvasToDisplaySize();

        requestAnimationFrame(() => {
          gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        });
      });
    });
  },

  addDomListener = () => {
    window.addEventListener('resize', resize);
  },

  removeDomListener = () => {
    window.removeEventListener('resize', resize);
  },

  setImageData = (svgPathsArray: any[]) => {
    const canvas2D = <HTMLCanvasElement> document.getElementById('background-canvas2d');
    twodContext = <CanvasRenderingContext2D>canvas2D.getContext('2d');

    svgPathsArray.forEach((svgPaths) => {
      twodContext.clearRect(0, 0, canvas2D.width, canvas2D.height);

      svgPaths.forEach((svgPathNode) => {
        twodContext.fill(new Path2D(svgPathNode));
      });

      const imageData = twodContext.getImageData(0, 0, DEFINE_RES, DEFINE_RES2);
      imageDatas.push(imageData);
    });
  },

  createTextureObject = (imgDatas: ImageData[]) => {
    // create 2 textures
    for (let i = 0; i < imgDatas.length; i++) {
      const texture: any = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, texture);

      // Set the parameters so we can render any size image.
      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.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

      // Upload the image into the texture.
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgDatas[i]);

      // add the texture to the array of textures.
      textures.push(texture);
    }
  },

  addTweakPane = () => {
    // get settings from storage
    const stor = localStorage.getItem('darvindoc-params');
    if (stor) {
      try {
        PARAMS = JSON.parse(stor);
      } catch {
        PARAMS = {
          light: -30.0,
          pulse: true,
          shake: false,
          blink: true
        };
      }
    } else {
      PARAMS = {
        light: -30.0,
        pulse: true,
        shake: false,
        blink: true
      };
    }

    // params panel
    const pane = new Tweakpane();
    pane.addInput(PARAMS, 'pulse').on('change', () => {
      localStorage.setItem('darvindoc-params', JSON.stringify(PARAMS));
    });
    pane.addInput(PARAMS, 'shake').on('change', () => {
      localStorage.setItem('darvindoc-params', JSON.stringify(PARAMS));
    });
    pane.addInput(PARAMS, 'blink').on('change', () => {
      localStorage.setItem('darvindoc-params', JSON.stringify(PARAMS));
    });
    pane.addInput(PARAMS, 'light', {
      min: -50.,
      max: -1.,
    }).on('change', () => {
      localStorage.setItem('darvindoc-params', JSON.stringify(PARAMS));
    });
  };

/**
 * Change framerate
 *
 * @param {number} fps - Set new fps e.g 55
 */
const setFps = (fps: number) => {
  fpsInterval = 1000 / fps;
};

/**
 * destroy all instances
 *
 */
const destroy = () => {
  removeDomListener();
};

/**
 * Initialize module
 *
 * @return {object} Instance of created module.
 * @param webgl boolean that defines wheather to use webgl or not
 */
const init = () => {
  const svgTetureObjects: NodeListOf<HTMLElement> | null = document.querySelectorAll('svg.texture-import');
  const svgTexturePathStrings: any[] = [];

  if (!svgTetureObjects) {
    console.error('> webgl: missing svg icons');
    return;
  }

  // import texture paths
  svgTetureObjects.forEach((svgTextureObject) => {
    const svgPaths: NodeListOf<HTMLElement> | null = svgTextureObject.querySelectorAll('.darvinIconPath');
    const svgPathsString: any[] = [];

    svgPaths.forEach((svgPathNode) => {
      let svgPath: string | undefined;
      // tslint:disable-next-line:no-non-null-assertion
      svgPath = svgPathNode!.getAttribute('d') || undefined;
      svgPathsString.push(svgPath);
    });

    svgTexturePathStrings.push(svgPathsString);
  });

  // init canvas
  canvas = <HTMLCanvasElement>document.getElementById('background-canvas');
  if (!canvas) {
    console.error('#dv> no canvas found');
    return;
  }

  const glContext = canvas.getContext('webgl', {
    preserveDrawingBuffer: false
  });

  if (!glContext) {
    console.error('#dv> error on webgl context');
    return;
  }

  gl = glContext;

  setImageData(svgTexturePathStrings);

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

  startShaderItems({
    vertex: vertexWobble,
    fragment: fragmentWobble
  });

  initCanvas();

  addDomListener();
  addTweakPane();
};

export default {
  init,
  destroy,
  setFps
};

Amusez-vous bien

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X