Three.js: Material: adicionado onBeforeCompile ()

Criado em 9 jun. 2017  ·  75Comentários  ·  Fonte: mrdoob/three.js

Ao longo dos anos, uma solicitação de recurso comum foi poder modificar os materiais embutidos. Hoje percebi que poderia ser implementado de maneira semelhante a Object3D onBeforeRender() .

https://github.com/mrdoob/three.js/commit/e55898c27a843f69a47e602761c60d9bbe91ee35

WebGLPrograms adiciona onBeforeCompile.toString() ao hash do programa, então isso não deve afetar outros materiais embutidos usados ​​na cena.

Aqui está um exemplo do recurso em ação:

http://rawgit.com/mrdoob/three.js/dev/examples/webgl_materials_modified.html

material.onBeforeCompile = function ( shader ) {

    // console.log( shader )

    shader.uniforms.time = { value: 0 };

    shader.vertexShader = 'uniform float time;\n' + shader.vertexShader;
    shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        'vec3 transformed = vec3( position.x + sin( time + position.y ) / 2.0, position.y, position.z );'
    );

    materialShader = shader;

};

Não apenas podemos mexer com o código do shader, mas podemos adicionar uniformes personalizados.

if ( materialShader ) {

    materialShader.uniforms.time.value = performance.now() / 1000;

}

É muito hacky?

/ cc @WestLangley @bhouston @tschw

Enhancement

Comentários muito úteis

@mrdoob Acho que é difícil (ou impossível) serializar Materiais hackeados com .onBeforeCompile() . Você acha que os usuários devem usar ShaderMaterial se quiserem materiais integrados totalmente funcionais (renderizar, copiar, clonar, serializar e assim por diante) modificados?

Não esperava (considerei) o uso de onBeforeCompile() para ser serializado ...

Todos 75 comentários

Parece um pouco hackeado exigir manipulação de string customizada quando WebGLProgram.js já faz muito processamento. Há um caso de uso relacionado, que é adicionar novos #includes em vez de substituir os existentes. Você pode fazer isso hoje, modificando THREE.ShaderLib em tempo de execução, mas parece igualmente sujo.

Talvez uma solução mais agradável para ambos seja permitir que você forneça um ShaderLib personalizado para um material, que substituiria / aumentaria os pedaços embutidos sem manipulação manual das cordas e sem destruí-los permanentemente. Isso não impede um gancho onBeforeCompile para outros usos, mas parece mais de acordo com o que seu exemplo está tentando fazer.

Dito isso, a composição do sombreador com base em inclusões brutas sempre será muito frágil de uma versão para a outra. Se você não se importa em incluir todo o código do esqueleto (como meshphong_vert.glsl ), você já pode estender os materiais integrados com algo assim:

export class MyPhongMaterial extends ShaderMaterial {
  constructor({ color, radius, map, normalMap, emissiveMap }) {
    super();
    this.vertexShader = "...."
    this.fragmentShader = "....";
    this.uniforms = UniformsUtils.clone(ShaderLib.phong.uniforms);

    this.isMeshPhongMaterial = true;
    this.lights = true;

    this.uniforms.time = { value: 1 };
    //...
  }
}

Em todos os casos, você precisa contar com a organização interna dos sombreadores integrados para não mudar muito de uma versão para a próxima. Cada include já é uma função disfarçada, com uma assinatura mal especificada. Eu suspeito que será muito mais limpo e passível de manutenção em ambos os lados para fornecer substituições de nível de função em vez de substituições de nível de inclusão.

Hoje eu pensei ..

Agradável! Também pensei o mesmo no dia 10 de fevereiro, quando fiz esta solicitação de pull:

https://github.com/mrdoob/three.js/pull/10791

Parece que a única diferença é que você está fazendo isso por meio de um retorno de chamada? Eu toquei no argumento não utilizado no WebGLRenderer.

Gosto de usar isso, por exemplo, para fazer o threejs funcionar com mapas normais, mas vinculei alguns problemas que parecem ser triviais de resolver com isso.

Você pode olhar meu branch e dar uma olhada ou dar uma olhada neste exemplo super simples (apenas testa uma deformação customizada com iluminação / sombras etc, e adiciona alguns uniformes).

http://dusanbosnjak.com/test/webGL/three-material-shader-override/webgl_materials_shader_override.html

Talvez uma solução mais agradável para ambos seja permitir que você forneça um ShaderLib personalizado para um material, que substituiria / aumentaria os pedaços embutidos sem manipulação manual das cordas e sem destruí-los permanentemente.

Resume exatamente o que acontece em: https://github.com/mrdoob/three.js/pull/10791 Eu gostaria de poder escrever descrições melhores :)


Devo admitir que não sei o que está acontecendo aqui. Acho que conheço alguém que desistiu de contribuir para três, não entendia na época, mas agora acho meio frustrante.

Frustrante principalmente porque estou tendo um déjà vu quando leio o primeiro parágrafo:

... forma semelhante ao onBeforeRender () do Object3D.

Exatamente a mesma coisa aconteceu com onBeforeRender() https://github.com/mrdoob/three.js/pull/9738 , quando levou um ano para ser convincente para lançar o recurso.

Caramba, duas vezes, com o mesmo dev? Obviamente estou fazendo algo errado aqui, mas o quê? A primeira vez foi o check-in dos arquivos de construção, eu não sabia o que fazer e simplesmente fui esquecido. Mas desta vez eu estava no topo dessa solicitação de pull, há um conflito agora, mas é facilmente resolvido, não houve conflitos por meses.

Não me interpretem mal, não acho que seja vaidade no trabalho aqui, acho que só quero publicar ideias de pessoas e obter feedback. @unconed é óbvio que você está familiarizado com o problema e provavelmente já tentou coisas diferentes. O que pode ter feito um usuário como você perder o outro PR?

Eu me senti muito bem com o shaderlib por material, mas encontrei uma função no renderizador que parecia / comparava shaders inteiros como strings para descobrir o cache, não estava me sentindo muito bem com isso. Gostaria que houvesse algum feedback dado ...

O exemplo estava com defeito? A cabeça pode tornar muito mais óbvio o que está acontecendo do que minha bola de ponta abstrata, mas todo o exemplo funciona com sombras que cobrem mais terreno. É pesado por algum motivo, mas acho que é porque sin / cos em muitos verts.

