PBRCustomMaterial (or PBRMaterial) with custom albedo and normal

Hi. I’m trying to create a splatmap material. I have something working based on PBRCustomMaterial that gives the correct albedo. However, I cannot seem to find how to define a custon normal.

I have seen this topic: [SOLVED] - BABYLON.TerrainMaterial Shader BUMP Support - Questions & Answers - HTML5 Game Devs Forum . I think this solution is fantastic, but I am working on ES6 pulling BabylonJS via NPM, and I can’t wrap my head around how to define a custom shader, reusing as much as possible of PBRMaterial, in my environment.

I’d be grateful if someone can point me in a direction forward. Ideally using PBRCustomMaterial to provide my normals, or to an example or steps on how to base a material off PBMaterial.

Thank you!

I think @nasimiasl and @MackeyK24 might have done exactly that ???

hey i have shaderbuilder that work on es6 also can define PBR material on that but i think @MackeyK24 have more experiance using customPBRmaterial on that maybe if that not work i can make sample for pbr in ShB

What do you mean provide your own normals???

I believe the parameter for normal in the shader is called normalW … you would make a custom shader block after one of the defined markers with what you want to do to normalW

Ummm… well I mean, since I’m atlasing several texrtures, I’d like to “replace” the value that comes from the bumpTexture, so it is then used for the rest of the lighting calculations.

I can replace the albedo and other values (Fragment_Custom_Albedo, Fragment_Custom_Alpha, Fragment_Custom_MetallicRoughness…), but I am unsure if I could alter the normal (and where).

(I’m a shader “ocasional user”… meaning that I try to avoid custom shaders and use standard materials :D, so I might be asking the wrong question or entirely overseeing something.)

Ok, thanks for the hint. I have tried mangling with normalW in the Fragment_Custom_Albedo function. I don’t know if this is quite correct (I need to implement other Fragment_Custom_* , will this apply?).

Anyway, I tried setting a normal up and seemed to have an effect, so I am now grabbing the normal from the appropriate splatmap. It seems to have an effect and I am sure the mapping is correct. But now I guess I need to combine this with whatever value normalW aready had?

I’m blindly assigning the normal from the normalmap, before setting result:

//this.shaderinjectpoint3 += 'normalW = perturbNormal(cotangentFrame, finalNormal' + (this.totalTiles) + ', vBumpInfos.y);';
this.shaderinjectpoint3 += 'normalW = finalNormal' + (this.totalTiles) + ';';
this.shaderinjectpoint3 += 'result = finalColor' + (this.totalTiles) + '.rgb;';

But I don’t know what I’m really doing, or if this is a correct way of altering the normal for all calculations that might come after.

Anyway, the normal from splatmap (ground and tiles below stairs) is somewhat being applied, I’m sure, so that’s some progress. Thanks.

I do the same thing for my Terrain Splatmap Shader that is also use Texture Atlas(s)

No i perturb then blend my normals… Here is a peek at my UniversalTerrainMaterial class i use for my Babylon Toolkit to export Unity Terrains (Using splatmaps and texture atlases).

