Three.js: Материал: добавлен onBeforeCompile ()

Созданный на 9 июн. 2017  ·  75Комментарии  ·  Источник: mrdoob/three.js

На протяжении многих лет обычным запросом функции была возможность модифицировать встроенные материалы. Сегодня я понял, что это можно реализовать аналогично Object3D onBeforeRender() .

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

WebGLPrograms добавляет onBeforeCompile.toString() к хешу программы, поэтому это не должно влиять на другие встроенные материалы, используемые в сцене.

Вот пример функции в действии:

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;

};

Мы не только можем возиться с кодом шейдера, но и можем добавить пользовательскую униформу.

if ( materialShader ) {

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

}

Это слишком взломано?

/ cc @WestLangley @bhouston @tschw

Enhancement

Самый полезный комментарий

@mrdoob Я думаю, что сериализовать Материалы, взломанные с помощью .onBeforeCompile() , сложно (или невозможно). Как вы думаете, следует ли пользователям использовать ShaderMaterial если им нужны полностью функциональные (рендеринг, копирование, клонирование, сериализация и т. Д.) Модифицированные встроенные материалы?

Я не ожидал (рассматривал) использование onBeforeCompile() для сериализации ...

Все 75 Комментарий

Кажется немного хакерским - требовать настраиваемых манипуляций со строками, когда WebGLProgram.js уже выполняет большую обработку. Есть связанный вариант использования, который заключается в добавлении новых #includes, а не замене существующих. Вы можете сделать это сегодня, изменив THREE.ShaderLib во время выполнения, но это выглядит так же грязно.

Возможно, более удачным решением для обоих является предоставление настраиваемой ShaderLib для материала, которая переопределяет / дополняет встроенные фрагменты без ручных манипуляций со строками и без их постоянного затирания. Это не исключает использования ловушки onBeforeCompile для других целей, но, похоже, больше соответствует тому, что пытается сделать ваш пример.

Тем не менее, композиция шейдеров на основе необработанных включений всегда будет очень хрупкой от одной версии к другой. Если вы не против включения всего кода скелета (например, meshphong_vert.glsl ), вы уже можете расширить встроенные материалы примерно так:

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 };
    //...
  }
}

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

Сегодня подумал ..

Отлично! Я тоже так подумал 10 февраля, когда сделал запрос на перенос:

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

Кажется, единственная разница в том, что вы делаете это через обратный вызов? Я воспользовался неиспользованным аргументом в WebGLRenderer.

Мне нравится использовать это, например, чтобы заставить threejs работать с картами нормалей, но я связал некоторые проблемы, которые кажется тривиальным решением для этого.

Вы можете взглянуть на мою ветку и попробовать это или проверить этот супер простой пример (просто проверяйте настраиваемую деформацию с освещением / тенями и т. Д. И добавляете несколько униформ).

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

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

Точно суммирует, что происходит в: https://github.com/mrdoob/three.js/pull/10791 Я хотел бы написать более качественные описания :)


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

Расстраивает в основном потому, что у меня начинается дежавю, когда я читаю первый абзац:

... аналогично методу onBeforeRender () в Object3D.

То же самое произошло с onBeforeRender() https://github.com/mrdoob/three.js/pull/9738 , когда потребовался год, чтобы убедить эту функцию.

Боже, дважды, с одним и тем же разработчиком? Я явно здесь что-то делаю не так, но что? В первый раз были проверены файлы сборки, я не знал, что делать, и об этом просто забыли. Но на этот раз я был на вершине этого запроса на перенос, сейчас есть конфликт, но он легко разрешается, конфликтов не было в течение нескольких месяцев.

Не поймите меня неправильно, я не думаю, что здесь дело в тщеславии, я думаю, что просто хочу продвигать идеи людей и получать обратную связь. @unconed очевидно, что вы знакомы с проблемой и, вероятно, пробовали разные вещи. Что могло заставить такого пользователя, как вы, пропустить другой PR?

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

Был ли пример ошибочным? Голова может сделать это намного более очевидным, чем мой абстрактный мяч с шипами, но весь пример работает с тенями, которые покрывают большую территорию. По какой-то причине он тяжелый, но я думаю, это из-за sin / cos на многих вертах.

Думаю, если вы начинаете сначала ... Несмотря на то, что набор сцен абсолютно ужасен, этот api действительно хорош:

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

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