Acho que se você está começando de novo ... Mesmo que o kit de cena seja absolutamente horrível, essa API é muito legal:

https://developer.apple.com/documentation/scenekit/scnshadable

Embora terrivelmente documentado, não consigo encontrar a parte que é mais interessante, mas basicamente eles abstraíram ganchos em muitos estágios diferentes do shader.

No início deste ano, fiz uma solicitação de pull que parece fazer algo semelhante, senão exatamente a mesma coisa:

https://github.com/mrdoob/three.js/pull/10791

Parece que a única diferença é que você está fazendo isso por meio de um retorno de chamada? Eu toquei no argumento não utilizado no WebGLRenderer.

Desculpe por não poder cuidar daquele PR ainda @pailhead. Acho que a principal vantagem da minha abordagem é que ela exigiu apenas 3 novas linhas.

Dito isso, vou passar algum tempo estudando as suas sugestões e as do @unconed .

Este definitivamente parece mais elegante, apenas três linhas. Eu estava seguindo um padrão diferente no outro. Ia dizer que pode ser um pouco mais prolixo, mas não tão certo. Acho que a única vantagem do outro é que estava bom para ir há alguns meses :)

Caramba, duas vezes, com o mesmo dev? Obviamente estou fazendo algo errado aqui, mas o quê?

Desculpe por isso. A única coisa que consigo pensar é que provavelmente fiquei confuso. Talvez uma longa discussão ou um RP complicado que consumiria muito de minhas energias, então decido deixar para depois e passar para RPs mais simples.

Não é só você @pailhead. @bhouston teve muitos RPs como este, até mesmo @WestLangley e @ Mugen87.

Novamente, não é que eu não goste do PR, é que o PR requer alguma atenção que não posso oferecer no momento. Um bom exemplo é o Instancing PR. Consegui encontrar tempo para lê-lo, fiz minha própria experimentação e sugeri simplificações. Espero poder revisitá-lo em breve.

É difícil administrar tudo isso e dar a cada RP a atenção que merece.

O exemplo estava com defeito? A cabeça pode tornar muito mais óbvio o que está acontecendo do que minha bola de ponta abstrata, mas todo o exemplo funciona com sombras que cobrem mais terreno. É pesado por algum motivo, mas acho que é porque sin / cos em muitos verts.

Agora você mencionou ... Sim, roda a 10 fps em um novo MacBook Pro ao aumentar o zoom 😮

Eu acho que são as sombras e o rand, sin cos e outras coisas, mas sim, parece que leva muito de um golpe.

De qualquer maneira, estou surpreso que você tenha feito isso em três linhas, eu estava muito focado em como os parâmetros dos materiais são transformados em uniformes e segui esse padrão. A diferença é que eu forneço um dicionário alternativo ShaderLib , então #include <> traz um código diferente. Remova a inclusão antes que isso aconteça e substitua-a por glsl. Acho que se você retornar algum tipo de invólucro ao redor do sombreador, talvez essa sintaxe possa ser mais limpa (basta fornecer o nome do trecho + glsl, em vez de replace() . Mas pode acabar se parecendo muito mais com o outro .

Seria muito bom ter isso, eu não trabalhei com tantos motores 3D, mas o kit de unidade e cena parece ter algo assim.

@unconed

Acho que um benefício de onBeforeCompile() é que as propriedades do material permanecem inalteradas. Portanto, parece mais uma extensão dos materiais integrados.

export class MyMeshPhongMaterial extends MeshPhongMaterial {
  constructor( parameters ) {
    super( parameters );
    this.onBeforeCompile = function ( shader ) {
      shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        'vec3 transformed = vec3( position.x + sin( position.y ) / 2.0, position.y, position.z );'
      );
    };
  }
}

var material = new MyMeshPhongMaterial();
material.color.setRGB( 1, 0, 0 ); // this still works

Mexer com ShaderLib é realmente complicado. A adição de ganchos permanentemente inalterados deve ser viável:

    #include <begin_vertex>
    % vertex %
    #include <morphtarget_vertex>
    #include <skinning_vertex>
    % transformed_vertex %
    #include <project_vertex>
    % projected_vertex %

O código de substituição se tornará este:

this.onBeforeCompile = function ( shader ) {
  shader.vertexShader = shader.vertexShader.replace(
    '% vertex %',
    'transformed.x += sin( position.y ) / 2.0;'
  );
);

Em seguida, removeremos qualquer gancho que não tenha sido usado antes da compilação, é claro.

aqui está o que parece em comparação com os ShaderChunks por instância

//given some material
var material = new THREE.MeshNormalMaterial();

//and some shader snippet
var myShader = [
    'float theta = sin( time + position.y ) / 2.0;',
    'float c = cos( theta );',
    'float s = sin( theta );',
    'mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );',
    'vec3 transformed = vec3( position ) * m;', //and making assumptions about THREE's shader framework
    'vNormal = vNormal * m;'
].join( '\n' );

https://github.com/mrdoob/three.js/pull/10791 igual a Material.defines abordagem:

material.shaderIncludes = {

        begin_vertex: myShader,

    //uv_pars_vertex: [
    //  THREE.ShaderChunk['uv_pars_vertex'], //this doesn't have to be
    //  "uniform float time;",
    //].join('\n')
};

material.shaderUniforms = { time: { value: 0, type: 'f' || 'float' } }; //because this could just inject it in the right place (but needs type)

_É apenas um dicionário com os nomes dos trechos e o objeto uniformes. A manipulação da string aqui é mais do tipo "quero reutilizar este pedaço e fazer algo por cima dele" _

este PR:

material.onBeforeCompile = function ( shader ) {

    shader.uniforms.time = { value: 0 };

    shader.vertexShader = 'uniform float time;\n' + shader.vertexShader; //this feels hacky

    shader.vertexShader = shader.vertexShader.replace( //this is more verbose
        '#include <begin_vertex>',
        myShader
    );

};

Acho que um benefício do onBeforeCompile () é que as propriedades do material permanecem inalteradas.

Funciona da mesma forma no outro PR, então acho que não importa como é feito.


