Three.js: dependências cíclicas

Criado em 16 mar. 2015  ·  81Comentários  ·  Fonte: mrdoob/three.js

Ei a todos.

@kumavis e eu trabalhamos duro tentando encontrar uma maneira eficiente de mover o THREE.js para uma arquitetura browserify. Fizemos um bom progresso, até o ponto de ter todos os arquivos movidos para um sistema de compilação browserify e ser capaz de gerar um three.min.js com gulp.

Infelizmente, os exemplos não funcionam porque, diferentemente do commonjs, o browserify não pode lidar com dependências cíclicas, das quais existem muitas em THREE.js.

Eu fiz um gráfico interativo que descreve as relações de dependência aqui .

A menos que e até que eles sejam desembaraçados, não poderemos mover THREE.js para uma compilação do browserify.

Não considero isso uma deficiência do browserify, mas sim um problema com o THREE.js. Dependências circulares são uma coisa ruim de se ter em software em geral e levam a todos os tipos de problemas.

Suggestion

Comentários muito úteis

@Mugen87 uau, 5 anos! parabéns por finalmente acertar :fire: :clap:
Eu me diverti muito fazendo esses gráficos na época :smile_cat:

Todos 81 comentários

isso é um nó para desembaraçar
http://jsbin.com/medezu/2/edit?html ,js,output
image

@coballast você pode postar o código que você usou para gerar a dependência json?

Basta usar o arquivo three.min.js pré-compilado diretamente. Não há necessidade de dividir o Three.js em arquivos individuais dentro do Browserfy, você está apenas dificultando sua vida sem nenhum benefício real.

Falo por experiência, pois usamos o módulo npm do three.js e funciona muito bem. Nós apenas o empacotamos como um único arquivo e o envolvemos em um módulo de estilo CommonJS. Essa abordagem funcionaria para o browserfy e muitas pessoas já estão fazendo isso, eu entendo.

Desembaraçar este nó não é necessário para este caso de uso.

@kumavis Acabei de despejar a estrutura de dependência. O código a seguir gerou o gráfico:

var fs = require('fs-extra');
var unique = require('uniq');
var util = require('util');

function getRequiredObjects(dependencies){
  var result = [];
  for(var i = 0; i < dependencies.usedObjects.length; i++){
    var object = dependencies.usedObjects[i];
    if(dependencies.definedObjects.indexOf(object) == -1)
      result.push(object);
  }

  return result;
};

var dependencies = JSON.parse(fs.readFileSync('./dependencies.json'));

var objects = [];
for(var f in dependencies){
  objects = objects.concat(dependencies[f].usedObjects);
  objects = objects.concat(dependencies[f].definedObjects);
}

objects = unique(objects);


var nodes = objects.map(function(o){
  return {data: {id: o} };
});

var edges = [];
for(var f in dependencies){
  var dependency = dependencies[f];
  var requiredObjects = getRequiredObjects(dependency);
  for(var j = 0; j < dependency.definedObjects.length; j++){
    for(var k = 0; k < requiredObjects.length; k++){
      edges.push({ data: { source: dependency.definedObjects[j], target: requiredObjects[k] } });
    }
  }
}

var graph = {nodes: nodes, edges: edges};

var eliminateImpossibleCycleNodes = function(graph){
  graph.nodes = graph.nodes.filter(function(node){
    var source_edge = null;
    var dest_edge = null;
    for(var i = 0; i < graph.edges.length; i++){
      if(graph.edges[i].data.source == node.data.id)
        source_edge = graph.edges[i];
      if(graph.edges[i].data.target == node.data.id)
        dest_edge = graph.edges[i];
    }

    if(source_edge != null && dest_edge != null)
      return true;
    else
      return false;
  });

  graph.edges = graph.edges.filter(function(edge){
    var source_exists = false, target_exists = false;
    for(var i = 0; i < graph.nodes.length; i++){
      if(edge.data.source == graph.nodes[i].data.id)
        source_exists = true;
      if(edge.data.target == graph.nodes[i].data.id)
        target_exists = true;
    }

    return source_exists && target_exists;
  });
};

for(var i = 0; i < 500; i++)
  eliminateImpossibleCycleNodes(graph)


console.log(JSON.stringify(graph));

@bhouston é mais sobre a saúde da base de código three.js