Ранее в этом году я сделал запрос на перенос, который, кажется, делает что-то похожее, если не то же самое:

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

Кажется, единственная разница в том, что вы делаете это через обратный вызов? Я воспользовался неиспользованным аргументом в WebGLRenderer.

Извините, что пока не могу позаботиться об этом пиаре @pailhead. Думаю, главное преимущество моего подхода в том, что для этого потребовалось всего 3 новые строки.

Сказав это, я сейчас потрачу некоторое время на изучение ваших и @ несогласованных предложений.

Этот определенно кажется более элегантным, всего три линии. В другом я следовал другому образцу. Хотел сказать, что это может быть немного более многословно, но не так уверенно. Думаю, единственное преимущество другого в том, что несколько месяцев назад все было хорошо :)

Боже, дважды, с одним и тем же разработчиком? Я явно здесь что-то делаю не так, но что?

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

Это не только ты, @pailhead. У @bhouston было много подобных пиаров , даже у

Опять же, дело не в том, что мне не нравится PR, а в том, что PR требует некоторого внимания, которое я не могу предложить в то время. Хороший пример - Instancing PR. Мне удалось найти время, чтобы прочитать его, поэкспериментировать и предложить упрощения. Надеюсь, я скоро смогу вернуться к нему.

Всем этим сложно управлять и уделять каждому пиару должное внимание.

Был ли пример ошибочным? Голова может сделать это намного более очевидным, чем мой абстрактный мяч с шипами, но весь пример работает с тенями, которые покрывают большую территорию. По какой-то причине он тяжелый, но я думаю, это из-за sin / cos на многих вертах.

Теперь вы упомянули об этом ... Да, работает со скоростью 10 кадров в секунду на новом MacBook Pro при увеличении 😮

Я думаю, это тени и ранд, sin cos и прочее, но да, похоже, это требует слишком большого удара.

В любом случае, я удивлен, что вы сделали это в трех строках, я был слишком сосредоточен на том, как параметры материала превращаются в униформу, и следовал этому шаблону. Разница в том, что я предоставляю альтернативный словарь типа ShaderLib , поэтому #include <> вводит другой код. Вы удаляете include до того, как это произойдет, и замените его на glsl. Я думаю, что если вы вернете какую-то оболочку вокруг шейдера, возможно, этот синтаксис может быть чище (просто укажите имя фрагмента + glsl вместо replace() . Но он может в конечном итоге выглядеть намного больше, чем другой .

Было бы действительно неплохо иметь это, я не работал с таким большим количеством 3D-движков, но у единства и набора сцен есть что-то вроде этого.

@unconed

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

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

Возиться с ShaderLib действительно сложно. Однако добавление навсегда неизменных хуков должно быть выполнимо:

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

Код замены станет следующим:

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

Затем мы, конечно же, удалим все хуки, которые не использовались перед компиляцией.

вот как это выглядит по сравнению с ShaderChunks для каждого экземпляра

//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 то же, что и подход Material.defines :

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)

_Это всего лишь словарь имен чанков и объект униформы. Манипуляции со строками здесь больше похожи на «я хочу повторно использовать этот кусок и сделать что-нибудь поверх него» _

этот 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
    );

};

Я предполагаю, что одним из преимуществ onBeforeCompile () является то, что свойства материала остаются неизменными.

То же самое работает и в другом PR, поэтому я думаю, не имеет значения, как это делается.


Одно, вероятно, не относящееся к делу преимущество, о котором я могу думать, заключается в том, что вы можете сохранить логику для управления материалом в одном блоке и на одном уровне. Если у вас есть фрагмент кода, где-то меняющий цвет, и вы позже добавляете пользовательский GLSL к этому материалу, вы можете добавить код, чтобы изменить униформу с цветом. (пропустить стену текста)


Если uv_pars_vertex сбивает с толку, я думаю, что хороший вопрос:

"где я могу ввести свою пользовательскую функцию GLSL, например float rand( vec2) ?"

Тогда это может иметь смысл https://github.com/mrdoob/three.js/pull/11050


Другой аргумент: если вы знаете GLSL, предполагая, что вы знаете шейдерную структуру THREE, вам все равно нужно проявить творческий подход и подумать о том, куда вы что вводите. Мне пришлось немного подумать и просмотреть большинство шейдеров, чтобы понять, что uv_pars_vertex - удобное место для расширения моего материала.