Um benefício provavelmente irrelevante em que posso pensar é que você pode manter a lógica para manipular o material no mesmo bloco e no mesmo nível. Se você tinha um pedaço de código em algum lugar mudando de cor, e mais tarde adicionou GLSL personalizado a esse material, você pode adicionar o código para mudar o uniforme com a cor. (pular parede de texto)


Se uv_pars_vertex é confuso, acho que uma boa pergunta é:

"onde injeto minha função GLSL personalizada, como float rand( vec2) ?"

Então, este pode fazer sentido https://github.com/mrdoob/three.js/pull/11050


Outro argumento poderia ser - supondo que você conheça GLSL, supondo que conheça a estrutura de shaders do THREE, você ainda precisa ser criativo e pensar sobre onde injetar o quê. Tive que pensar um pouco e examinar a maioria dos sombreadores para descobrir que uv_pars_vertex é um lugar conveniente para fazer minha extensão de material.

Seria melhor ir de algo assim para uma abstração, ter um gancho lightingPhase , objectTransformation , perspectiveTransformation etc. Ou pelo menos um bloco fictício guaranteedToBeAboveMainButBelowCommonsAndExtensionCalls incluído em cada programa de sombreador. Parece que você vai conseguir isso com % vertex % ? Mas não estou familiarizado com essa sintaxe.

Este PR, como está, parece estar indo na direção oposta. Além de saber onde você precisa tocar, você também precisa ser explícito e manipular strings, repetindo parte do trabalho que o renderizador já faz para você. Além disso, se você fizer outra pergunta além da primeira

Como faço para usar uma função, declarada no bloco comum, dentro da função que estou injetando?

você pode se ver fazendo mais manipulação de strings do que apenas prefixando os uniformes ao sombreador inteiro.

Tive que pensar um pouco e examinar a maioria dos sombreadores para descobrir que uv_pars_vertex é um lugar conveniente para fazer minha extensão de material.

Não acho que haja uma maneira de contornar isso, a menos que façamos uma engenharia excessiva. A ideia com % vertex % é tentar ajudar um pouco nomeando os ganchos e mais ou menos dizer em que estado você está injetando código, mas seria difícil documentar quais variáveis ​​estão disponíveis em que ponto .

Estamos abrindo uma porta para permitir a invasão de materiais embutidos, mas não acho que possamos fornecer "suporte" adequado. O usuário deve estar ciente de que as coisas podem quebrar.

Tentei resolver isso no outro PR. Eu adicionei um gancho de manequim pelo menos para as funções, variações, atributos e uniformes. Muita da lógica GLSL já acontece em structs, o scenekit documentou cada variável (embora não seja mais possível encontrar a mesma documentação).

Provavelmente precisaria ser refatorado à medida que avançamos, mas algo nesse sentido:

#ifdef PHASE_FOO

#include <shader_foo> 
//someGlobalStruct.mvPosition = modelViewMatrix * myLogic( transformed );
//if there is glsl provided for phase "foo" document that it should operate on "transformed" 
//and that it should return "mvPosition" 
#end including <shader_foo>

#else 

  //default
  #ifdef USE_SKINNING

    vec4 mvPosition = modelViewMatrix * skinned;

  #else

    vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );

  #endif

#ifdef PHASE_BAR

#include <something_like_this>
//mvPosition = myPostDefaultTransformationLogic( mvPosition );

#endif

gl_Position = projectionMatrix * mvPosition;

#endif

É claro que ifdefs e includes provavelmente não funcionariam assim. Mas posso ver que é relativamente simples com a outra abordagem. Poderíamos adicionar #include <some_phase> sempre, com strings vazias. Algo pode ajudar a gerenciar o que é acionado usando #ifdef s. Portanto, por exemplo, se você fornecer lógica para a fase "vertex_transformation", o sombreador definirá tudo o que você fornecer. Documentar o que cada bloco faz com GLSL seria de grande ajuda. Sem ir mais longe na abstração, acho que seria ótimo ter este mapa de como os blocos estão estruturados agora.

Estamos abrindo uma porta para permitir a invasão de materiais embutidos, mas não acho que possamos fornecer "suporte" adequado. O usuário deve estar ciente de que as coisas podem quebrar.

Já temos as definições disponíveis em todos os materiais. https://github.com/mrdoob/three.js/issues/10764 sugeriu modificar THREE.ShaderLib e então definir uma propriedade .defines em uma instância de Material . Concedido, a propriedade não estava documentada até que eu escrevesse a entrada (mas você a aprovou :)), e ainda está indefinida .

Mas a maneira como isso funciona atualmente, e é herdado por todos os materiais, é suportado - se você colocar coisas nele, afetará o sombreador desse material . Além disso, se por acaso você definir algo que já faz parte da biblioteca de sombreadores, isso pode quebrar algumas coisas .

Não tentando ser um espertinho, mas tentando dar um exemplo. Usei principalmente definições com ShaderMaterial mas isso afeta todos os materiais, apenas porque funciona como WebGLRenderer . Não há lógica que diga:

se você for muito mais esperto, supere a abstração de um material como Phong então não leve as definições em consideração. Como o three não foi projetado para ter seus fragmentos de sombreador sobrescritos, e o modelo Phong é a autoridade final sobre o que GLSL precisa fazer e serve para ocultar isso do uso, faz sentido não ter as definições GLSL interferindo nisso.

Não tenho certeza se ele pode ser quebrado, porém, eu tenho que tentar.

Com isso dito, gostaria da opção de quebrar coisas, se isso trouxer muito ganho. Com um fragmento GLSL bem encapsulado e minúsculo, eu poderia facilmente mudar minha versão de três para renderizar mapas normais de forma diferente. Pegar um normal e transformá-lo em outro normal é super simples, a menos que comecemos a fazer computação gráfica de uma maneira completamente diferente, não consigo ver isso quebrando :)

Instanciar foi outro exemplo:

oh, eu gostaria de usar ANGLE_INSTANCED_ARRAYS, três não parecem suportá-lo com suas sombras ou materiais PBR, deixe-me apenas adicionar essas duas linhas e fazer funcionar para mim "