Só sei que na biblioteca de matemática, que ajudei bastante, as dependências cíclicas são a norma em todas as linguagens. Porque as funções em Matrix4 podem receber Vector3 como parâmetros, e Vector3 pode ser transformado por Matrix4. Tornar todas as dependências de uma maneira na biblioteca matemática tornaria essa parte da biblioteca chata de usar.

Agora eu defendo que a biblioteca de matemática não conhece nenhuma outra parte da biblioteca - tipos mais complexos não devem realmente vazar para esse módulo. Assim, nesse sentido, defendo tentar reduzir as dependências cíclicas entre módulos, mas não remover todas as dependências cíclicas entre arquivos individuais dentro de um módulo.

Aqui está um caso que ilustra as complicações sutis. Para ser claro, aqui não sou crítico da implementação em si, mas dos efeitos colaterais.

Vector3 e Matrix4 formam uma dependência cíclica porque expõem um intervalo de funções que usam umas às outras como tipos de entrada ou saída. Ambos são implementados com um estilo comum ao Three.js, definindo funções via IIFEs para incluir variáveis ​​de rascunho para realizar cálculos.

Matrix4#lookAt é capaz de instanciar o scratch imediatamente, como parte da definição da função.

lookAt: function () {

  var x = new THREE.Vector3();
  var y = new THREE.Vector3();
  var z = new THREE.Vector3();

  return function ( eye, target, up ) {
    /* ... */

Vector3#project no entanto, deve instanciar o zero na primeira execução.

project: function () {

  var matrix;

  return function ( camera ) {

    if ( matrix === undefined ) matrix = new THREE.Matrix4();

    /* ... */

Por quê? pois ao definir a Classe, nem todas as Classes ainda foram definidas. Ao definir Vector3 , Matrix4 ainda não existe. Agora, o tempo real de instanciação das variáveis ​​de rascunho não é realmente importante. A verdadeira lição aqui é que a implementação atual depende da ordem em que o sistema de compilação concatena os arquivos juntos. Esse é um acoplamento muito distante, e alterações no sistema de compilação ou renomeação de arquivos de forma que altere a ordem de concat pode resultar em compilações inválidas, sem conexão óbvia.

Esta é apenas uma das maneiras pelas quais esse nó se manifesta em insetos. No entanto, embora possamos resolver esse problema específico, não tenho uma solução geral que não exija muitas alterações importantes na API.

Hmm... Eu dei uma olhada na biblioteca de matemática C++ do ILM, que considero o padrão ouro em termos de bibliotecas de matemática. Surpreendentemente, eles não são cíclicos. Eles basicamente têm uma ordem bem definida de tipos simples a complexos que eles definem e acho que se você fizer isso com muito cuidado, funcionará:

https://github.com/openexr/openexr/tree/master/IlmBase/Imath

Math e Vec parecem ser os mais simples.

Outras observações sobre o gráfico de dependência:

Material s têm deps bidirecionais com sua classe base
image
Um pouco difícil de ver, mas Geometry s parecem ter bons deps unidirecionais na classe base
image
Light s e Camera s têm uma situação semelhante -- boa em relação à sua classe base, mas a dependência Object3D _em eles_ parece desnecessária.
image
image
Curve s Path s Line s parecem bons, mas Shape é um pouco confuso.
image

@coballast obrigado! este é um grande insight.

Me add nos comentários :)

Aliás, observei como o Material depende do MeshDepthMaterial, por exemplo. É simples

if ( this instanceof THREE.MeshDepthMaterial )

que é trivial mudar para

if ( this.type == 'MeshDepthMaterial' )

e voila - sem dependência. Acho que metade desse gráfico assustador é o mesmo nível de problema.

O engraçado é que essa dependência ocorre em um único método toJSON. Quero dizer, não poderia ser apenas substituído em MeshDepthMaterial? algo como

THREE.MeshDepthMaterial.prototype.toJSON =  function () {
    var output = THREE.Material.prototype.toJSON.apply(this);
    if ( this.blending !== THREE.NormalBlending ) output.blending = this.blending;
    if ( this.side !== THREE.FrontSide ) output.side = this.side;

@makc Em geral, em qualquer lugar que estejamos fazendo instanceof , devemos mover esse código para a própria classe específica. Isso ajudará a remover muitos dos nós.

só queria dizer que enquanto a AMD não suporta referências circulares, os módulos ES6 suportam https://github.com/ModuleLoader/es6-module-loader/wiki/Circular-References-&-Bindings

Estou curioso para saber além do problema de resolução de dependências (que pode ser resolvido na implementação de um carregador de sistema de módulo, por exemplo, system.js ), quais problemas a referência circular em three.js cria?

Felizmente, parece que podemos atacar isso em etapas. Acho que muitas mudanças de quebra de API foram feitas em versões anteriores, então não tenho certeza de que isso esteja fora de questão.

Para os casos instanceof (talvez a maioria) eles devem poder ser resolvidos sem alterações significativas.

Também estou me inscrevendo aqui. Vamos passo a passo aqui.
Concordo que devemos remover todas as dependências cíclicas desnecessárias , como a material.
Também concordo com @bhouston que a biblioteca matemática é muito dependente uma da outra porque a interação é o que torna a biblioteca matemática útil.

Alguém pode mapear os mais fáceis?? Ter menos dependências cíclicas é sempre uma boa ideia se não prejudicar a biblioteca. Podemos ver mais tarde o que fazer com os outros.

@ zz85 Também me deparei com o problema de dependências circulares. É principalmente um problema quando estamos tentando pré-criar certos objetos dentro de arquivos referenciados circulares.

6252 deve esclarecer muitos deps circulares em Material e Object3D .

Aqui está a aparência de Mesh . Talvez alguns deps estranhos, mas não muito loucos.
image

Circular com Object3D e Geometry . A referência Object3D -> Mesh é abordada no PR acima. A referência Mesh -> Geometry é boa, b/c Mesh controla uma instância de Geometry . Ele ainda pode ser interrompido porque está fazendo verificação de tipo para comportamento específico de classe ( Geometry / BufferGeometry ).

Quanto à referência Geometry -> Mesh , é fornecer geometry.mergeMesh( mesh ) . Geometry é um conceito de nível mais baixo que Mesh , então eu o inverteria como mesh.mergeIntoGeometry( geo ) e descontinuaria mergeMesh .

Se alguém obtiver um pr mesclado que corrija alguns deles, avise-me e atualizarei o gráfico para refletir o estado atual das coisas.

@bhouston @gero3 Não estou convencido de que as dependências cíclicas sejam necessárias para obter o mesmo nível de usabilidade/utilidade para a biblioteca matemática. Posso estar errado, mas não poderíamos manter o Vector3 completamente isolado/ignorante do resto do sistema e modificar seu protótipo para acomodar o Matrix4 no módulo Matrix4? Isso faz sentido para mim conceitualmente, já que as matrizes são mais complexas que os vetores. Acho melhor ter uma ordem bem definida na qual os protótipos e classes são construídos para evitar contratempos.

@bhouston @gero3 Acho que podemos fazer isso sem alterar a API. Vou bisbilhotar e ver o que é o quê.

em relação à matemática, você poderia ter todos os "blocos de rascunho" sentados em um único lugar, eu acho. mas aposto que não haverá um único gráfico utilizável de 3js que não tenha Vector3 e Matrix4

Se houver uma solução que não altere o desempenho ou a API da biblioteca de matemática, sou a favor.

@coballast não pode remover o dep cíclico sem uma mudança de API, b/c ambos fornecem métodos que usam o outro tipo. Vector3 Matrix4

Quanto à compatibilidade do browserify, nosso único requisito é mover a instanciação nas vars de rascunho para fora do tempo de definição de classe (torná-las instanciadas na primeira execução). Faça este preguiçoso assim. Isso não teria impacto na API ou no desempenho.

Eu acho que esses tipos de mudanças estão bem.

@kumavis Ah! sim. Ok, entendi agora. Isso é bastante fácil.

Eu apoio totalmente o THREE sendo dividido em módulos menores com uma estrutura obrigatória, pelos seguintes motivos:

  1. TRÊS é muito grande. Se um usuário pudesse exigir apenas o que precisa, isso poderia reduzir os tamanhos de compilação do cliente. Por exemplo , react-bootstrap permite que você faça coisas como var Alert = require('react-bootstrap/lib/Alert'); que não agrupa todos os módulos de bootstrap.
  2. Existem alguns "plugins", como OrbitControls.js que modificam o próprio objeto global TRÊS, colocando-se em THREE.OrbitControls . Este é um antipadrão em estruturas javascript modernas porque requer que THREE esteja presente no namespace global em um processo de compilação, em vez de exigido pelos arquivos que precisam dele. O THREE também faz isso internamente, sempre modificando o namespace THREE, o que não é ideal para incluir módulos THREE específicos.

colocando-se em THREE.OrbitControls

mas cada pedaço de código em 3js faz isso?

@DelvarWorld escreveu:

Eu apoio totalmente o THREE sendo dividido em módulos menores com uma estrutura obrigatória, pelos seguintes motivos:

Eu costumava pensar que era uma boa ideia separá-lo, mas há uma simplicidade no ThreeJS como é agora. É mais utilizável por quem é novo em 3D na forma que está agora e que tem sido uma prioridade para a equipe de desenvolvimento. Você pode usar o ThreeJS sem precisar de um sistema de módulos (que existem muitos e nem todos são totalmente compatíveis).

Assim como @makc , também estou intrigado com a sugestão de @DelvarWorld de não colocar coisas no namespace THREE.

Onde / como eles estariam em vez disso?

Para mim parece o bom padrão criar apenas um objeto global, TRÊS, no qual todas as partes (e possivelmente algumas extensões/plugins) dele estão.

Eu concordo com @DelvarWorld que a técnica de colocar no global não é boa para a saúde da base de código - é um tipo de argumento sutil porque colocá-lo no global em si não é o problema, é o gráfico de dependência oculta e outras práticas que resultam de ter o global disponível.

Mas esse argumento é principalmente limitado ao desenvolvimento interno e à estrutura do código. Quanto a entregar a biblioteca como um pacote de código estático, colocar todas as classes no global TRÊS faz sentido para mim.

Um contra-argumento é que ao desserializar um json de cena THREE.js, as entradas podem apenas listar sua classe como uma string que pode ser extraída do global como: THREE[ obj.type ] . Isso funciona com classes que não estão na biblioteca three.js padrão, desde que você as defina em THREE antes de desserializar. Não tenho certeza da melhor forma de substituir esse comportamento sem o THREE global.

Isso funciona com classes que não estão na biblioteca three.js padrão, desde que você as defina em THREE antes de desserializar. Não tenho certeza da melhor forma de substituir esse comportamento sem o TRÊS global.

Você pode fazer este padrão (ou alguma variante dele) se tudo for um módulo:

var objectType = require( "THREE." + obj.type );

Existem muitas mudanças no ES6 em relação aos módulos. Eu revisitaria a modularidade do ThreeJS nesse ponto.

A versão construída de três (o arquivo javascript que as pessoas podem baixar manualmente) ainda teria tudo no namespace três. você pode fazer isso com o arquivo de ponto de entrada para a construção sendo:

var THREE = {
    Geometry: require("./geometry"),

etc, o que ainda seria útil para os recém-chegados e fácil de começar.

Para aqueles que usam três do npm e requirejs/browserify/webpack em uma compilação javascript moderna, poderíamos fazer algo como

var Scene = require("three/scene"),
     Camera = require("three/camera"),

etc, o que pode reduzir parte do tamanho de três embutidos em um pacote de tamanho de cliente. Posso estar errado porque não sei quanto de três é "núcleo". No momento, porém, isso é impossível porque três não usam instruções require.

de qualquer forma, o padrão moderno é exigir módulos e, em vez de fazer todo o seu código modificar outra biblioteca (modificando o global TRÊS, ruim), seu código é independente e modular e especifica o que ele requer com instruções require , como o código fonte do React .

Eu não acho que tentar fazer um argumento completo para usar a sintaxe require/module será útil, já que existem muitos bons recursos online sobre isso. Mas é ruim encorajar outros a modificar o namespace THREE para adicionar plugins como OrbitControls.

Apenas esteja ciente @DelvarWorld que o ES6 introduz módulos oficialmente no JavaScript com uma sintaxe muito específica e distinta:

http://www.2ality.com/2014/09/es6-modules-final.html

@bhouston oh sim totalmente, sou agnóstico em exigir vs importação (importar é provavelmente a melhor escolha), apenas suportando o padrão do módulo em geral.

@bhouston @DelvarWorld @kumavis Um dos meus projetos de longo prazo é escrever um conversor automático es5 -> es6 que possa acomodar e converter módulos commonjs/amd para es6, e espero identificar e reescrever muito javascript usando construções es6 como classes/geradores e assim por diante. Pode-se usar uma transformação browserify como es6ify enquanto os navegadores estão alcançando o padrão para preparar o código para consumo. Mover o THREE para o browserify apenas no nível interno é um bom primeiro passo para prepará-lo para entrada em tal ferramenta.

Isso está além do ponto que eu estava (aparentemente muito mal) tentando fazer. Eu quero remover o máximo possível dessas dependências cíclicas, independentemente de quaisquer preocupações de modularidade, porque acredito que isso tornará o THREE mais estável, flexível e provavelmente eliminará muitos bugs como um efeito colateral agradável.

@coballast https://github.com/mrdoob/three.js/pull/6252 foi mesclado, deve reduzir muitos deps cíclicos. Acha que pode gerar um novo gráfico dep? Talvez torná-lo um utilitário no repositório da ferramenta de conversão

o próximo é: fazer as vars de rascunho em Vector3 Matrix4 definidas preguiçosamente no primeiro uso, não no tempo de definição

alguém quer ser voluntário? deve ser rápido

O gráfico foi atualizado. http://jsbin.com/medezu/3/

Aqui está uma captura de tela:

snapshot3

Estou feliz em informar que um grande número de deps de circ Object3Ds foi eliminado. Bom trabalho @kumavis!

uau é a mesma base de código? louco

Trabalhará para tornar a geração de gráficos parte do utilitário.

Na inspeção do gráfico sozinho, Shape e Geometry parecem ser possíveis árvores de classe que podem ser desvendadas.

@coballast acha que você pode enfrentar isso?

tornando as vars de rascunho no Vector3 Matrix4 definidas preguiçosamente no primeiro uso, não no tempo de definição

isso seria um PR contra dev upstream em oposição a uma mudança automatizada

Também acho que podemos fazer o downgrade do título do problema de "Problemas sérios de dependência cíclica" para "dependências cíclicas" - a situação melhorou muito!

@kumavis Com certeza. Trabalhará nele quando o tempo permitir.

Aqui está o estado atual da limpeza de interdependência como eu vejo:
(as setas mostram conexões que devem ser removidas se apropriado)

  • [x] Materiais
  • [x] Geometrias
  • [x] Object3D's
  • [x] Matemática
  • [x] Formas

    • [x] Forma -> FontUtils

    • [x] Forma -> ExtrudeGeometria

    • [x] Forma -> FormaGeometria

    • [x] Caminho -> Forma

  • [ ] Caixa 3

    • [ ] Box3 -> BufferGeometria

    • [ ] Caixa 3 -> Geometria

Formas:

image

Caixa3:

image

Matemática:

Esses nós são interconectados, mas fornecem conveniência suficiente por meio disso.
image

Shape parece ter dependências com ExtrudeGeometry e ShapeGeometry através de código como este:

// Convenience method to return ExtrudeGeometry

THREE.Shape.prototype.extrude = function ( options ) {

  var extruded = new THREE.ExtrudeGeometry( this, options );
  return extruded;

};

// Convenience method to return ShapeGeometry

THREE.Shape.prototype.makeGeometry = function ( options ) {

  var geometry = new THREE.ShapeGeometry( this, options );
  return geometry;

};

Agora parece que Shape é uma subclasse de Path , e ExtrudeGeometry e ShapeGeometry são ambas subclasses de Geometry . Então, você sabe, você pode descobrir quais dependências precisariam desaparecer no caso ideal.

Sim, isso se enquadra na mesma categoria que Vector3 <-> Matrix4 . Eles estão ligados por conveniência. Eu acho que é uma má ideia, mas não vale a pena lutar contra isso. vou marcar como completo

Shape -> FontUtils pode ser removido usando métodos de movimentação como triangulate para Utils mais genéricos. Mas não é uma grande vitória para fazer isso. Indo marcar como concluído.

Box3 -> BufferGeometry e Box3 -> Geometry podem ser limpos.

É outro caso de não colocar comportamento dependente de classe na própria classe.

fonte :

setFromObject: function () {

  // Computes the world-axis-aligned bounding box of an object (including its children),
  // accounting for both the object's, and childrens', world transforms

  /* ... */

  if ( geometry instanceof THREE.Geometry ) {
    /* ... */
  } else if ( geometry instanceof THREE.BufferGeometry && geometry.attributes[ 'position' ] !== undefined ) {
    /* ... */
  }

  /* ... */

}

Em ambos os casos, está apenas tentando iterar pelos vértices/posições worldCoordinate da geometria. Gostaria de saber se simplificaria muito o código para criar um objeto preguiçoso vertices em BufferGeometry que procura os valores conforme eles são solicitados. Não tenho certeza sobre o impacto do perf.

Alternativamente, poderíamos usar Geometry.computeBoundingBox :
Geometry
BufferGeometry

Box3 é um ponto problemático ao executar a compilação do browserify. Veja as notas em coballast/threejs-browserify-conversion-utility#21.

@kumavis Você se importaria de delinear sua solução recomendada para lidar com Box3/Geometry/BufferGeometry? Se for rápido, posso implementá-lo.

Eu não posso olhar para isso agora, mas eu começaria com minha sugestão acima para usar geo.computeBoundingBox como implementado em Geometry e BufferGeometry no lugar do if/else aqui . Em vez disso box3.setFromObj deve chamar geometry.computeBoundingBox e, em seguida, definir parâmetros com base na caixa3 produzida.

Isso deve remover o final Box3 -> BufferGeometry e Box3 -> Geometry das dependências circulares. Me avise se estiver faltando alguma coisa.

Hmm, talvez o código resultante seja um pouco complicado, o que realmente faz sentido aqui? Box3.setFromObject não deveria existir, mas isso não é uma opção. Geo deve ser capaz de produzir box3s, não tenho nenhum problema com isso. Sim, eu acho que Box3.setFromObject deveria pedir aos geos por uma caixa delimitadora/extensões, mas talvez eles devam pedir Object3D / Mesh pela caixa delimitadora/extensões.

desculpe ficou um pouco divagante. deixe-me saber o que você pensa.

Possivelmente relevante: #6546

Sem algo assim, é impossível analisar essas dependências dinâmicas dos scripts do carregador.

Com base em meus testes, dependências cíclicas não são um problema com commonJSification. Eles precisam ser tratados corretamente e, como dito anteriormente neste tópico, eles tornam o gráfico de dependência bastante confuso, mas não impedem que o THREE.js funcione em ambientes commonJS (quando transformados, é claro).

Acabei de publicar uma versão completa do commonjs no npm como three.cjs usando meu transpilador de três commonjs

Nota: para isso funcionar, eu tive que escolher manualmente o #6546 no master. Embora as dependências dinâmicas funcionem bem no node.js, elas não podem funcionar no Browserify (ou qualquer outra ferramenta de cjs para navegador), pois precisam executar análise de dependência estática.

Prova de navegador: http://requirebin.com/?gist=b7fe528d8059a7403960

@kamicane FYI - Aqui é onde THREE foi adicionado como um argumento de uma função anônima para Raycaster (anteriormente Ray ).

Eu entendo a necessidade de uma função anônima (para evitar vazamentos globais nos navegadores), o argumento é supérfluo no entanto, e faz com que todo o arquivo caia na categoria de dependências computadas. Embora o argumento possa ser removido dinamicamente com modificações de AST, ele nunca pode ser uma solução à prova de balas (dependendo do que está sendo gravado no argumento, lido do argumento etc.). A análise estática torna-se quase impossível. A intervenção manual é necessária neste caso.

Agora que Raycaster é mais leve, podemos tentar torná-lo como o resto das classes.

@mrdoob , @Mugen87 usamos Rollup para usar apenas as partes do Three.js que realmente precisamos. Quando executamos o build ainda recebemos o seguinte aviso:

(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Matrix4.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Quaternion.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Sphere.js -> node_modules/three/src/math/Box3.js -> node_modules/three/src/math/Sphere.js
(!) Circular dependency: node_modules/three/src/objects/LineSegments.js -> node_modules/three/src/objects/Line.js -> node_modules/three/src/objects/LineSegments.js

Ainda existem dependências circulares no Three.js ou estamos fazendo algo errado?

Vector3 e Matrix4 estão ligados um ao outro, se você puxar um, você precisa puxar o outro. Dependências circulares devem ser tecnicamente permitidas.

@bhouston sim, entendo, obrigado pela dica. Sim, dependências circulares são permitidas e o rollup não causa problemas, mas não tenho certeza se é uma boa prática ter dependências circulares. Vector3 só depende de Matrix4 por causa de multiplyMatrices e getInverse , para mais detalhes veja (https://github.com/mrdoob/three.js/ blob/dev/src/math/Vector3.js#L315)

@roomle-build Idk, cara, só porque referencia explicitamente o construtor Matrix4? A respeito

    applyMatrix4: function ( m ) {

        var x = this.x, y = this.y, z = this.z;
        var e = m.elements;

        var w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );

        this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
        this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
        this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;

        return this;

},

?

você pode dizer que poderia passar { elements: [....] } e funcionará, mas todos sabemos que espera Matrix4 lá

Vamos começar com Vector3 .

Vector3 depende de Matrix4 por causa de project e unproject :

    project: function () {

        var matrix = new Matrix4();

        return function project( camera ) {

            matrix.multiplyMatrices( camera.projectionMatrix, matrix.getInverse( camera.matrixWorld ) );
            return this.applyMatrix4( matrix );

        };

    }(),

    unproject: function () {

        var matrix = new Matrix4();

        return function unproject( camera ) {

            matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) );
            return this.applyMatrix4( matrix );

        };

    }(),

Vector3 depende de Quaternion por causa de applyEuler e applyAxisAngle :

    applyEuler: function () {

        var quaternion = new Quaternion();

        return function applyEuler( euler ) {

            if ( ! ( euler && euler.isEuler ) ) {

                console.error( 'THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.' );

            }

            return this.applyQuaternion( quaternion.setFromEuler( euler ) );

        };

    }(),

    applyAxisAngle: function () {

        var quaternion = new Quaternion();

        return function applyAxisAngle( axis, angle ) {

            return this.applyQuaternion( quaternion.setFromAxisAngle( axis, angle ) );

        };

    }(),

Sugestões?

Não tenho certeza se precisamos remover dependências circulares por todos os meios. Mas eu poderia imaginar mover multiplyMatrices para um módulo Math . A assinatura então mudaria para multiplyMatrices( a: Matrix4, b: Matrix4, result: Matrix4 ): Matrix4 . Dentro Vector3 você poderia então import { multiplyMatrices } from './Math'; o mesmo poderia ser feito em Matrix4 (para manter a superfície da API de Matrix4 a mesma).

Acabei de dar uma olhada rápida (não olhei para o caso Quaternian - apenas Vec3/Mat4 ) e também não tenho certeza sobre as implicações de desempenho e consequências para o restante da base de código. Além disso, também não estou convencido de que seja absolutamente necessário remover essas dependências circulares. Só queria compartilhar meu pensamento porque @mrdoob pediu sugestões

@roomle-build então basicamente cria mais módulos para não ter dependências circulares, mas você ainda vai usar todos esses módulos? isso talvez pudesse fazer mais sentido se cada método matemático fosse seu próprio módulo, então você está puxando apenas aqueles que você usa, mas serão muitos módulos.

@makc não realmente. Seria um grande módulo com muitas pequenas funções "auxiliares". Isso também ajudaria a sacudir as árvores, etc. Um módulo de matemática pode se parecer com:

export const multiplyMatrices( a, b, result ) { // ... DO STUFF ... // }
export const getInverse( /* ... */ ) { // ... DO STUFF ... // }
// ...
// ...

E o módulo consumidor faria algo como:

import { Matrix4 } from './Matrix4.js';
import { multiplyMatrices } from './math';
const result = new Matrix4( );
multiplyMatrices( a, b, result );

Ao agrupar tudo, o rollup faz mágica e cria o pacote mais eficiente.

Isso é o que muitas bibliotecas populares estão fazendo. Na verdade, o RxJS mudou sua "lógica" import também para o padrão que descrevi. Lá parece algo como:

 import { flatMap, map, tap } from 'rxjs/operators';

myObject.run().pipe(
  tap(result => doSomething()), 
  flatMap(() => doSomethingElse()), 
  map(() => doAnotherThing())
);

Você pode ler sobre o "por que e como" eles mudaram essas coisas no RxJS 6 em vários posts do blog, por exemplo: https://auth0.com/blog/whats-new-in-rxjs-6/

Mas como eu disse, é apenas um pensamento e não tenho certeza sobre todas as implicações que isso teria para o resto da base de código. Além disso, o módulo math atual não está "preparado" para ser usado assim. Atualmente todos os métodos do módulo matemático estão anexados "tipo de estática". Isso também evita que o rollup detecte o que é realmente necessário...

@roomle-build hmm, então você está dizendo que o rollup pode entender se o código no mesmo escopo não precisa de todo o escopo, legal.

Você está falando sobre a mudança para uma abordagem funcional (funções com objetos) em vez de uma abordagem orientada a objetos (objetos com funções-membro). bem grande e quebraria todo o código existente.

Não tenho certeza de que os argumentos a favor dessa mudança sejam tão significativos neste momento para justificar a quebra de toda a compatibilidade com versões anteriores.

@makc não realmente. Seria um grande módulo com muitas pequenas funções "auxiliares". Isso também ajudaria a sacudir as árvores, etc. Um módulo de matemática pode se parecer com:

Se é isso que está sendo proposto, deve ser descrito corretamente. É a mudança do Three.JS de um estilo de design orientado a objetos para um design funcional.

@roomle-build hmm, então você está dizendo que o rollup pode entender se o código no mesmo escopo não precisa de todo o escopo, legal.

yes rollup entende como todas as importações se relacionam umas com as outras e faz trepidação de árvores, eliminação de código morto etc. As novas versões de rollup também podem fazer "chunking" e muitas outras coisas legais. Mas a estrutura atual do projeto não aproveita totalmente esses recursos.

Você está falando sobre a mudança para uma abordagem funcional (funções com objetos) em vez de uma abordagem orientada a objetos (objetos com funções-membro). bem grande e quebraria todo o código existente.

Não acho que esses dois paradigmas sejam mutuamente exclusivos. Acho que você pode misturar e combinar esses dois paradigmas. Também não proponho mudar para programação funcional. Eu só queria descrever uma maneira de se livrar da dependência cíclica. Você também pode anexar o método multiplyMatrices Math . Mas se alguém reescrever esse tipo de coisa, faria sentido considerar o uso dos recursos dos módulos ES6. Mas como eu disse, não sou um especialista na base de código Three.js e foi apenas uma ideia de como eliminar a dependência cíclica. Eu acho que Three.js é um projeto incrível com uma ótima base de código e eu não quero incomodar. Então espero que ninguém se sinta ofendido com meus comentários 😉

Não tenho certeza se devemos discutir decisões de design em um problema. Você tem algum lugar onde esse tipo de coisa se encaixa melhor?

BTW gl-matrix é uma biblioteca matemática funcional: https://github.com/toji/gl-matrix/tree/master/src/gl-matrix

@roomle-build

Atualmente todos os métodos do módulo matemático estão anexados "tipo de estática".

Como assim?

@mrdoob Acredito que, com o design funcional do gl-matrix, cada função de digamos vec3 (no arquivo vec3 ao qual vinculei no meu comentário anterior) é exportada individualmente. Isso permite que você escolha quais funções importar. Você não precisa trazer todo o vec3.

Enquanto com Three.JS, porque ele usa um design orientado a objetos, todas as funções matemáticas para Vector3 são anexadas ao protótipo de objeto Vector3 e você importa apenas a própria classe Vector3.

Assim, as importações em Three.JS são de classes inteiras, enquanto com uma abordagem funcional você importa funções individuais.

(A outra coisa muito legal sobre a biblioteca gl-matrix é que todas as funções individuais não usam outras funções, @toji basicamente incorporou uma versão otimizada de toda a matemática em cada operação individual. Isso provavelmente é bastante eficiente em termos de velocidade mas leva a uma biblioteca difícil de manter.)

Eu não acho que precisamos refatorar esta parte do Three.JS, exceto talvez para nos livrarmos de quaisquer referências em /math para outros diretórios em three.js. A biblioteca de matemática é bastante pequena e nunca aparece em meus testes de criação de perfil nos dias de hoje. Sim, não é extremamente eficiente, mas é próximo o suficiente, mantendo a legibilidade e a facilidade de uso.

@bhouston Entendi. Muito obrigado pela explicação! 😊

Eu só queria acompanhar o assunto. Mas eu quero voltar de importing function vs importing classes para o tópico de resolução de cyclic dependencies . (Também não vejo por que import { someFunction } from 'SomeModule' é menos sustentável do que import SomeClass from 'SomeModule' , mas esse definitivamente não é o tópico desta questão/conversa.

Para resolver a dependência cíclica seria possível colocar a funcionalidade em uma classe separada. Você pode anexar um método multiplyMatrices ao Math-Class ou criar um Multiplier-Class que tenha o método multiplyMatrices . Mas como eu disse antes não tenho certeza se temos que remover dependências cíclicas. Se a decisão for não removê-los, acho que esse problema pode estar próximo 😃

Depois de resolver o #19137, isso pode ser fechado agora 🎉.

@Mugen87 uau, 5 anos! parabéns por finalmente acertar :fire: :clap:
Eu me diverti muito fazendo esses gráficos na época :smile_cat:

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

Questões relacionadas

jack-jun picture jack-jun  ·  3Comentários

alexprut picture alexprut  ·  3Comentários

boyravikumar picture boyravikumar  ·  3Comentários

fuzihaofzh picture fuzihaofzh  ·  3Comentários

makc picture makc  ·  3Comentários