Было бы лучше перейти от чего-то вроде этого к абстракции, иметь хуки lightingPhase , objectTransformation , perspectiveTransformation т. Д. Или, по крайней мере, фиктивный кусок guaranteedToBeAboveMainButBelowCommonsAndExtensionCalls который включен в каждую программу шейдера. Похоже, вы собираетесь это сделать с % vertex % ? Но я не знаком с этим синтаксисом.

Этот пиар, как он есть, похоже, идет в обратном направлении. Помимо того, что вы знаете, где вам нужно подключиться, вам также необходимо быть явным и манипулировать строками, повторяя часть работы, которую средство визуализации уже делает за вас. Также, если вы зададите еще один вопрос помимо первого

Как мне использовать функцию, объявленную в чанке commons, внутри функции, которую я внедряю?

вы можете обнаружить, что выполняете больше манипуляций со строками, чем просто добавляете униформу ко всему шейдеру.

Мне пришлось немного подумать и просмотреть большинство шейдеров, чтобы понять, что uv_pars_vertex - удобное место для расширения моего материала.

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

Мы открываем дверь для взлома встроенных материалов, но я не думаю, что мы сможем обеспечить надлежащую «поддержку». Пользователь должен знать, что есть вероятность, что что-то сломается.

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

Вероятно, потребуется реорганизовать его по мере продвижения, но что-то в этом роде:

#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

Конечно, ifdefs и includes, вероятно, не будут работать так. Но я вижу, что с другим подходом это относительно просто. Мы могли бы всегда добавлять #include <some_phase> с пустыми строками. Что-то может помочь управлять тем, что запускается с помощью #ifdef s. Так, например, если вы предоставляете логику для фазы «vertex_transformation», шейдер определит все, что вы предоставите. В целом было бы полезно документировать, что делает каждый фрагмент с помощью GLSL. Не углубляясь в абстракцию, я думаю, было бы здорово иметь эту карту того, как сейчас структурированы чанки.

Мы открываем дверь для взлома встроенных материалов, но я не думаю, что мы сможем обеспечить надлежащую «поддержку». Пользователь должен знать, что есть вероятность, что что-то сломается.

У нас уже есть определения, доступные для каждого материала. https://github.com/mrdoob/three.js/issues/10764 предлагает изменить THREE.ShaderLib а затем определить свойство .defines на экземпляре Material . Конечно, свойство не было задокументировано, пока я не написал запись (но вы ее одобрили :)), и все еще не определено .

Но то, как это работает в настоящее время и наследуется каждым материалом, он поддерживается - если вы поместите в него какой-либо материал, это это может что-то сломать .

Не пытаюсь быть умницей, а пытаюсь привести пример. Я в основном использовал определения с ShaderMaterial но это влияет на все материалы просто потому, что WebGLRenderer работает. Нет никакой логики, говорящей:

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

Я не совсем уверен, можно ли его сломать, мне нужно попробовать.

С учетом сказанного, я хотел бы иметь возможность сломать вещи, если это даст большую выгоду. С помощью довольно инкапсулированного и крошечного фрагмента GLSL я мог легко изменить свою версию из трех, чтобы по-другому отображать карты нормалей. Взять нормаль и преобразовать ее, чтобы получить другую нормаль, очень просто, если мы не начнем делать компьютерную графику совершенно другим способом, я не вижу, чтобы это когда-либо ломалось :)

Другой пример - создание экземпляров:

о, я бы хотел использовать ANGLE_INSTANCED_ARRAYS, три, похоже, не поддерживают его с помощью теней или материалов PBR, позвольте мне просто добавить эти две строки и заставить их работать для меня "

