Three.js: 材料:添加 onBeforeCompile()

创建于 2017-06-09  ·  75评论  ·  资料来源: mrdoob/three.js

多年来,一个共同的功能要求是能够修改内置材料。 今天我意识到它可以以类似于Object3DonBeforeRender()

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

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

在所有情况下,您都必须依赖内置着色器的内部组织,以免从一个版本到下一个版本发生太大变化。 每个包含都已经是一个伪装的函数,带有一个错误指定的签名。 我怀疑提供功能级别而不是包含级别的覆盖在双方都会更清晰和可维护。

今天我想..

好的! 我在 2 月 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 我希望我能写出更好的描述:)


我必须承认,我不知道这里发生了什么。 我想我知道有人放弃了对三个的贡献,当时不理解,但现在我觉得有点令人沮丧。

令人沮丧的主要是因为我在阅读第一段时有一种似曾相识的感觉:

...与 Object3D 的 onBeforeRender() 类似的方法。

这完全相同的事情发生在onBeforeRender() https://github.com/mrdoob/three.js/pull/9738 上,当时花了一年的时间才说服了该功能。

天啊,两次,同一个开发者? 我显然在这里做错了什么,但是什么? 第一次是签入构建文件,我不知道该怎么做,然后就忘记了。 但是这一次我在这个拉取请求之上,现在有一个冲突,但很容易解决,几个月没有冲突。

不要误会我的意思,我不认为这里的作品是虚荣的,我想我只是想通过人们来运行想法并获得反馈。 @unconed很明显,您对这个问题很熟悉,并且您可能尝试过不同的方法。 是什么导致像您这样的用户错过了另一个 PR?

我对每个材质的 shaderlib 感觉很好,但我确实在渲染器中找到了一个函数,它将整个着色器看作/比较为字符串以找出缓存,对此感觉并不是那么好。 希望得到一些反馈...

这个例子有缺陷吗? 头部可能比我的抽象尖刺球更清楚发生了什么,但整个示例使用覆盖更多地面的阴影。 出于某种原因它很重,但我认为这是因为许多顶点上的 sin/cos。

我想如果你从头开始......尽管场景工具包绝对可怕,但这个 api 非常好:

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

尽管有可怕的记录,但我找不到更感兴趣的部分,但基本上他们已经在着色器的许多不同阶段抽象了钩子。

今年早些时候,我提出了一个拉取请求,它似乎做了一些类似的事情,如果不是完全一样的事情:

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

似乎唯一的区别是你是通过回调来做的? 我利用了 WebGLRenderer 中未使用的参数。

很抱歉还不能处理那个公关@pailhead。 我想我的方法的主要优点是它只需要 3 条新线。

话虽如此,我现在将花一些时间研究您和@unconed 的建议。

这个绝对感觉更优雅,只有三行。 我在另一个中遵循不同的模式。 本来想说它可能有点冗长,但不太确定。 我想另一个的唯一优点是几个月前去还不错:)

天啊,两次,同一个开发者? 我显然在这里做错了什么,但是什么?

对于那个很抱歉。 我唯一能想到的就是我可能不知所措。 也许长时间的讨论或复杂的 PR 会消耗我太多的精力,所以我决定把它留到以后,转向更简单的 PR。

不仅仅是你@pailhead。 @bhouston有很多这样的 PR,甚至是@WestLangley和 @Mugen87。

同样,不是我不喜欢 PR,而是 PR 需要一些我当时无法提供的关注。 一个很好的例子是 Instancing PR。 我设法找到时间通读它,做了我自己的实验并提出了简化建议。 希望我能很快重新审视它。

管理这一切并给予每个 PR 应有的关注是很困难的。

这个例子有缺陷吗? 头部可能比我的抽象尖刺球更清楚发生了什么,但整个示例使用覆盖更多地面的阴影。 出于某种原因它很重,但我认为这是因为许多顶点上的 sin/cos。