Estou feliz em ver que esse recurso finalmente está chegando ao three.js :). Sim, eu também criei um yet another material modifier PR (https://github.com/mrdoob/three.js/pull/7581). Era tão antigo que o código não é mais relevante, mas basicamente injeta ganchos como @mrdoob propõe e, em seguida, basta substituí-los por seu próprio código.
Eu gosto da ideia de ganchos predefinidos, pois é fácil entender o que você está modificando, pois acredito que a maioria das pessoas que deseja usar esse recurso deseja modificar "ligeiramente" o material padrão em vez de reescrevê-lo completamente.

Gosto da simplicidade deste PR, mas se quisermos uma maneira mais estruturada / limpa de fazer as coisas, prefiro já ter um dicionário de ganchos para substituir pelo seu código e uma maneira mais simples de adicionar uniformes ou código personalizado do que apenas substituindo cordas.

@fernandojsg qualquer chance de você poder verificar # 10791, dê feedback, parece super parecido com o que você fez, exceto que coloquei o gancho adicional fora do principal (algo como seu "pre_vertex") e há um pouco mais de gerenciamento.

@fernandojsg Esqueci- me do seu PR 😚. Eu acho que seu PR se parece muito com o que eu estava começando a ver isso funcionando. Subconsciente?

Não tentando ser um espertinho, mas tentando dar um exemplo. Usei principalmente definições com ShaderMaterial mas isso afeta todos os materiais, apenas porque funciona como WebGLRenderer .

Fico feliz que a biblioteca possa ser hackeada dessa forma, mas não devemos usar recursos de abuso internamente para coisas que não foram planejadas. É fácil acabar com um código frágil dessa maneira.

Lendo # 7581 novamente ... Além de https://github.com/mrdoob/three.js/commit/e55898c27a843f69a47e602761c60d9bbe91ee35 , podemos adicionar % HOOKS % e criar uma classe como esta:

THREE.ExtendedMaterial = function ( material, hooks ) {

    material.onBeforeCompile = function ( shader ) {
        var vertexShader = shader.vertexShader;
        var fragmentShader = parameters.fragmentShader;
        for ( var name in hooks ) {
           vertexShader = vertexShader.replace( '%' + name + '%', hooks[ name ] );
           fragmentShader = fragmentShader.replace( '%' + name + '%', hooks[ name ] );
        }
        shader.vertexShader = vertexShader;
        shader.fragmentShader = fragmentShader;
    };

    return material;

};

Então podemos fazer isso:

`` `js
var material = new THREE.ExtendedMaterial (
novo THREE.MeshBasicMaterial (),
{vértice: 'transformado.x + = sin (posição.y) / 2.0;' }
);

Portanto, a questão é: quais ganchos devemos ter?

VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

FRAGMENT_UNIFORMS
INPUT_FRAGMENT
OUTPUT_FRAGMENT

@mrdoob Como exemplo, você pode ver nesta essência onde o código precisa ser injetado no sombreador de vértice MeshBasicMaterial para dar suporte à instanciação.

E aqui para o resto dos materiais :)

https://github.com/mrdoob/three.js/pull/10750

VERTEX_UNIFORMS parece muito opinativo, onde você deve adicionar uma função, um atributo ou uma variação? PRE_VERTEX ou algo assim?

NORMAL_MAP é definitivamente necessário

vert:

PRE_VERTEX
VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

UV
fragment:

PRE_FRAGMENT
INPUT_FRAGMENT
LIGHT
NORMAL

OUTPUT_FRAGMENT

VERTEX_UNIFORMS parece muito opinativo, onde você deve adicionar uma função, um atributo ou uma variação? PRE_VERTEX ou algo assim?

Hmm, teimoso? Como assim?

attribute vec4 aMyAttribute; não é um uniforme :)

float myRand( vec4 foo ) { /*logic*/ return vec4(bar,baz)} não é um uniforme

varying vec3 vMyVarying; também não é um uniforme

ou não é plural, ou nem uniformes

Qual é o caso de uso para "injetar" atributos?
Você não deveria usar geometry.addAttribute( 'aMyAttribute', ... ) vez disso?

Eu não sabia que você não precisava declarar isso. Ainda deixa variações e funções?

Ainda deixa variações e funções?

Bom ponto.

você realmente não precisa mais declarar atributos em GLSL? Achei que funcionava mais como uniformes, você declara em GLSL, mas não precisa digitar em JS.

Acho que essa lista pode crescer muito rapidamente, quando se trata de certas coisas, o código fica bem disperso. Uma forma de fazer mapas normais envolve dois atributos adicionais, variações e lógica de transformação em vert e frag.

Eu gosto das últimas mudanças, ainda é simples e elegante. Eu concordo com @pailhead que precisamos de um local para colocar o código no escopo global. Em https://github.com/mrdoob/three.js/pull/7581 eu tinha preMainVertex preMainFragment para ser injetado globalmente para que você pudesse usá-los para funções e variações. Provavelmente, um nome como globalVertex/Fragment ou semelhante poderia ser melhor.

GLOBAL_VERTEX e GLOBAL_FRAGMENT soam bem 👌

exatamente como os nomeei no outro PR 😆 👍

@pailhead Mas agora você sabe o segredo para @mrdoob eventualmente pense em fazer algo semelhante, aguarde seu PR e então comente lá referenciando o exato da mesma forma que você fez no seu PR para que ele pensasse que era ideia dele, ele vai somar, fundir ... lucro!

image

Agora falando sério também gosto desses nomes e com isso acredito que estamos prontos para ir, ou estou faltando alguma coisa? Vou tentar converter o exemplo do meu PR para este novo código;)

A parte sobre a entrada do código está bem, a linha do tempo está meio errada. # 7581 foi sugerido há quase dois anos. Não foi referenciado em https://github.com/mrdoob/three.js/issues/10789 , então provavelmente saiu do radar. Eu teria batido ou bifurcado.

Mesmo ao importar módulos e escolher seus próprios três, as pessoas não ficam muito felizes em usar módulos centrais não oficiais. Muito mais fácil dizer, "ei, há um pacote npm que manipula foo". É por isso que acho que PRs como esse são importantes, três não é flexível o suficiente.

lucro!

Qual é o lucro? 😆

A parte sobre a entrada do código está bem, a linha do tempo está meio errada. # 7581 foi sugerido há quase dois anos. Não foi referenciado em https://github.com/mrdoob/three.js/issues/10789 , então provavelmente saiu do radar. Eu teria batido ou bifurcado.

O que? Você não verificou todos os PRs abertos antes de criar um novo? #trololol

verdade :)

ok disque a lista e vamos ver o mais rápido possível: D

Este sombreador (por exemplo):

#include <shadowmap_pars_vertex>

void main() {

    #include <begin_vertex>
    #include <project_vertex>
    #include <worldpos_vertex>
    #include <shadowmap_vertex>

}

parece com isso:

#include <shadowmap_pars_vertex>

void main() {

    % begin_vertex %
    % project_vertex %
    % worldpos_vertex %
    % shadowmap_vertex %

}

Umm, não tenho certeza se substituir todos eles ou apenas injetar o código deixando parte do comportamento atual:

#include <shadowmap_pars_vertex>

%GLOBAL_VERTEX% 
void main() {
       %PRE_VERTEX% 
    #include <begin_vertex>
...
}

Minha abordagem inicial foi injetar o código extra, deixando o resto do include intocado.

Então % hook % é apenas para os ganchos reais, se alguém quiser substituir <begin_vertex> eles ainda terão que procurá-lo em uma string, isto é. não haverá um gancho begin_vertex ?

No meu PR, eu só queria modificar um pouco o comportamento do material, normalmente fazer algumas correções de cores depois que o fs foi totalmente executado ou transformações vs semelhantes ao que @mrdoob mostrou em seu exemplo.
Mas sim, se quisermos substituir totalmente cada um dos includes, devemos usar os ganchos em vez dos includes e adicionar a lógica para injetar o include no caso de você não vincular o gancho.

Eu gostaria de fazer isso para normais (não use derivadas e use informações reais de TBN do 3ds max ou maya ou de qualquer lugar onde o mapa normal esteja sendo gerado). Adaptar MeshPhongMaterial para trabalhar com instâncias também exigiu a substituição de alguns pedaços, mas isso parece um caso extremo.

Esses são os dois em que posso pensar, me pergunto se há mais.

Então % hook % é apenas para os ganchos reais, se alguém quiser substituir <begin_vertex> eles ainda terão que procurá-lo em uma string, isto é. não haverá um gancho begin_vertex ?

Você pode substituir ambos. O código atual já permite que você substitua qualquer coisa. %HOOK% uns são para sua conveniência.

Como mencionei em # 11562, que tal o conceito de pedaços de sombreador substituíveis?

Exemplo:

const material = new THREE.MeshPhongMaterial({
  color: 0xffffff,
  map: texture,

  parts: {
    frag: {
      map_pars_fragment: `
        uniform sampler2D map;
        uniform sampler2D map2;
     `,

      map_fragment: `
        vec4 texelColor = texture2D( map, vUv );
        vec4 texelColor2 = texture2D( map2, vUv );

    texelColor = mapTexelToLinear( 
          mix( texelColor, texelColor2, progress)
        );

    diffuseColor *= texelColor;
      `
    }
  }
});

material.uniforms.progress = {value: 1};
material.uniforms.map2 = {value: texture2};

Este é o quão simples pode ser o código de transição entre várias texturas em um material.

Ele pode ser facilmente implementado e evitar a criação de código desnecessário.

@ sasha240100

https://github.com/mrdoob/three.js/pull/10791 se parece exatamente com o que você está sugerindo, exceto parts são chamados de shaderChunks e não o considera como um argumento.
: roll_eyes: 😆

Você também não precisa do dicionário frag{} , uma vez que os pedaços já estão marcados. Todos eles têm _fragment e _vertex .

Este faz o mesmo, exceto que você precisa pesquisar a string para encontrar o que precisa substituir.

@pailhead Sim, vamos abandonar o frag: {} . Usei-o como argumento porque deve ser passado antes que o material shader seja concatenado. Nesse caso, podemos simplesmente evitar a substituição das partes existentes após a concatenação.

Na verdade, podemos adicionar ambos: como argumento e como propriedade. Como color faz

Onde está isso agora? Vejo que o exemplo está fazendo .replace() e não vejo ganchos / pedaços substituíveis. @mrdoob

Não fui capaz de voltar a isso ainda.

Você gostaria de fazer uma RP para a coisa de % hook % ?

Quer o material estendido, do comentário acima?

THREE.ExtendedMaterial = function ( material, hooks ) {

    material.onBeforeCompile = function ( shader ) {
        var vertexShader = shader.vertexShader;
        var fragmentShader = parameters.fragmentShader;
        for ( var name in hooks ) {
           vertexShader = vertexShader.replace( '%' + name + '%', hooks[ name ] );
           fragmentShader = fragmentShader.replace( '%' + name + '%', hooks[ name ] );
        }
        shader.vertexShader = vertexShader;
        shader.fragmentShader = fragmentShader;
    };

    return material;

};

Escolher o local para os ganchos e documentá-los pode não ser tão trivial? Eu talvez substituísse hooks passado por chunks e procurasse as inclusões, com a adição de global_vert e global_frag por enquanto? O que agora que eu escrevi, parece que não deveria ser responsabilidade de três, mas por outro lado, iria arrancar THREE.ExtendedMaterial por % hook % s no futuro?

Não sei se você conhece a extensão THREE.BAS. Ele usa marcadores de posição nos materiais originais que podem ser acessados ​​a partir do material personalizado como matrizes, como:

  var material = new BAS.PhongAnimationMaterial({
    flatShading: true,
    vertexColors: THREE.VertexColors,
    side: THREE.DoubleSide,
    uniforms: {
      uTime: {type: 'f', value: 0}
    },
    vertexFunctions: [
      // cubic_bezier defines the cubicBezier function used in the vertexPosition chunk
      BAS.ShaderChunk['cubic_bezier'],
      BAS.ShaderChunk['quaternion_rotation']
    ],
    vertexParameters: [
      'uniform float uTime;',
      'attribute vec2 aDelayDuration;',
      'attribute vec3 aStartPosition;',
      'attribute vec3 aEndPosition;',
      'attribute vec3 aControl0;',
      'attribute vec3 aControl1;',
      'attribute vec4 aAxisAngle;'
    ],
    vertexInit: [
      'float tProgress = mod((uTime + aDelayDuration.x), aDelayDuration.y) / aDelayDuration.y;',
      'vec4 tQuat = quatFromAxisAngle(aAxisAngle.xyz, aAxisAngle.w * tProgress);'
    ],
    vertexPosition: [
      'transformed = rotateVector(tQuat, transformed);',
      'transformed += cubicBezier(aStartPosition, aControl0, aControl1, aEndPosition, tProgress);'
    ]
  });

Eu gosto do fato de que é baseado em convenções simples: vertexInit e fragmentInit estarão no topo da função main (), por exemplo. vertexParameters e fragmentParameters estão no topo do shader, para definir uniformes ou atributos. vertexFunctions e fragmentFunctions estão antes da função main (). E assim por diante para normais, posição etc ...

Como você pode ver na posição, você altera a variável transformed para alterar a posição final. Coisa semelhante para normais ( objectNormal ) ou cores ( diffuseColor ) no sombreador de fragmento, por exemplo.

É assim que o material Phong se parece, apenas adiciona os marcadores de posição: https://github.com/zadvorsky/three.bas/blob/master/src/materials/PhongAnimationMaterial.js

Oi pessoal, eu estava tentando seguir o convo, mas honestamente estou perdido desde que eu vim de Front end e não de desenvolvedor de jogos.

Estou tentando carregar um MTL e estou recebendo:

material.onBeforeCompile.toString () é indefinido.

Meu arquivo MTL estava apontando originalmente para um arquivo .psd e eu mudei para um dos poucos png que contém a mesma pasta (Normal) Eu acho que não é possível carregar o psd, mas mesmo assim mesmo com o psd está falhando.

# Blender MTL File: 'goblin.blend'
# Material Count: 1

newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.001617 0.005682 0.002517
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd Normal.png

A saída do console é:

THREE.WebGLRenderer 87
bundle.js:54570 OBJLoader: 29.447998046875ms
bundle.js:28429 Uncaught TypeError: Cannot read property 'toString' of undefined
    at WebGLPrograms.getProgramCode (bundle.js:28429)
    at initMaterial (bundle.js:32350)
    at setProgram (bundle.js:32542)
    at WebGLRenderer.renderBufferDirect (bundle.js:31605)
    at renderObject (bundle.js:32335)
    at renderObjects (bundle.js:32308)
    at WebGLRenderer.render (bundle.js:32072)
    at bundle.js:9658
    at bundle.js:53976
    at XMLHttpRequest.<anonymous> (bundle.js:40153)

e o código que estou usando é principalmente:

OBJLoader(THREE);

const modelLoader = new THREE.OBJLoader();
const textureLoader = new MTLLoader();
textureLoader.setTexturePath( 'models/' );
    textureLoader.load('models/goblin.mtl', function( materials ) {

            materials.preload();

            modelLoader.setMaterials( materials );
            modelLoader.load('models/goblin.obj', function ( obj ) {
                 scene.add( obj );
                renderer.render( scene, camera );


            });

        }); 


    renderer.render( scene, camera );

}

Isso é um problema no modelo? Em três? no carregador mtl? tem solução?

Qualquer ajuda seria apreciada.
Obrigado.

Temos que fazer algo como material.shader = shader em material.onBeforeCompile(shader=>...) ?

Acho que parece um pouco desajeitado, pode valer a pena revisitar a outra sugestão em algum momento :)

Está sendo causado por isso, mas o que está fazendo com que Material não tenha essa função?
https://github.com/mrdoob/three.js/blob/35a26f178c523514673d992da1aece23c1cfca6e/src/renderers/webgl/WebGLPrograms.js#L244

@mrdoob

É possível que esses sombreadores estendidos não estejam sendo armazenados em cache corretamente? Estou tentando usar dois materiais diferentes, um mudou dithering_fragment e o outro não. Quando adiciono ambos à cena, parece que apenas aquele sem o pedaço dithering_fragment está sendo usado.

Não consegui recriá-lo com este violino
https://codepen.io/anon/pen/KQPBjd

Mas com este código no meu exemplo

const dithering_fragment = 
`
gl_FragColor.xyz *= foobar
`
// in onBeforeCompile
console.log(shader.fragmentShader)



md5-0f1a3ac67268b230968df20abdbd03d1



/**
#if defined( DITHERING )
  gl_FragColor.rgb = dithering( gl_FragColor.rgb );
#endif

gl_FragColor.xyz *= foobar

}
*/

Mas não há erro se eu adicionar outro material à cena que compartilhe outros inclusões e definições.

É devido a isso:

https://github.com/mrdoob/three.js/blob/35a26f178c523514673d992da1aece23c1cfca6e/src/renderers/webgl/WebGLPrograms.js#L244

Eu tenho a mesma função com alguma lógica fornecida para ambos os materiais, as variáveis ​​para a lógica estão fora do escopo do retorno de chamada, portanto, ambos os meus materiais recebem o mesmo hash?

array.push( material.onBeforeCompile( obtainShaderSomehow ) )

https://github.com/mrdoob/three.js/pull/10791 resolveu assim:

https://github.com/pailhead/three.js/blob/879d52349bd5ef72c64fc962d8ca26bacaf489bf/src/renderers/webgl/WebGLPrograms.js#L242

Você reconsideraria a outra solicitação de pull se eu removesse os marcadores / ganchos global_vertex e global_fragment ? Com eles tocou em muitos arquivos, sem eles, é menos código. Não três linhas, mas faz hash corretamente :)

Comentário #41 neste violino https://codepen.io/anon/pen/KQPBjd
e o material da outra malha na cena mudará.

Isso foi incluído em um lançamento recente ou ainda está em desenvolvimento?

@mrdoob Acho que é difícil (ou impossível) serializar Materiais hackeados com .onBeforeCompile() . Você acha que os usuários devem usar ShaderMaterial se quiserem materiais integrados totalmente funcionais (renderizar, copiar, clonar, serializar e assim por diante) modificados?

_Por favor, leia o seguinte da maneira mais agradável, construtiva e não confrontadora possível_ :)

@takahirox por que isso continua sendo reivindicado?

Eu acho que é difícil (ou impossível) serializar materiais hackeados

Quando você "hackear" materiais com onBeforeCompile você apenas adiciona uniformes que são iguais a qualquer outro material. Não há diferença. Se você pode serializar material.color por que não serializar material.userData.myColor ?

Não estou tentando começar uma briga, paredes de texto ou qualquer coisa. Nos termos mais curtos possíveis, você pode explicar, ou pelo menos me apontar na direção certa por que isso é impossível ou difícil? Estou aberto à possibilidade de estar perdendo algo óbvio, mas se estou, gostaria de entender o que é :)

A partir de uma amostra muito pequena, gerada por artigos , parece que as pessoas não querem usar a abordagem ShaderMaterial nesta experiência .


Isso acabou de me ocorrer? Existe algum tipo de teste que prova que isso é difícil ou impossível? Gostar:

runSerializationTest( someMaterialWithOnBeforeCompile ).expect( something )

Como o método onBeforeCompile pode conter código muito arbitrário, é certamente impossível serializar de forma robusta esse retorno de chamada. Por exemplo, o método pode fazer um XMLHttpRequest síncrono e fazer algo com o resultado. 😅

No entanto ... pela forma como é usado de forma prática (para corrigir os shaders), concordo que não é impossível. Mas as APIs fáceis (por exemplo, fn.toString() ) são hacky e as APIs robustas são mais complexas.

Ainda não entendo o problema. Que tal este ponto de vista:

Pegue rN que tinha MeshPhongMaterial e não tinha MeshStandardMaterial .

Você pode serializar MeshPhongMaterial ie. escreva um JSON como este:

{
  color: 'red',
  specular: 'very',
  glossy: 'not much'
}

Pegue rN + 1 que tem os dois materiais:

Você ainda pode serializar os dois da mesma maneira:

//phong
{
  color: 'red',
  specular: 'very',
  glossy: 'not much'
}

//standard
{
  color:'red',
  shininess: 'shiny',
  metalness: 'not much'
}

Em nenhum lugar você serializou o GLSL de MeshStandardMaterial .

Da mesma forma, serializando qualquer material estendido .

//extended PhongMaterial
{
   color: 'red',
   specular: 'very',
   glossy: 'not much',
   hamburger: 'medium rare'
}

Desserializando:

if ( data.userData.hamburger && HamburgerPhongMaterial )
   mat = new HamburgerPhongMaterial( data ) 
else{ 
   console.warn(`I'm terribly sorry, but you don't have the HamburgerPhongMaterial extension, using default fallback`)
   mat = new PhongMaterial( data ) 
}

O que estou perdendo aqui?


Como onBeforeCompile ... serialize esse retorno de chamada.

Por que, por que você consideraria isso :) Eu acho que este é o ponto crucial da minha confusão, por que qualquer pensamento dado a este gancho, não faz parte da definição material, descrição, nada. Tem algo a ver com o mecanismo, plug-ins, aplicativos, etc., não com os dados.

O exemplo que você deu acima parece bom para mim, ele apenas requer que o código do usuário desserializando o material saiba sobre a existência de HamburgerPhongMaterial? Não tenho nenhum problema com isso, não necessariamente requer alterações de API se você adotar uma abordagem semelhante ao # 14099 e substituir toJSON e fromJSON.

Talvez tenhamos interpretado a pergunta de @takahirox de forma diferente aqui?

... é difícil (ou impossível) serializar Materiais hackeados com .onBeforeCompile (). Você acha que os usuários devem usar o ShaderMaterial se quiserem materiais integrados totalmente funcionais (renderizar, copiar, clonar, serializar e assim por diante)?

Vou tentar separar isso:

  • se o usuário chamar toJSON () em um MeshStandardMaterial simples, onde o GLSL que foi modificado por seu onBeforeCompile , as alterações no GLSL não serão serializadas automaticamente.
  • se o usuário serializar um MeshStandardMaterial com algumas propriedades extras indicando uma alteração no GLSL e carregá-lo com um código personalizado que sabe o que fazer com essas propriedades extras, então certamente tudo bem.
  • várias outras opções (ShaderMaterial, NodeMaterial, KHR_techniques_webgl do glTF, ...) estão disponíveis para serializar materiais personalizados arbitrários.

Se o caso sobre o qual estamos falando não for um desses, acho que estou entendendo mal aqui. Se for sobre o # 14099, (ainda) sou a favor da fusão.

Hmmm, eu não estava realmente distinguindo entre os dois primeiros. Estou tentando pensar em algo que não exigiria uniformes, poderia ser um gl_FragColor.xyz /= gl_FragColor.a; por exemplo. Mas não consigo imaginar um cenário em que você gostaria de serializar algo assim ( junto com o material? ). Parece que cairia no escopo de algum renderizador de efeito ou algo assim, não dados materiais.

materials = loadSerializedMaterials()

effect = new Effect()

effect.load( 'alphaDivide').then(()=>materials.forEach(mat.onBeforeCompile = effect.getOnBeforeCompile('alphaDivide')))

O segundo exemplo é o que eu sempre tive em mente. Junto com várias entradas, serialize uma dica sobre o que fazer com elas userData.extension === 'hamburger' .

Acho que escrevi mal. O que eu queria mencionar é

  • O material integrado hackeado com .onBeforeCompile() não é copiado, clonado, serializado corretamente na mesma API dos materiais integrados '. É tratado como material não hackeado.
  • Se os usuários desejam o resultado correto, eles precisam de algum trabalho extra no lado do usuário.
  • Se os usuários quiserem o material interno hackeado com a API central do Three.js sem nenhum trabalho extra do lado do usuário, eles devem pensar em outras opções, como ShaderMaterial .

Minha pergunta é apenas para a política de .onBeforeCompile() e não para o caso específico. Eu queria esclarecer a limitação e adicionar uma observação no documento.

Se eu esquecer o three.js e todo o sistema de renderização, acho que entendi. Como um objeto genérico, se ele tiver .clone() você deseja clonar e ter expectativas consistentes. O problema é que ambas as soluções (clone, não clone) parecem ter um argumento válido. Você não clona os ouvintes, mas também espera que algo que influencie o desenho seja consistente.

Ainda acho que minha proposta acima é uma maneira válida de fazer isso, já que encontrei o mesmo problema em outras áreas (como compartilhar buffers de profundidade entre alvos também pode ser resolvido com ownThing || outsideThing .

Em vez de armazenar a função em cache, o que não funciona, você basicamente armazenaria em cache userData mas um subconjunto dela. Não faz sentido armazenar userData.hamburger: 'rare' em cache, mas faz userData.effectOn: true". The ownInput | ownChunks | ownWhatever` resolveria esse problema.

Também resolve o problema de:

Como o método onBeforeCompile pode conter código muito arbitrário, é certamente impossível serializar de forma robusta esse retorno de chamada.

Já se passou muito tempo, as pessoas usaram onBeforeCompile agora devemos saber que tipo de código arbitrário pode ser encontrado lá. Acho que só opera no objeto shader que é passado. Você altera esse objeto, mas apenas as mutações que fazem sentido com ShaderMaterial teriam efeito de qualquer maneira. Definir algumas coisas gl sozinho provavelmente seria sobrescrito, e essa é a única coisa que vem à minha mente.

É essencialmente uma etapa de pre three parse , onde você tem a chance de analisar o sombreador junto com a interface uniforms:{} obrigatória, já que você não a tem disponível em Material ( defines:{} por outro lado, você sim, e ambos são irmãos em ShaderMaterial ).

Como você faz isso, ou seja, qualquer que seja o código arbitrário, não importa. Ele não retorna nada, mas muda shader:{} e o renderizador então o usa.

Abordagens que sugiro:

  1. Adicione ownThing a várias classes. THREE.WebGLRenderTarget poderia ter .ownStencilDepthBuffer . Aqui, como sugeri acima, seria alguma versão de ownGLSL:{} ownInput | ownUniforms . Você remove a necessidade de código arbitrário para transformar o objeto shader:{} :

Entradas como esta que se originam de dentro de WebGLRenderer (superprivado):

const shader = {
  uniforms: buildFromMaterial(mat),
  vs: getVSTemplate(key),
  fs: getFSTemplate(key)
}

detonar isso:

shader = onBeforeCompile( shader )

shader.vs //invalid GLSL
shader.uniforms //you got some extra ones that onBeforeCompile may have mutated, maybe removed some others

shader = ___parse(shader) //private step

shader.vs //valid glsl, (not a template)

compile(shader)

Usa isto:

function parseShader(shader){
   return {
     uniforms: shader.uniforms,
     vs: parseChunks(shader.vs)
     fs: parseChunks(shader.fs)
   }
}
//FIX FOR HASHING BUG
function parseChunk( material, chunkName ){
  return material.ownChunks[chunkName] || THREE.ShaderChunks[chunkName]
}

//do whatever you want with the templates, maybe remove an `#include <>`
shader = onBeforeParse(shader)

shader.vs //template || valid glsl

shader = parseShader(shader) //sample OWN || PRIVATE 

shader.vs //valid GLSL 

//if you want to apply a regex on the entire valid glsl, or compile node material or something else
shader = onBeforeCompile( shader )

compile(shader)
  1. Faça WebGLRenderer não saber de nada além de ShaderMaterial .

Onde quer que você tivesse

const mat = new THREE.MeshBasicMaterial()
const sm = new THREE.ShaderMaterial()

Tenho

const mat = nodeMaterial.compile() //returns ShaderMaterial, with a friendly interface (same as StandardMaterial for example)

const mat = chunkMaterial.compile()

A tl: dr das coisas acima é usuário não se preocupa com a determinado ponto no tempo em que "compilação" (parse) ocorre. Acho que eles se preocupam mais com o que acontece, e o que acontece pode ser isolado em um punhado, senão em um único caso de uso in: shaderString, out: shaderString . Acho que vale a pena investigar e compreender o motivo pelo qual isso deve ser feito naquele ponto específico do pipeline. Parece estar mais atrapalhando as coisas do que ajudando.

@mrdoob Acho que é difícil (ou impossível) serializar Materiais hackeados com .onBeforeCompile() . Você acha que os usuários devem usar ShaderMaterial se quiserem materiais integrados totalmente funcionais (renderizar, copiar, clonar, serializar e assim por diante) modificados?

Não esperava (considerei) o uso de onBeforeCompile() para ser serializado ...

Acabei de ser mordido com força usando onBeforeCompile. Eu tenho uma classe auxiliar de shader que me permite fazer modificações nos shaders integrados de uma forma definida. Eu uso o método onBeforeCompile para fazer isso. Aparentemente, como afirmado no primeiro comentário, WebGLProgram usa onBeforeCompile.toString () como um hash. Mas, como minha função é genérica (ela substitui partes dos shaders de vértice e fragmento por variáveis) onBeforeCompile.toString () não parece diferente para shaders diferentes. Isso significa que todos os meus diferentes sombreadores foram armazenados em cache como o mesmo programa. Uma chamada eval e um uuid e agora minhas funções parecem diferentes. Demorou muito para descobrir isso.

@donaldr Veja https://github.com/mrdoob/three.js/issues/13192. Acho que seria bom documentar essas limitações.

você poderia compartilhar como está usando o eval? Estive pensando sobre isso, mas parecia muito sujo. Você poderia basicamente adicionar const fdscxnmdrek435rkjl ao topo do corpo e então avaliar?

Isso significa que todos os meus diferentes sombreadores foram armazenados em cache como o mesmo programa. Uma chamada eval e um uuid e agora minhas funções parecem diferentes. Demorou muito para descobrir isso.

Isso parece muito frustrante, desculpe. 😞 Se as modificações que você precisou fazer parecerem fazer sentido na própria biblioteca, sinta-se à vontade para abrir as edições também.

sinta-se à vontade para abrir questões também.

Você consideraria reabrir os existentes? # 13192 parece o mesmo, mas foi fechado: cry_cat_face: Seria muito bom se uma solução alternativa fosse postada lá se fosse considerada um recurso.

Hmmm...

const _obc = shader => {...}
const obc = `
function (shader){
  const _${ hash() }
  ${ _obc.toString() }
  _obc(shader)
}
`
material.onBeforeCompile = eval(obc)

^ Isso funcionaria? Nunca usei eval .

A introdução de Material.customProgramCacheKey () via # 17567 garante que os desenvolvedores agora tenham a possibilidade de que programas de sombreador não sejam compartilhados para materiais modificados via onBeforeCompile() com instruções condicionais.

Discuta outros problemas existentes ou aprimoramentos no contexto de onBeforeCompile() em novos tópicos (como # 13446).

Esta página foi útil?
0 / 5 - 0 avaliações