Я рад видеть, что эта функция наконец-то появилась в three.js :). Да, я также создал yet another material modifier PR (https://github.com/mrdoob/three.js/pull/7581) Он был настолько стар, что код больше не актуален, но в основном вводит хуки вроде @mrdoob предлагает, а затем просто замените их своим собственным кодом.
Мне нравится идея предопределенных хуков, поскольку легко понять, что вы изменяете, поскольку я считаю, что большинство людей, которые хотят использовать эту функцию, хотят «слегка» изменить материал по умолчанию, а не полностью его переписать.

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

@fernandojsg Если бы вы могли проверить # 10791, оставить отзыв, это кажется очень похожим на то, что вы сделали, за исключением того, что я поставил дополнительный крючок сразу за пределами main (что-то вроде вашего "pre_vertex") и немного больше управления.

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

Не пытаюсь быть умницей, а пытаюсь привести пример. Я в основном использовал определения с ShaderMaterial но это влияет на все материалы просто потому, что WebGLRenderer работает.

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

Повторное чтение # 7581 ... Поверх https://github.com/mrdoob/three.js/commit/e55898c27a843f69a47e602761c60d9bbe91ee35 мы можем добавить % HOOKS % а затем создать такой класс:

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;

};

Тогда мы можем сделать это:

`` js
var material = new THREE.ExtendedMaterial (
новый THREE.MeshBasicMaterial (),
{вершина: 'transformed.x + = sin (position.y) / 2.0;' }
);

Итак, вопрос в том, какие у нас должны быть крючки?

VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

FRAGMENT_UNIFORMS
INPUT_FRAGMENT
OUTPUT_FRAGMENT

@mrdoob В качестве примера вы можете увидеть в этой сути, где код должен быть вставлен в вершинный шейдер MeshBasicMaterial для поддержки создания экземпляров.

А вот и остальные материалы :)

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

VERTEX_UNIFORMS звучит слишком самоуверенно, куда добавить функцию, атрибут или переменную? PRE_VERTEX или что-то в этом роде?

NORMAL_MAP обязательно нужен

vert:

PRE_VERTEX
VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

UV
fragment:

PRE_FRAGMENT
INPUT_FRAGMENT
LIGHT
NORMAL

OUTPUT_FRAGMENT

VERTEX_UNIFORMS звучит слишком самоуверенно, куда добавить функцию, атрибут или переменную? PRE_VERTEX или что-то в этом роде?

Хм, самоуверенный? Как так?

attribute vec4 aMyAttribute; не униформа :)

float myRand( vec4 foo ) { /*logic*/ return vec4(bar,baz)} не униформа

varying vec3 vMyVarying; тоже не униформа

либо не во множественном числе, либо вообще не униформа

Каков вариант использования «вставки» атрибутов?
Разве вы не должны использовать вместо этого geometry.addAttribute( 'aMyAttribute', ... ) ?

Я не знал, что вам не нужно это декларировать. Еще оставляет вариации и функции?

Еще оставляет вариации и функции?

Хорошая точка зрения.

вам действительно больше не нужно объявлять атрибуты в GLSL? Я думал, что это больше похоже на униформу, которую вы объявляете в GLSL, но не должны вводить ее в JS.

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

Мне нравятся последние изменения, все еще просто и элегантно. Я согласен с @pailhead, что нам нужно место для размещения кода в глобальной области видимости. В https://github.com/mrdoob/three.js/pull/7581 у меня было глобальное preMainVertex preMainFragment чтобы вы могли использовать их для изменения функций. Вероятно, имя вроде globalVertex/Fragment или подобное могло бы быть лучше.

GLOBAL_VERTEX и GLOBAL_FRAGMENT звучат хорошо 👌

именно так я назвал их в другом PR 😆 👍

@pailhead Но теперь вы знаете секрет, как объединить свой код: просто откройте PR, оставьте его там на некоторое время, затем подождите, пока @mrdoob в конце концов подумает о том, чтобы сделать что-то подобное, дождитесь его PR, а затем просто прокомментируйте там, ссылаясь на точное так же, как вы это делали в своем PR, чтобы он мог подумать, что это его идея, он добавит ее, объединит ... прибыль!

image

Серьезно, мне тоже нравятся эти имена, и я считаю, что мы готовы к работе, или я что-то упускаю? Попробую преобразовать пример из моего PR в этот новый код;)

Часть, касающаяся ввода кода, в порядке, временная шкала как бы отключена. №7581 был предложен почти два года назад. Не упоминался в https://github.com/mrdoob/three.js/issues/10789 , поэтому, вероятно, исчез с радаров. Я бы его толкнул или раздвоил.

Даже при импорте модулей и выборе трех собственных модулей люди не слишком довольны использованием неофициальных основных модулей. Гораздо проще сказать: «Эй, есть пакет npm, который обрабатывает foo». Вот почему я считаю, что такие PR важны, три - недостаточно гибкие.

выгода!