现在你提到它......是的,放大时在新的MacBook Pro上以10fps运行😮

我认为是阴影和兰特,sin cos 和其他东西,但是是的,它看起来太受打击了。

不管怎样,我很惊讶你把它分成三行,我太关注材料参数如何变成制服并遵循这种模式。 不同之处在于我提供了一个替代的ShaderLib像字典,所以#include <>带来了不同的代码。 在发生这种情况之前删除包含,并用 glsl 替换它。 我认为如果你在着色器周围返回某种包装器,也许这种语法会更清晰(只需提供块名称 + glsl,而不是replace() 。但它最终可能看起来更像另一个.

拥有这个真的很好,我没有使用过那么多 3d 引擎,但是 unity 和 scene kit 似乎有这样的东西。

@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/10791Material.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)

_它只是一个chunk名称的字典,和uniforms对象。 这里的字符串操作更像是“我想重用这个块,并在它上面做一些事情”_

这个公关:

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是进行材质扩展的一个方便的地方。

最好从这样的东西转向抽象,有一个lightingPhaseobjectTransformationperspectiveTransformation等钩子。 或者至少包含在每个着色器程序中的guaranteedToBeAboveMainButBelowCommonsAndExtensionCalls虚拟块。 看起来你打算用% vertex % ? 但我不熟悉这种语法。

这个公关,就这样,似乎在朝着相反的方向发展。 除了知道需要点击的位置之外,您还需要明确和操作字符串,重复渲染器已经为您完成的一些工作。 另外,如果你在第一个问题之外再问一个问题

我如何在我正在注入的函数内使用在公共块中声明的函数?

您可能会发现自己进行了更多的字符串操作,而不仅仅是将制服添加到整个着色器中。

我不得不考虑一下并查看大多数着色器,以找出uv_pars_vertex是进行我的材质扩展的方便位置。

我认为除非我们过度设计,否则没有办法解决这个问题。 % vertex %的想法是尝试通过命名钩子来帮助解决这个问题,并或多或少地告诉您正在注入代码的状态,但是很难记录哪些变量在什么时候可用.

我们正在为内置材料黑客打开一扇门,但我认为我们无法提供适当的“支持”。 用户应该意识到事情可能会破裂。

我试图在另一个 PR 中解决它。 我至少为功能、变量、属性和制服添加了一个虚拟钩子。 许多 GLSL 逻辑已经发生在结构上,scenekit 记录了每个变量(虽然再也找不到相同的文档)。

随着我们的进行,它可能需要重构,但大致如下:

#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触发的内容。 例如,如果您为“vertex_transformation”阶段提供逻辑,着色器将定义您提供的任何内容。 用 GLSL 记录每个块的作用总体上是有帮助的。 在不进一步抽象的情况下,我认为现在有这张图来了解块的结构会很好。

我们正在为内置材料黑客打开一扇门,但我认为我们无法提供适当的“支持”。 用户应该意识到事情可能会破裂。

我们已经在每种材料上都有可用的定义内容。 https://github.com/mrdoob/three.js/issues/10764建议修改THREE.ShaderLib然后定义一个.defines上的实例属性Material 。 当然,在我写下这个条目之前,该属性没有记录(但你批准了它:)),并且仍然是undefined

但是当前的工作方式,并且被每个材质继承,它是受支持的——如果你把东西放进去,它会影响那个材质的着色器。 此外,如果您碰巧定义了一些已经是着色器库一部分的东西

不是想成为一个聪明的人,而是想举个例子。 我主要使用ShaderMaterial定义,但它确实影响所有材料,只是因为WebGLRenderer是如何工作的。 没有逻辑说:

如果你是一个更聪明的表面,比如​​像Phong这样的材料的抽象,那么不要考虑定义。 因为三的设计并不是为了覆盖它的着色器块,而且 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我忘记了你的 PR 😚。 我认为你的 PR 看起来很像我开始看到这个工作的方式。 潜意识?