I made the PBRCustomMaterial so I can have all the #define sections… But i dont really use that class from unity… I wrote UniversalTerrainMaterial that does the same stuff my PBRCustomMaterial does but it is design to export unity terrains with unity materials.

    /**
     * Babylon universal terrain material pro class
     * @class UniversalTerrainMaterial
     */
    export class UniversalTerrainMaterial extends BABYLON.UniversalAlbedoMaterial {
        public constructor(name: string, scene: BABYLON.Scene) {
            super(name, scene);
            this.enableShaderChunks = true;
        }
        public getClassName(): string {
            return "UniversalTerrainMaterial";
        }
        public getShaderName(): string {
            return "pbr";
        }
        public getShaderChunk(): string {
            return null;
        }
        protected updateShaderChunks(): void {
            const colorName:string = "surfaceAlbedo";
            const splatmapSampler:string = "splatmapSampler";
            const detailsSampler:string = "detailsSampler";
            const normalsSampler:string = "normalsSampler";
            const colorCorrection:number = BABYLON.System.ToLinearSpace;
            // ..
            // Vertex Shader Chunks
            // ..
            if (this.materialShaderChunks.Vertex_Definitions != null && this.materialShaderChunks.Vertex_Definitions !== "") {
                this.materialShaderChunks.Vertex_Definitions = this.formatTerrainVertexDefintions() + this.materialShaderChunks.Vertex_Definitions;
            } else {
                this.materialShaderChunks.Vertex_Definitions = this.formatTerrainVertexDefintions();
            }
            if (this.materialShaderChunks.Vertex_MainEnd != null && this.materialShaderChunks.Vertex_MainEnd !== "") {
                this.materialShaderChunks.Vertex_MainEnd = this.formatTerrainVertexMainEnd() + this.materialShaderChunks.Vertex_MainEnd;
            } else {
                this.materialShaderChunks.Vertex_MainEnd = this.formatTerrainVertexMainEnd();
            }
            // ..
            // Fragment Shader Chunks
            // ..
            if (this.materialShaderChunks.Fragment_Definitions != null && this.materialShaderChunks.Fragment_Definitions !== "") {
                this.materialShaderChunks.Fragment_Definitions = this.formatTerrainFragmentDefintions(splatmapSampler, detailsSampler, normalsSampler) + this.materialShaderChunks.Fragment_Definitions;
            } else {
                this.materialShaderChunks.Fragment_Definitions = this.formatTerrainFragmentDefintions(splatmapSampler, detailsSampler, normalsSampler);
            }
            if (this.materialShaderChunks.Fragment_Custom_Albedo != null && this.materialShaderChunks.Fragment_Custom_Albedo !== "") {
                this.materialShaderChunks.Fragment_Custom_Albedo = this.formatTerrainFragmentUpdateColor(this.terrainInfo, colorName, splatmapSampler, detailsSampler, normalsSampler, colorCorrection) + this.materialShaderChunks.Fragment_Custom_Albedo;
            } else {
                this.materialShaderChunks.Fragment_Custom_Albedo = this.formatTerrainFragmentUpdateColor(this.terrainInfo, colorName, splatmapSampler, detailsSampler, normalsSampler, colorCorrection);
            }
        }
        private formatTerrainVertexDefintions():string {
            return ("\r\n#define TERRAIN_VERTEX_DEFINITIONS\r\n\r\n"
            + "varying vec2 vSplatmapUV;\r\n"
            + "\r\n");
        }
        private formatTerrainVertexMainEnd():string {
            return ("\r\n#define TERRAIN_VERTEX_MAIN_END\r\n\r\n"
            + "#ifdef UV1\r\n"
            + "vSplatmapUV = uv;\r\n"
            + "#endif\r\n"
            + "\r\n");
        }
        private formatTerrainFragmentDefintions(splatmapSampler:string, detailsSampler:string, normalsSampler:string):string {
            return ("\r\n#define TERRAIN_FRAGMENT_DEFNITIONS\r\n\r\n"
            + "varying vec2 vSplatmapUV;\r\n"
            + "uniform sampler2D " + splatmapSampler + ";\r\n"
            + "uniform sampler2D " + detailsSampler + ";\r\n"
            + "uniform sampler2D " + normalsSampler + ";\r\n"
            + "\r\n"
            + "float calculateMipmapLevel(const in vec2 uvs, const in vec2 size)\r\n"
            + "{\r\n"
            + "    vec2 dx = dFdx(uvs * size.x);\r\n"
            + "    vec2 dy = dFdy(uvs * size.y);\r\n"
            + "    float d = max(dot(dx, dx), dot(dy, dy));\r\n"
            + "    return 0.4 * log2(d);\r\n"
            + "}\r\n"
            + "\r\n"
            + "vec4 sampleTextureAtlas2D(const in sampler2D atlas, const in float gamma, const in vec2 tile, const in vec4 rect, in vec2 uvs, in float lod)\r\n"
            + "{\r\n"
            + "    if (lod < 0.0) lod = clamp(calculateMipmapLevel(uvs, vec2(tile.x, tile.x)), 0.0, tile.y);   // Tile Info (tile.xy)\r\n"
            + "    float size = pow(2.0, tile.y - lod);                                                        // Tile Bits (tile.y)\r\n"
            + "    float sizex = size / rect.z;                                                                // Tile Width (rect.z)\r\n"
            + "    float sizey = size / rect.w;                                                                // Tile Height (rect.w)\r\n"
            + "    uvs = fract(uvs);                                                                           // Perfrom Tiling (fract)\r\n"
            + "    uvs.x = uvs.x * ((sizex * rect.z - 1.0) / sizex) + 0.5 / sizex + rect.z * rect.x;           // Tile Position X (rect.x)\r\n"
            + "    uvs.y = uvs.y * ((sizey * rect.w - 1.0) / sizey) + 0.5 / sizey + rect.w * rect.y;           // Tile Position Y (rect.y)\r\n"
            + "    vec4 color = texture2DLodEXT(atlas, uvs, lod);\r\n"
            + "    if (gamma != 1.0) {\r\n"
            + "        color.r = pow(color.r, gamma);\r\n"
            + "        color.g = pow(color.g, gamma);\r\n"
            + "        color.b = pow(color.b, gamma);\r\n"
            + "    }\r\n"
            + "    return color;\r\n"
            + "}\r\n"
            + "\r\n"
            + "vec4 sampleSplatmapAtlas2D(const in sampler2D atlas, const in vec2 tile, const in vec4 rect, in vec2 uvs)\r\n"
            + "{\r\n"
            + "    float size = pow(2.0, tile.y);                                                              // Tile Bits (tile.y)\r\n"
            + "	   float sizex = size / rect.z;                                                                // Tile Width (rect.z)\r\n"
            + "	   float sizey = size / rect.w;                                                                // Tile Height (rect.w)\r\n"
            + "	   uvs.x = uvs.x * ((sizex * rect.z - 1.0) / sizex) + 0.5 / sizex + rect.z * rect.x;           // Tile Position X (rect.x)\r\n"
            + "	   uvs.y = uvs.y * ((sizey * rect.w - 1.0) / sizey) + 0.5 / sizey + rect.w * rect.y;           // Tile Position Y (rect.y)\r\n"
            + "    return texture2D(atlas, uvs);\r\n"
            + "}\r\n"
            + "\r\n"
            + "vec3 blendSplatmapAtlasColors(const in vec4 splatmap, in vec4 color1, in vec4 color2, in vec4 color3, in vec4 color4, in vec3 mixbuffer)\r\n"
            + "{\r\n"
            + "    vec3 buffer1 = mix(mixbuffer, color1.rgb, splatmap.r);\r\n"
            + "    vec3 buffer2 = mix(buffer1, color2.rgb, splatmap.g);\r\n"
            + "    vec3 buffer3 = mix(buffer2, color3.rgb, splatmap.b);\r\n"
            + "    return mix(buffer3, color4.rgb, splatmap.a);\r\n"
            + "}\r\n"
            + "\r\n"
            + "vec3 perturbNormalSamplerColor(mat3 cotangentFrame, vec3 samplerColor, float scale)\r\n"
            + "{\r\n"
            + "    vec3 map = samplerColor.xyz;\r\n"
            + "    map = map * 2.00787402 - 1.00787402;\r\n"
            + "    #ifdef NORMALXYSCALE\r\n"
            + "        map = normalize(map * vec3(scale, scale, 1.0));\r\n"
            + "    #endif\r\n"
            + "    return normalize(cotangentFrame * map);\r\n"
            + "}\r\n"
            + "\r\n"
            + "\r\n");
        }
        private formatTerrainFragmentUpdateColor(terrainInfo:any, colorName:string, splatmapSampler:string, detailsSampler:string, normalsSampler:string, colorCorrection:number = 1.0):string {
            let result:string = "";
            if (terrainInfo != null && terrainInfo.textureAtlas != null && terrainInfo.splatmapAtlas != null && terrainInfo.splatmapCount > 0) {
                result = ("\r\n#define TERRAIN_FRAGMENT_UPDATE_COLOR\r\n\r\n"
                + "vec3 normalsColor = vec3(0.5, 0.5, 1.0);\r\n"
                + "vec3 normalsBuffer = normalW.rgb;\r\n"
                + "vec3 splatmapBuffer = " + colorName + ".rgb;\r\n"
                + "float autoMipMapLevel = -1.0;\r\n"
                + "float normalCorrection = 1.0;\r\n"
                + "float detailCorrection = " + colorCorrection.toFixed(4) + ";\r\n"
                + "\r\n"
                + "#if defined(ALBEDO) && defined(" + splatmapSampler.toUpperCase() + ") && defined(" + detailsSampler.toUpperCase() + ")\r\n"
                + "\r\n"
                + "// Reset Normal Values\r\n"
                + "#if defined(BUMP) || defined(PARALLAX) || defined(ANISOTROPIC)\r\n"
                + "    uvOffset = vec2(0.0, 0.0);\r\n"
                + "    #ifdef NORMAL\r\n"
                + "        normalW = normalize(vNormalW);\r\n"
                + "    #else\r\n"
                + "        normalW = normalize(cross(dFdx(vPositionW), dFdy(vPositionW))) * vEyePosition.w;\r\n"
                + "    #endif\r\n"
                + "    #ifdef CLEARCOAT\r\n"
                + "        clearCoatNormalW = normalW;\r\n"
                + "    #endif\r\n"
                + "    #if defined(BUMP) || defined(PARALLAX)\r\n"
                + "        #if defined(TANGENT) && defined(NORMAL)\r\n"
                + "            TBN = vTBN;\r\n"
                + "        #else\r\n"
                + "            TBN = cotangent_frame(normalW, vPositionW, vSplatmapUV);\r\n"
                + "        #endif\r\n"
                + "    #elif defined(ANISOTROPIC)\r\n"
                + "        #if defined(TANGENT) && defined(NORMAL)\r\n"
                + "            TBN = vTBN;\r\n"
                + "        #else\r\n"
                + "            TBN = cotangent_frame(normalW, vPositionW, vSplatmapUV, vec2(1.0, 1.0));\r\n"
                + "        #endif\r\n"
                + "    #endif\r\n"
                + "    #ifdef PARALLAX\r\n"
                + "        invTBN = transposeMat3(TBN);\r\n"
                + "    #endif\r\n"
                + "    normalW = perturbNormalSamplerColor(TBN, normalsColor, 1.0);\r\n"
                + "#endif\r\n"
                + "\r\n"
                + "// Global Atlas Values\r\n"
                + "float splatTileSize = " + terrainInfo.splatmapAtlas[2].toFixed(4) + ";\r\n"
                + "float splatTileBits = " + terrainInfo.splatmapAtlas[3].toFixed(4) + ";\r\n"
                + "float detailTileSize = " + terrainInfo.textureAtlas[2].toFixed(4) + ";\r\n"
                + "float detailTileBits = " + terrainInfo.textureAtlas[3].toFixed(4) + ";\r\n"
                + "\r\n"
                + "// Sample splatmap textures\r\n");
                /////////////////////////////////////////////////////////////////////////////////////////////////////////
                // Sample Each Splatmap Textures
                /////////////////////////////////////////////////////////////////////////////////////////////////////////
                let counter:number = 0;
                for (let index:number = 0; index < terrainInfo.splatmapCount; index++) {
                    counter = (index * 4);
                    const splatmapRect:number[] = terrainInfo["splatmapRect" + index];
                    result += "vec4 splatmapRect" + index + " = vec4(" + splatmapRect[0].toFixed(4) + ", " + splatmapRect[1].toFixed(4) + ", " + splatmapRect[2].toFixed(4) + ", " + splatmapRect[3].toFixed(4) + ");\r\n";
                    result += "vec4 splatmapAlbedo" + index + " = sampleSplatmapAtlas2D(" + splatmapSampler + ", vec2(splatTileSize, splatTileBits), splatmapRect" + index + ", (vSplatmapUV + uvOffset));\r\n";
                    result += "vec4 textureAlbedo" + (counter + 0) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "vec4 textureAlbedo" + (counter + 1) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "vec4 textureAlbedo" + (counter + 2) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "vec4 textureAlbedo" + (counter + 3) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    if (terrainInfo["textureRect" + (counter + 0)])
                    {
                        const textureRect:number[] = terrainInfo["textureRect" + (counter + 0)];
                        const textureInfo:number[] = terrainInfo["textureInfo" + (counter + 0)];
                        result += "vec4 textureRect" + (counter + 0) + " = vec4(" + textureRect[0].toFixed(4) + ", " + textureRect[1].toFixed(4) + ", " + textureRect[2].toFixed(4) + ", " + textureRect[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureScale" + (counter + 0) + " = vec2(" + textureInfo[0].toFixed(4) + ", " + textureInfo[1].toFixed(4) + ");\r\n";
                        result += "vec2 textureOffset" + (counter + 0) + " = vec2(" + textureInfo[2].toFixed(4) + ", " + textureInfo[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureTileUV" + (counter + 0) + " = ((vSplatmapUV + textureOffset" + (counter + 0) + ") * textureScale" + (counter + 0) + ");\r\n";
                        result += "textureAlbedo" + (counter + 0) + " = sampleTextureAtlas2D(" + detailsSampler + ", detailCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 0) + ", textureTileUV" + (counter + 0) + ", autoMipMapLevel);\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 1)])
                    {
                        const textureRect:number[] = terrainInfo["textureRect" + (counter + 1)];
                        const textureInfo:number[] = terrainInfo["textureInfo" + (counter + 1)];
                        result += "vec4 textureRect" + (counter + 1) + " = vec4(" + textureRect[0].toFixed(4) + ", " + textureRect[1].toFixed(4) + ", " + textureRect[2].toFixed(4) + ", " + textureRect[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureScale" + (counter + 1) + " = vec2(" + textureInfo[0].toFixed(4) + ", " + textureInfo[1].toFixed(4) + ");\r\n";
                        result += "vec2 textureOffset" + (counter + 1) + " = vec2(" + textureInfo[2].toFixed(4) + ", " + textureInfo[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureTileUV" + (counter + 1) + " = ((vSplatmapUV + textureOffset" + (counter + 1) + ") * textureScale" + (counter + 1) + ");\r\n";
                        result += "textureAlbedo" + (counter + 1) + " = sampleTextureAtlas2D(" + detailsSampler + ", detailCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 1) + ", textureTileUV" + (counter + 1) + ", autoMipMapLevel);\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 2)])
                    {
                        const textureRect:number[] = terrainInfo["textureRect" + (counter + 2)];
                        const textureInfo:number[] = terrainInfo["textureInfo" + (counter + 2)];
                        result += "vec4 textureRect" + (counter + 2) + " = vec4(" + textureRect[0].toFixed(4) + ", " + textureRect[1].toFixed(4) + ", " + textureRect[2].toFixed(4) + ", " + textureRect[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureScale" + (counter + 2) + " = vec2(" + textureInfo[0].toFixed(4) + ", " + textureInfo[1].toFixed(4) + ");\r\n";
                        result += "vec2 textureOffset" + (counter + 2) + " = vec2(" + textureInfo[2].toFixed(4) + ", " + textureInfo[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureTileUV" + (counter + 2) + " = ((vSplatmapUV + textureOffset" + (counter + 2) + ") * textureScale" + (counter + 2) + ");\r\n";
                        result += "textureAlbedo" + (counter + 2) + " = sampleTextureAtlas2D(" + detailsSampler + ", detailCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 2) + ", textureTileUV" + (counter + 2) + ", autoMipMapLevel);\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 3)])
                    {
                        const textureRect:number[] = terrainInfo["textureRect" + (counter + 3)];
                        const textureInfo:number[] = terrainInfo["textureInfo" + (counter + 3)];
                        result += "vec4 textureRect" + (counter + 3) + " = vec4(" + textureRect[0].toFixed(4) + ", " + textureRect[1].toFixed(4) + ", " + textureRect[2].toFixed(4) + ", " + textureRect[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureScale" + (counter + 3) + " = vec2(" + textureInfo[0].toFixed(4) + ", " + textureInfo[1].toFixed(4) + ");\r\n";
                        result += "vec2 textureOffset" + (counter + 3) + " = vec2(" + textureInfo[2].toFixed(4) + ", " + textureInfo[3].toFixed(4) + ");\r\n";
                        result += "vec2 textureTileUV" + (counter + 3) + " = ((vSplatmapUV + textureOffset" + (counter + 3) + ") * textureScale" + (counter + 3) + ");\r\n";
                        result += "textureAlbedo" + (counter + 3) + " = sampleTextureAtlas2D(" + detailsSampler + ", detailCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 3) + ", textureTileUV" + (counter + 3) + ", autoMipMapLevel);\r\n";
                    }
                    result += "splatmapBuffer = blendSplatmapAtlasColors(splatmapAlbedo" + index + ", textureAlbedo" + (counter + 0) + ", textureAlbedo" + (counter + 1) + ", textureAlbedo" + (counter + 2) + ", textureAlbedo" + (counter + 3) + ", splatmapBuffer);\r\n";
                    result += "#if defined(BUMP) || defined(PARALLAX) || defined(ANISOTROPIC)\r\n";
                    result += "    #if defined(" + normalsSampler.toUpperCase() + ")\r\n";
                    result += "        vec4 normalColor" + (counter + 0) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "        vec4 normalColor" + (counter + 1) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "        vec4 normalColor" + (counter + 2) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    result += "        vec4 normalColor" + (counter + 3) + " = vec4(0.0, 0.0, 0.0, 1.0);\r\n";
                    if (terrainInfo["textureRect" + (counter + 0)])
                    {
                        const normalScale:number = terrainInfo["normalsScale" + (counter + 0)];
                        result += "        float normalScale" + (counter + 0) + " = " + normalScale.toFixed(4) + ";\r\n";
                        result += "        normalColor" + (counter + 0) + " = sampleTextureAtlas2D(" + normalsSampler + ", normalCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 0) + ", textureTileUV" + (counter + 0) + ", autoMipMapLevel);\r\n";
                        result += "        normalColor" + (counter + 0) + ".rgb = perturbNormalSamplerColor(TBN, normalColor" + (counter + 0) + ".rgb, normalScale" + (counter + 0) + ");\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 1)])
                    {
                        const normalScale:number = terrainInfo["normalsScale" + (counter + 1)];
                        result += "        float normalScale" + (counter + 1) + " = " + normalScale.toFixed(4) + ";\r\n";
                        result += "        normalColor" + (counter + 1) + " = sampleTextureAtlas2D(" + normalsSampler + ", normalCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 1) + ", textureTileUV" + (counter + 1) + ", autoMipMapLevel);\r\n";
                        result += "        normalColor" + (counter + 1) + ".rgb = perturbNormalSamplerColor(TBN, normalColor" + (counter + 1) + ".rgb, normalScale" + (counter + 1) + ");\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 2)])
                    {
                        const normalScale:number = terrainInfo["normalsScale" + (counter + 2)];
                        result += "        float normalScale" + (counter + 2) + " = " + normalScale.toFixed(4) + ";\r\n";
                        result += "        normalColor" + (counter + 2) + " = sampleTextureAtlas2D(" + normalsSampler + ", normalCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 2) + ", textureTileUV" + (counter + 2) + ", autoMipMapLevel);\r\n";
                        result += "        normalColor" + (counter + 2) + ".rgb = perturbNormalSamplerColor(TBN, normalColor" + (counter + 2) + ".rgb, normalScale" + (counter + 2) + ");\r\n";
                    }
                    if (terrainInfo["textureRect" + (counter + 3)])
                    {
                        const normalScale:number = terrainInfo["normalsScale" + (counter + 3)];
                        result += "        float normalScale" + (counter + 3) + " = " + normalScale.toFixed(4) + ";\r\n";
                        result += "        normalColor" + (counter + 3) + " = sampleTextureAtlas2D(" + normalsSampler + ", normalCorrection, vec2(detailTileSize, detailTileBits), textureRect" + (counter + 3) + ", textureTileUV" + (counter + 3) + ", autoMipMapLevel);\r\n";
                        result += "        normalColor" + (counter + 3) + ".rgb = perturbNormalSamplerColor(TBN, normalColor" + (counter + 3) + ".rgb, normalScale" + (counter + 3) + ");\r\n";
                    }
                    result += "        normalsBuffer = blendSplatmapAtlasColors(splatmapAlbedo" + index + ", normalColor" + (counter + 0) + ", normalColor" + (counter + 1) + ", normalColor" + (counter + 2) + ", normalColor" + (counter + 3) + ", normalsBuffer);\r\n";
                    result += "    #endif\r\n";
                    result += "#endif\r\n";
                    result += "\r\n";
                }
                /////////////////////////////////////////////////////////////////////////////////////////////////////////
                result += ("// Update Color Values\r\n"
                + colorName + ".rgb = splatmapBuffer.rgb;\r\n"
                + "#if defined(BUMP) || defined(PARALLAX) || defined(ANISOTROPIC)\r\n"
                + "    #if defined(" + normalsSampler.toUpperCase() + ")\r\n"
                + "        normalW.rgb = normalsBuffer.rgb;\r\n"
                + "    #endif\r\n"
                + "    #if defined(FORCENORMALFORWARD) && defined(NORMAL)\r\n"
                + "        vec3 faceNormal = normalize(cross(dFdx(vPositionW), dFdy(vPositionW))) * vEyePosition.w;\r\n"
                + "        #if defined(TWOSIDEDLIGHTING)\r\n"
                + "            faceNormal = gl_FrontFacing ? faceNormal : -faceNormal;\r\n"
                + "        #endif\r\n"
                + "        normalW *= sign(dot(normalW, faceNormal));\r\n"
                + "    #endif\r\n"
                + "    #if defined(TWOSIDEDLIGHTING) && defined(NORMAL)\r\n"
                + "        normalW = gl_FrontFacing ? normalW : -normalW;\r\n"
                + "    #endif\r\n"
                + "#endif\r\n"
                + "\r\n"
                + "#endif\r\n"
                + "\r\n");
            }
            return result;
        }
    }

That has all my code i use for splatmaps, texture atlasing, blending or mixing colors and most important my sampleTextureAtlas2D functions with support for LODS and full MIPMAP support… so far away looks blured…

Take a look at how i pragmatically build up the terrain shader to support up to 64 textures without exceeding MAX TEXTURES

Anyways… the good stuff you want is in the formatTerrainFragmentUpdateColor function.

Hope that helps :slight_smile:

3 Likes

I’ve been giving this another try, I appreciate very much you sharing your code, it’s very enlightening.

I tried using your code directly, but then again, I had to translate it to ES6 and when I try to use it, the browser complains about the constructor not being able to call the constructor from the base class.

I guess what I’m seeking is a way of writing these sort of classes that extend Babylon and Babylon shaders. As my knowledge of browser packaging and ES6/TS interoperability is pretty reduced, I was trying to avoid using Typescript as this project is ES6, and my build pipeline would become more complex.

So, newbie question, can I do this without Typescript? what would it involve? I would much appreciate some explanation of the approach for extending Babylon classes from ES6, what the different PBR classes are intended for, or why this cannot be done with a CustomPBRMaterial (I guess it’s maybe because shader includes cannot be used in its context?)… As you can see, what I’m failing at is with the basics or the general setup for this. Thanks a lot.