Какая прибыль? 😆

Часть, касающаяся ввода кода, в порядке, временная шкала как бы отключена. №7581 был предложен почти два года назад. Не упоминался в https://github.com/mrdoob/three.js/issues/10789 , поэтому, вероятно, исчез с радаров. Я бы его толкнул или раздвоил.

Какие? Вы не проверили все открытые PR перед созданием нового? # трололол

правда :)

ок, набери в списке и давай сделаем это как можно скорее: D

Будет ли этот шейдер (например):

#include <shadowmap_pars_vertex>

void main() {

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

}

выглядят так:

#include <shadowmap_pars_vertex>

void main() {

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

}

Умм, я не уверен, заменить ли их все или просто ввести код, оставив часть текущего поведения:

#include <shadowmap_pars_vertex>

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

Мой первоначальный подход заключался в том, чтобы ввести дополнительный код, оставив остальную часть включения нетронутой.

Итак, % hook % предназначен только для фактических хуков, если кто-то хочет заменить <begin_vertex> он все равно должен искать его в строке, т.е. не будет крючка begin_vertex ?

В моем PR я просто хотел немного изменить поведение материала, как правило, сделать некоторые цветовые коррекции после того, как fs был полностью выполнен, или преобразование vs, подобное тому, которое @mrdoob показал в своем примере.
Но да, если мы хотим полностью заменить каждое из включений, мы должны использовать хуки вместо включений и добавить логику для внедрения самого включения в случае, если вы не связали крючок.

Я хотел бы сделать это для нормалей (не использовать производные, а использовать фактическую информацию TBN из 3ds max, maya или где-либо еще, где создается карта нормалей). Адаптация MeshPhongMaterial для работы с экземплярами также потребовала полной замены некоторых фрагментов, но это похоже на крайний случай.

Это те два, о которых я могу думать, интересно, есть ли еще.

Итак, % hook % предназначен только для фактических хуков, если кто-то хочет заменить <begin_vertex> он все равно должен искать его в строке, т.е. не будет крючка begin_vertex ?

Вы можете заменить оба. Текущий код уже позволяет заменять что угодно. %HOOK% - для удобства.

Как я упоминал в №11562, как насчет концепции сменных блоков шейдера?

Пример:

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};

Вот насколько простым может быть код перехода между несколькими текстурами на одном материале.

Его можно легко реализовать и избежать ненужного кода.

@ sasha240100

https://github.com/mrdoob/three.js/pull/10791 выглядит в точности так, как вы предлагаете, за исключением того, что parts называются shaderChunks и не воспринимаются как аргумент.
: roll_eyes: 😆

Вам также не нужен словарь frag{} , поскольку фрагменты уже отмечены. У всех есть _fragment и _vertex .

Этот делает то же самое, за исключением того, что вам нужно искать в строке, чтобы найти то, что вам нужно заменить.

@pailhead Да, давайте оставим frag: {} . Я использовал его как аргумент, потому что он должен быть передан до объединения шейдера материала. В этом случае мы можем просто избежать замены существующих частей после объединения.

Фактически мы можем добавить и то, и другое: как аргумент и как свойство. Как color делает

Где это сейчас? Я вижу, что в примере выполняется .replace() и я не вижу перехватчиков / заменяемых фрагментов? @mrdoob

Пока не могу вернуться к этому.

Хотели бы вы сделать пиар для вещи % hook % ?

Вам нужен расширенный материал из комментария выше?

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;

};

Выбор места для крючков и их документирование может оказаться не таким уж тривиальным делом? Я бы, возможно, заменил переданный hooks на chunks и поищу включения с добавлением global_vert и global_frag на данный момент? Что теперь, когда я его написал, звучит так, как будто это не должно быть ответственностью даже троих, но, с другой стороны, в будущем он заглушит THREE.ExtendedMaterial на % hook % s?

Не знаю, известно ли вам о расширении THREE.BAS. Он использует заполнители в исходных материалах, к которым можно получить доступ из настроенного материала в виде массивов, например:

  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);'
    ]
  });

Мне нравится тот факт, что он основан на простых соглашениях: например, vertexInit и fragmentInit будут в верхней части функции main (). vertexParameters и fragmentParameters находятся в верхней части шейдера для определения униформ или атрибутов. vertexFunctions и fragmentFunctions находятся перед функцией main (). И так далее для нормалей, положения и т. Д.