不是想成为一个聪明的人,而是想举个例子。 我主要使用ShaderMaterial定义,但它确实影响所有材料,只是因为WebGLRenderer是如何工作的。

我很高兴该库可以通过这种方式进行 hack,但是我们不应该在内部使用滥用功能来处理不适合的事情。 很容易以这种方式结束脆弱的代码。

再次阅读 #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(
新三.MeshBasicMaterial(),
{顶点:'transformed.x += sin(position.y)/2.0;' }
);

那么问题来了,我们应该有哪些钩子呢?

VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

FRAGMENT_UNIFORMS
INPUT_FRAGMENT
OUTPUT_FRAGMENT

@mrdoob例如,您可以在此要点中看到需要将代码注入MeshBasicMaterial顶点着色器以支持实例化的地方。

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 中声明属性了吗? 我认为它更像 uniforms ,你在 GLSL 中声明但不必在 JS 中输入它。

我认为这个列表可以很快变大,当涉及到某些事情时,代码变得非常分散。 制作法线贴图的一种方法涉及两个额外的属性,vert 和 frag 中的变量和转换逻辑。

我喜欢最新的变化,它仍然简单而优雅。 我同意@pailhead 的观点,我们需要一个地方将代码放在全局范围内。 在https://github.com/mrdoob/three.js/pull/7581 中,我将preMainVertex preMainFragment全局注入,以便您可以将它们用于不同的功能。 可能像globalVertex/Fragment或类似的名称可能会更好。

GLOBAL_VERTEXGLOBAL_FRAGMENT听起来不错👌

正是我在另一个 PR 中给它们命名的方式 😆 👍

@pailhead但是现在您知道合并代码的秘诀了:只需打开一个 PR,将其保留一段时间,然后等待@mrdoob最终考虑做类似的事情,等待他的 PR,然后在那里发表评论,引用确切的内容就像你在 PR 上所做的那样,这样他就可以认为这是他的想法,他会添加它,合并......利润!

image

现在我真的很喜欢那些名字,我相信我们已经准备好了,或者我错过了什么? 我会尝试将示例从我的 PR 转换为这个新代码;)

关于代码进入的部分很好,时间线有点偏离。 #7581 是大约两年前提出的。 在https://github.com/mrdoob/three.js/issues/10789 中没有被引用,所以可能没有被注意到。 我会撞到它或分叉它。

即使在导入模块并挑选自己的三个模块时,人们也不太乐意使用非官方的核心模块。 更容易说,“嘿,有一个处理 foo 的 npm 包”。 这就是为什么我觉得这样的 PR 很重要,三个不够灵活。

利润!

什么利润? 😆

关于代码进入的部分很好,时间线有点偏离。 #7581 是大约两年前提出的。 在https://github.com/mrdoob/three.js/issues/10789 中没有被引用,所以可能没有被注意到。 我会撞到它或分叉它。

什么? 在创建新 PR 之前,您没有检查所有打开的 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 完全执行后进行一些颜色校正,或者类似于@mrdoob在他的示例中显示的转换。
但是是的,如果我们想完全替换每个包含,我们应该使用钩子而不是包含,并添加逻辑来注入包含本身,以防您没有绑定钩子。

我想对法线执行此操作(不使用导数,并使用来自 3ds max 或 Maya 或生成法线贴图的任何地方的实际 TBN 信息)。 使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};

这就是在 1 个材质上的多个纹理之间的过渡代码是多么简单。

它可以轻松实现并避免编写不必要的代码。

@sasha240100

https://github.com/mrdoob/three.js/pull/10791看起来和你建议的完全一样,除了parts被称为shaderChunks并且它不把它当作争论。
:roll_eyes: 😆

您也不需要frag{}字典,因为块已经被标记。 他们都有_fragment_vertex

除了您必须搜索字符串以找到您需要替换的内容之外,这个功能也是如此。

@pailhead是的,让我们放弃frag: {} 。 我使用它作为参数,因为它应该在连接材质着色器之前传递。 在这种情况下,我们可以简单地避免在连接后替换现有部件。

