regl-project / regl

👑 Functional WebGL
MIT License
5.19k stars 320 forks source link

New deferred interface for command and resource definition? #576

Open kevzettler opened 4 years ago

kevzettler commented 4 years ago

I may be missing some information here but i'm looking for a best pattern to define commands. I would like to define regl draw commands as stand alone modules and import them as needed like:

import regl from './regl';
import flatVert from '../dsl/render/shaders/flat.vert';
import flatFrag from '../dsl/render/shaders/flat.frag';

export default regl({
  vert: flatVert,
  frag: flatFrag,
  elements: regl.prop('cells'),
  attributes: {
    position: regl.prop('positions'),
  },
  uniforms: {
    color: regl.prop('color'),
    model: regl.prop('model')
  }
});

This however is not possible with the current wrapREGL interface design. Because the default export of the regl module is actually the wrapREGL function, you need to first execute wrapRegl before getting a regl instance to defining any commands or resources:

import reglInit from 'regl';

const regl = reglInit({
      gl,
      extensions: ['oes_texture_half_float'],
    });

const drawFlat = regl({
  vert: flatVert,
  frag: flatFrag,
  elements: this.regl.prop('cells'),
  attributes: {
    position: this.regl.prop('positions'),
  },
  uniforms: {
    color: this.regl.prop('color'),
    model: this.regl.prop('model')
  }
});

This means in order to define standalone modules for the draw commands you need to first execute wrapREGL to get a reference to the actual regl instance and then some how pass that into the standalone modules you need a wrapper function and mskes things a bit awkward

// drawFlat.js
import flatVert from '../dsl/render/shaders/flat.vert';
import flatFrag from '../dsl/render/shaders/flat.frag';

export default function(regl){
  return regl({
    vert: flatVert,
    frag: flatFrag,
    elements: regl.prop('cells'),
    attributes: {
      position: regl.prop('positions'),
    },
    uniforms: {
      color: regl.prop('color'),
      model: regl.prop('model')
    }
  })
}
// main drawing app.js
import reglInit from 'regl';
import DrawFlat from 'drawFlat.js'

const regl = reglInit({
      gl,
      extensions: ['oes_texture_half_float'],
    });

const drawFlat = DrawFlat(regl);

In order to achieve the individual module pattern I outlined in the first example, I have hacked together a helper library I'm calling reglDefer. Its usuage looks like this:

import regl from './reglDefer.js';
import flatVert from '../dsl/render/shaders/flat.vert';
import flatFrag from '../dsl/render/shaders/flat.frag';

export default regl({
  vert: flatVert,
  frag: flatFrag,
  elements: regl.prop('cells'),
  attributes: {
    position: regl.prop('positions'),
  },
  uniforms: {
    color: regl.prop('color'),
    model: regl.prop('model')
  }
});

reglDefer is a meta library that lets you define regl commands and regl.props before regl is initalized and queues them up. Once a 'live' regl instance is registered it will flush the queues and initalize all the pending resource and command registration. Here is the reglDefer implementation:

import uuidv1 from 'uuid/v1';
const deferredMethods = {};
const deferredDefs = {};

let realRegl = null;

let reglDefer = function(reglDefinition){
  if(realRegl) return realRegl(reglDefinition);
  const callId = `defer-${uuidv1()}`;
  deferredDefs[callId] = reglDefinition
  return function(){
    return deferredDefs[callId](...arguments);
  }
};

['prop'].forEach((method) => {
 reglDefer[method] = function(){
    const args = [...arguments];
    if(realRegl) return realRegl[method](args);
    const callId = `defer-${uuidv1()}`;
    deferredMethods[callId] = [method, args]
    return callId;
  }
});
reglDefer.init = function(initalizedRegl){
  realRegl = initalizedRegl;
  Object.entries(deferredMethods).forEach(([callId, [method, args]]) => {
    deferredMethods[callId] = realRegl[method](args);
  });

  // recurse over the regl draw definition and replace any deffered props
  function replaceMethods(acc, key){
    if(deferredMethods[acc[key]]){
      acc[key] = deferredMethods[acc[key]]
      return acc
    }

    if(typeof acc[key] === 'object'){
      acc[key] = Object.keys(acc[key]).reduce(replaceMethods, acc[key])
      return acc;
    }

    acc[key] = acc[key];
    return acc;
  }

  Object.entries(deferredDefs).forEach(([key, definition]) => {
    deferredDefs[key] = Object.keys(definition).reduce(replaceMethods, definition);
    deferredDefs[key] = realRegl(deferredDefs[key]);
  });
};

export default reglDefer;

Please let me know if this is insane and I missed an already existing pattern for handling this. I'm curious how others are handling command definition. Am I missing some functionality that wrapREGL handles?

jwerle commented 4 years ago

Hi @kevzettler! have you checked out https://github.com/substack/deferred-regl ?

kevzettler commented 4 years ago

@jwerle lmao no, I haven't, and looks like the exact same idea. Thanks for sharing and confirming my problems. Maybe this should be mentioned in the docs? Maybe eventual incorporated in to core regl?

kevzettler commented 4 years ago

@jwerle I spoke to soon. deferred-regl has the right idea but the execution is still awkward. It still requires you to instantiate a "deferred regl instance" before you can create commands or resources with it. Then you have to pass that deferred regl instance into other modules just like with regular regl. Heres a deferred-regl example to my list of module examples:

import defRegl from 'deferred-regl';
import flatVert from '../dsl/render/shaders/flat.vert';
import flatFrag from '../dsl/render/shaders/flat.frag';

// this will need to live externally to this module and be passed in
//  to have the 'real' regl instance passed to it
const regl = defRegl();

export default regl({
  vert: flatVert,
  frag: flatFrag,
  elements: regl.prop('cells'),
  attributes: {
    position: regl.prop('positions'),
  },
  uniforms: {
    color: regl.prop('color'),
    model: regl.prop('model')
  }
});
kevzettler commented 4 years ago

Ok if I wrap deferred-regl with another singleton module that exports the deferred-regl instance I can then use it in a similar manner.

// reglDefer.js
import defregl from 'deferred-regl';
const dregl = defregl();
export default dregl;
// drawFlat.js
import regl from './reglDefer.js';
import flatVert from '../dsl/render/shaders/flat.vert';
import flatFrag from '../dsl/render/shaders/flat.frag';

export default regl({
  vert: flatVert,
  frag: flatFrag,
  elements: regl.prop('cells'),
  attributes: {
    position: regl.prop('positions'),
  },
  uniforms: {
    color: regl.prop('color'),
    model: regl.prop('model')
  }
});
// main .js
import reglInit from 'regl';
import reglDefer from './reglDefer.js'
import drawFlat from 'drawFlat.js'

const regl = reglInit({
      gl,
      extensions: ['oes_texture_half_float'],
    });

reglDefer.setRegl(regl)
drawFlat();
mikolalysenko commented 3 years ago

This is a pretty cool idea, I need to think about it a bit.