Как вы можете видеть в позиции, вы меняете переменную transformed чтобы изменить окончательную позицию. То же самое для нормалей ( objectNormal ) или цветов ( diffuseColor ), например, во фрагментном шейдере.

Так выглядит материал Фонга, только добавляет заполнители: https://github.com/zadvorsky/three.bas/blob/master/src/materials/PhongAnimationMaterial.js

Привет, ребята, я пытался следить за конвоем, но, честно говоря, я проиграл, так как я пришел из Front end, а не из разработчика игры.

Я пытаюсь загрузить MTL и получаю:

material.onBeforeCompile.toString () не определен.

Мой файл MTL изначально указывал на файл .psd, и я изменил его на один из немногих png, содержащих ту же папку (нормальный). Я думаю, что невозможно загрузить psd, но в любом случае даже с psd не работает.

# 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

Вывод консоли:

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)

и код, который я использую, в основном:

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 );

}

Это проблема в модели? через три? в загрузчике мтл? у него есть решение?

Любая помощь будет оценена по достоинству.
Спасибо.

Должны ли мы делать что-то вроде material.shader = shader в material.onBeforeCompile(shader=>...) ?

Я думаю, что это несколько неуклюже, возможно, в какой-то момент стоит пересмотреть другое предложение :)

Это вызвано этим, но что заставляет Material не выполнять эту функцию?
https://github.com/mrdoob/three.js/blob/35a26f178c523514673d992da1aece23c1cfca6e/src/renderers/webgl/WebGLPrograms.js#L244

@mrdoob

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

Мне не удалось воссоздать это с помощью этой скрипки
https://codepen.io/anon/pen/KQPBjd

Но с этим кодом в моем примере

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

}
*/

Но это не ошибка, если я добавлю в сцену еще один материал, который разделяет другие включения и определения.

Это связано с этим:

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

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

array.push( material.onBeforeCompile( obtainShaderSomehow ) )

https://github.com/mrdoob/three.js/pull/10791 решил это так:

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

Не могли бы вы пересмотреть другой запрос на перенос, если я удалю заполнители / хуки global_vertex и global_fragment ? С ними было затронуто много файлов, без них меньше кода. Не три строчки, но хеширует правильно :)

Прокомментируйте #41 в этой скрипке https://codepen.io/anon/pen/KQPBjd
и материал другой сетки в сцене изменится.

Включено ли это в недавний выпуск или все еще находится в разработке?

@mrdoob Я думаю, что сериализовать Материалы, взломанные с помощью .onBeforeCompile() , сложно (или невозможно). Как вы думаете, следует ли пользователям использовать ShaderMaterial если им нужны полностью функциональные (рендеринг, копирование, клонирование, сериализация и т. Д.) Модифицированные встроенные материалы?

_Пожалуйста, прочтите нижеследующее как можно лучше, конструктивнее и без конфронтации _ :)

@takahirox, почему на это продолжают претендовать?

Считаю сложно (или невозможно) сериализовать взломанные Материалы

Когда вы «взламываете» материалы с помощью onBeforeCompile вы просто добавляете униформу, которая такая же, как и с любым другим материалом. Нет никакой разницы. Если вы можете сериализовать material.color почему вы не можете сериализовать material.userData.myColor ?

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

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


Это только что пришло в голову? Существует ли какой-то тест, который доказывает, что это сложно или невозможно? Нравиться:

runSerializationTest( someMaterialWithOnBeforeCompile ).expect( something )

Поскольку метод onBeforeCompile может содержать очень произвольный код, определенно невозможно надежно сериализовать этот обратный вызов. Например, метод может создать синхронный запрос XMLHttpRequest и что-то сделать с результатом. 😅

Однако ... учитывая то, как это используется практически (для исправления шейдеров), я согласен, что это не невозможно. Но простые API (например, fn.toString() ) хакерские, а надежные API более сложны.

Я все еще не понимаю в чем проблема. Как насчет этой точки зрения:

Возьмем rN, в котором было MeshPhongMaterial и не было MeshStandardMaterial .

Вы можете сериализовать MeshPhongMaterial ie. напишите такой JSON:

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

Возьмем rN + 1, в котором есть оба материала:

Вы по-прежнему можете сериализовать оба способа одинаково:

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

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