我们实际上可以同时添加:作为参数和作为属性。 就像color那样

这是现在在哪里? 我看到示例正在执行.replace()并且我没有看到挂钩/可替换块? @mrdoob

还没能回到这个问题。

你想为% hook %事情做一个 PR 吗?

你想要扩展材料,从上面的评论?

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_vertglobal_frag ? 现在我写了它,听起来它甚至不应该是三个人的责任,但另一方面,它会在未来为% hook % s 存根THREE.ExtendedMaterial

我不知道你是否知道 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);'
    ]
  });

我喜欢它基于简单约定的事实:例如, vertexInitfragmentInit将位于 main() 函数的顶部。 vertexParametersfragmentParameters位于着色器的顶部,用于定义制服或属性。 vertexFunctionsfragmentFunctions位于 main() 函数之前。 等等法线,位置等......

正如您在位置中看到的,您更改变量transformed以更改最终位置。 例如,片段着色器中的法线 ( objectNormal ) 或颜色 ( diffuseColor ) 类似。

这是 Phong 材质的样子,只添加了占位符: https :

嗨,伙计们,我正试图关注会议,但老实说我输了,因为我来自前端而不是游戏开发者。

我试图加载一个 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 );

}

这是模型中的问题吗? 在三? 在 mtl 装载机中? 它有解决方案吗?

任何帮助,将不胜感激。
谢谢。

我们是否必须在material.onBeforeCompile(shader=>...)执行类似material.shader = shader material.onBeforeCompile(shader=>...)

我认为这感觉有点笨拙,在某些时候可能值得重新审视其他建议:)

@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_vertexglobal_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 则更加复杂。

我还是不明白问题所在。 如何看待这个观点:

以具有MeshPhongMaterial而没有MeshStandardMaterial rN为例。

您可以序列化MeshPhongMaterial即。 像这样写一个 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'
}

您没有从MeshStandardMaterial序列化 GLSL。

以同样的方式,序列化任何扩展材料。

//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 的存在? 我对此没有任何问题,如果您采用类似于 #14099 的方法并覆盖 toJSON 和 fromJSON,它甚至不一定需要更改 API。

也许我们在这里对@takahirox的问题有不同的解释?

...很难(或不可能)序列化使用 .onBeforeCompile() 破解的材料。 如果用户想要全功能(渲染、复制、克隆、序列化等)修改的内置材质,您认为用户应该使用 ShaderMaterial 吗?

我会试着把它分开:

  • 如果用户在普通 MeshStandardMaterial 上调用 toJSON(),其中 GLSL 已被其onBeforeCompile修改,则对 GLSL 的更改将不会自动序列化。
  • 如果用户使用一些指示 GLSL 更改的额外属性序列化 MeshStandardMaterial,并使用知道如何处理这些额外属性的自定义代码加载它,那么当然没问题。
  • 各种其他选项(ShaderMaterial、NodeMaterial、glTF 的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 中正确复制、克隆和序列化。 它作为非黑客材料处理。
  • 如果用户想要正确的结果,他们需要在用户端做一些额外的工作。
  • 如果用户希望在没有任何用户端额外工作的情况下使用 Three.js 核心 API 的黑客内置材料,他们应该考虑其他选项,例如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,现在我的函数看起来都不一样了。 花了很长时间才弄清楚这一点。

@donaldrhttps://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

通过#17567 引入Material.customProgramCacheKey()确保开发人员现在有可能不共享着色器程序,用于通过条件语句onBeforeCompile()修改的材料。

请在新线程(如 #13446)中讨论onBeforeCompile()上下文中的其他现有问题或增强功能。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

boyravikumar picture boyravikumar  ·  3评论

ghost picture ghost  ·  3评论

clawconduce picture clawconduce  ·  3评论

akshaysrin picture akshaysrin  ·  3评论

makc picture makc  ·  3评论