Нигде вы не сериализовали GLSL из MeshStandardMaterial .

Таким же образом можно сериализовать любой расширенный материал.

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

Десериализация:

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 ) 
}

Что мне здесь не хватает?


Как onBeforeCompile ... сериализуйте этот обратный вызов.

Почему, почему вы даже рассматриваете это :) Я думаю, что в этом суть моего замешательства, почему все думают об этом крючке, он не является частью определения материала, описания, чего-то еще. Это как-то связано с движком, плагинами, приложениями и т. Д., А не с данными.

Пример, который вы привели выше, мне нравится, он просто требует, чтобы пользовательский код, десериализующий материал, знал о существовании HamburgerPhongMaterial? У меня нет проблем с этим, это не обязательно даже требует изменений API, если вы воспользуетесь подходом, аналогичным # 14099, и переопределите toJSON и fromJSON.

Может быть, мы по- другому истолковали вопрос

... трудно (или невозможно) сериализовать Материалы, взломанные с помощью .onBeforeCompile (). Как вы думаете, следует ли пользователям использовать ShaderMaterial, если они хотят полностью функциональные (рендеринг, копирование, клонирование, сериализация и т. Д.) Модифицированных встроенных материалов?

Я попробую разделить это на части:

  • если пользователь вызывает toJSON () для простого MeshStandardMaterial, где GLSL, измененный с помощью onBeforeCompile , изменения в GLSL не будут автоматически сериализованы.
  • если пользователь сериализует MeshStandardMaterial с некоторыми дополнительными свойствами, указывающими на изменение GLSL, и загружает его с помощью специального кода, который знает, что делать с этими дополнительными свойствами, тогда, безусловно, это нормально.
  • различные другие параметры (ShaderMaterial, NodeMaterial, glTF's KHR_techniques_webgl , ...) доступны для сериализации произвольных пользовательских материалов.

Если дело, о котором мы говорим, не из тех, то, наверное, я неправильно понимаю. Если речь идет о №14099, я (все еще) за слияние.

Хммм, на самом деле я не различал первых двух. Я пытаюсь придумать что-нибудь, для чего не потребуется униформа, например, gl_FragColor.xyz /= gl_FragColor.a; . Но я не могу представить сценарий, в котором вы хотели бы сериализовать что-то вроде этого ( вместе с материалом? ). Похоже, что это попало бы в сферу применения какого-то средства визуализации эффектов или чего-то подобного, а не материальных данных.

materials = loadSerializedMaterials()

effect = new Effect()

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

Второй пример - это то, что я всегда имел в виду. Наряду с различными входными данными, сериализуйте подсказку о том, что с ними делать userData.extension === 'hamburger' .

Думаю, я плохо написал. Я хотел упомянуть

  • Встроенный материал, взломанный с помощью .onBeforeCompile() не копируется, не клонируется, не сериализуется правильно в том же API, что и встроенные материалы ». Это не взломанный материал.
  • Если пользователи хотят получить правильный результат, им потребуется дополнительная работа на стороне пользователя.
  • Если пользователям нужен взломанный встроенный материал с базовым API Three.js без какой-либо дополнительной работы со стороны пользователя, им следует подумать о других вариантах, например ShaderMaterial .

Мой вопрос касается политики .onBeforeCompile() а не конкретного случая. Хотел уточнить ограничение и добавить заметку в документ.

Если я забуду о three.js и всей системе рендеринга, я думаю, что увижу смысл. Как общий объект, если у него есть .clone() вы хотите иметь возможность клонировать и иметь согласованные ожидания. Проблема в том, что оба решения (клонировать это, а не клонировать), похоже, имеют действительный аргумент. Вы не клонируете слушателей, но вы также отчасти ожидаете, что что-то, что влияет на рисование, будет последовательным.

Я все еще думаю, что мое предложение, приведенное выше, является допустимым способом сделать это, поскольку я столкнулся с той же проблемой в других областях (например, совместное использование буферов глубины между целями также может быть решено с помощью ownThing || outsideThing .

Вместо кеширования функции, которая не работает, вы в основном кешируете userData но ее подмножество. Нет смысла кэшировать userData.hamburger: 'rare' но он делает userData.effectOn: true". The ownInput | ownChunks | ownWhatever` решит эту проблему.

Это также решает проблему:

Поскольку метод onBeforeCompile может содержать очень произвольный код, невозможно надежно сериализовать этот обратный вызов.

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

По сути, это шаг pre three parse , на котором у вас есть возможность самостоятельно проанализировать шейдер вместе с обязательным интерфейсом uniforms:{} , поскольку он недоступен в Material ( defines:{} с другой стороны, и оба являются братьями и сестрами в ShaderMaterial ).

Как вы это делаете, т.е. каким бы ни был произвольный код, не имеет значения. Он ничего не возвращает, но изменяет shader:{} и затем использует его.

Подходы, которые я предлагаю:

  1. Добавьте ownThing к различным классам. THREE.WebGLRenderTarget может иметь .ownStencilDepthBuffer . Здесь, как я предлагал выше, это будет какая-то версия ownGLSL:{} ownInput | ownUniforms . Вы устраняете необходимость в произвольном коде для изменения объекта shader:{} :

Учитывая такие входные данные, которые исходят из WebGLRenderer (супер частный):

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

ядерное оружие это:

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)

Использовать этот:

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. Сделайте WebGLRenderer не знаю ни о чем, кроме ShaderMaterial .

Где бы вы ни были

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

Иметь

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

const mat = chunkMaterial.compile()

Tl: dr приведенного выше материала заключается в том, что пользователя не волнует конкретный момент времени, когда происходит "компиляция" (синтаксический анализ) . Я думаю, что их больше волнует то, что происходит, и то, что происходит, может быть выделено для небольшого, если не единственного варианта использования in: shaderString, out: shaderString . Я думаю, что причину, по которой это должно быть сделано на этом конкретном этапе конвейера, стоит изучить и понять. Кажется, это больше мешает, чем помогает.

@mrdoob Я думаю, что сериализовать Материалы, взломанные с помощью .onBeforeCompile() , сложно (или невозможно). Как вы думаете, следует ли пользователям использовать ShaderMaterial если им нужны полностью функциональные (рендеринг, копирование, клонирование, сериализация и т. Д.) Модифицированные встроенные материалы?

Я не ожидал (рассматривал) использование onBeforeCompile() для сериализации ...

Меня просто укусило использование onBeforeCompile. У меня есть вспомогательный класс шейдеров, который позволяет мне определенным образом вносить изменения во встроенные шейдеры. Для этого я использую метод onBeforeCompile. По-видимому, как указано в первом комментарии, WebGLProgram использует onBeforeCompile.toString () в качестве хеша. Но поскольку моя функция является универсальной (она заменяет части вершинных и фрагментных шейдеров переменными), onBeforeCompile.toString () не выглядит по-разному для разных шейдеров. Это означало, что все мои шейдеры кэшировались как одна и та же программа. Вызов eval и uuid, а теперь мои функции выглядят по-разному. Потребовалась целая вечность, чтобы понять это.

@donaldr См. https://github.com/mrdoob/three.js/issues/13192. Я думаю, что было бы хорошо задокументировать эти ограничения.

не могли бы вы рассказать, как вы используете eval? Я думал об этом, но это показалось мне слишком грязным. Вы могли бы добавить const fdscxnmdrek435rkjl в верхнюю часть тела, а затем eval?

Это означало, что все мои шейдеры кэшировались как одна и та же программа. Вызов eval и uuid, а теперь мои функции выглядят по-разному. Потребовалась целая вечность, чтобы понять это.

Это звучит очень неприятно, извините. 😞 Если изменения, которые вам нужно было внести, кажутся имеющими смысл в самой библиотеке, пожалуйста, не стесняйтесь открывать вопросы.

пожалуйста, не стесняйтесь открывать вопросы.

Вы бы рассмотрели возможность повторного открытия существующих? # 13192 кажется таким же, но он был закрыт: crying_cat_face: Было бы здорово, если бы там был опубликован обходной путь, если бы это считалось функцией.

Хм...

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

^ Будет ли это работать? Я никогда не использовал eval .

Введение Material.customProgramCacheKey () через # 17567 гарантирует, что разработчики теперь имеют возможность не использовать шейдерные программы для материалов, измененных с помощью onBeforeCompile() с условными операторами.

Пожалуйста, обсудите другие существующие проблемы или улучшения в контексте onBeforeCompile() в новых потоках (например, # 13446).

Была ли эта страница полезной?
0 / 5 - 0 рейтинги