Unsolved hlsl to static mesh help

Discussion in 'Mod Support' started by bendervdb06, Dec 1, 2025 at 11:50 AM.

  1. bendervdb06

    bendervdb06
    Expand Collapse

    Joined:
    May 29, 2014
    Messages:
    153
    this is a id colors ready for make real mask rgb value
    and the real mask rgb

    the blender plugin make materias.cs and hlsl files
    config how many canal r b & g have rule of split
    here 3 by canal
    r Mat 1 Mat 2 Mat 3
    g Mat 4 Mat 5 Mat 6
    b Mat 7 Mat 8 Mat 9
    upload_2025-12-1_12-0-20.png
    --- Post updated ---
    Hi everyone,

    I’m currently working on a workflow to generate large-scale terrains from OSM data (and other GIS sources) and bring them into BeamNG as static mesh terrain, with an ID-mask driven material system.

    My goal is to have a single static mesh terrain (DAE) exported from Blender/UPBGE, and drive all the material distribution using a RGB mask texture and a custom HLSL shader via ShaderData / CustomMaterial.

    Right now, the geometry loads fine in the World Editor, the material name is correctly mapped, but my HLSL logic is not actually being used – BeamNG seems to fall back to a regular material instead of my CustomMaterial + ShaderData.

    I’d really appreciate some guidance from the BeamNG team or anyone who has already done this with custom shaders.

    1. Context / Intent
    • I’m generating terrain from OSM / GIS data and baking:
      • a high-resolution static mesh (exported as .dae from Blender),

      • a RGB “ID mask” texture where each channel encodes multiple terrain classes (forest, arid, rock, water, road, etc.).
    • The idea is similar to an “ID Color map” used in Substance Painter:
      • the mask is not a classic splatmap, but a quantized ID map with several “slices” per channel.
    • On the BeamNG side, I want a custom shader that:
      • reads the RGB mask,

      • picks the correct texture per “slice” inside each channel (R/G/B),

      • blends the final color by channel weights.
    This would be very useful for OSM-based maps where the terrain material distribution can be autogenerated from real-world data (landuse, roads, rivers, etc.), instead of hand-painting everything.

    2. Current setup (files & structure)
    Right now I have something like this (example paths):


    levels/myMap/art/shapes/TerrainMesh/
    ├─ terrainMesh.dae
    ├─ terrainBlendV.hlsl
    ├─ terrainBlendP.hlsl
    ├─ terrain_maskid.dds
    ├─ grass_rocky_d.dds
    ├─ grass_rocky_n.dds
    ├─ desert_rocky_d.dds
    ├─ desert_rocky_n.dds
    ├─ mntn_white_d.dds
    ├─ mntn_x2_n.dds
    ├─ desert_mntn_d.dds
    ├─ desert_mntn_n.dds
    ├─ ground_mud_d.dds
    ├─ ground_mud_n.dds
    ├─ jungle_mntn_d.dds
    ├─ jungle_mntn_n.dds
    └─ materials.cs

    In Blender, the material on the mesh is just a normal Principled BSDF, and its name is:


    TerrainMesh

    The DAE therefore has a material called TerrainMesh, and that’s what I use in mapTo in the script.

    3. materials.cs (ShaderData + CustomMaterial)
    Here is the script I’m currently using:


    // materials.cs
    // Auto-generated by my Terrain Material Generator

    singleton ShaderData(TerrainMesh_Shader)
    {
    DXVertexShaderFile = "./terrainBlendV.hlsl";
    DXPixelShaderFile = "./terrainBlendP.hlsl";
    pixVersion = 3.0;
    };

    singleton CustomMaterial(TerrainMesh_Mat)
    {
    mapTo = "TerrainMesh";
    shader = TerrainMesh_Shader;

    sampler["maskTex"] = "terrain_maskid.dds";

    // R channel – example mapping
    sampler["R0_diffuseTex"] = "grass_rocky_d.dds";
    sampler["R0_normalTex"] = "grass_rocky_n.dds";

    sampler["R1_diffuseTex"] = "desert_rocky_d.dds";
    sampler["R1_normalTex"] = "desert_rocky_n.dds";

    // G channel
    sampler["G0_diffuseTex"] = "mntn_white_d.dds";
    sampler["G0_normalTex"] = "mntn_x2_n.dds";

    sampler["G1_diffuseTex"] = "desert_mntn_d.dds";
    sampler["G1_normalTex"] = "desert_mntn_n.dds";

    // B channel
    sampler["B0_diffuseTex"] = "ground_mud_d.dds";
    sampler["B0_normalTex"] = "ground_mud_n.dds";

    sampler["B1_diffuseTex"] = "jungle_mntn_d.dds";
    sampler["B1_normalTex"] = "jungle_mntn_n.dds";

    // NOTE: eventually there can be up to 3+ slices per channel (R2/G2/B2, etc.)

    version = 2.0;
    };

    From what I understand, this should:

    • load terrainBlendV.hlsl / terrainBlendP.hlsl,

    • bind all the samplers to the shader,

    • and apply the CustomMaterial to any mesh material named TerrainMesh.
    4. HLSL shaders (simplified)
    Vertex shader (terrainBlendV.hlsl)

    // terrainBlendV.hlsl
    // Vertex shader for terrain mask (Torque3D / BeamNG, SM3)

    float4x4 WorldViewProj;

    struct VS_INPUT
    {
    float4 position : POSITION0;
    float3 normal : NORMAL0;
    float2 uv : TEXCOORD0;
    };

    struct VS_OUTPUT
    {
    float4 position : POSITION0;
    float3 normal : TEXCOORD1;
    float2 uv : TEXCOORD0;
    };

    VS_OUTPUT main( VS_INPUT IN )
    {
    VS_OUTPUT OUT;

    OUT.position = mul(IN.position, WorldViewProj);
    OUT.normal = IN.normal;
    OUT.uv = IN.uv;

    return OUT;
    }

    Pixel shader (terrainBlendP.hlsl – mask logic)
    For now I’m focusing only on the diffuse logic.
    Conceptually:

    • I read the RGB mask (maskTex).

    • Each channel R/G/B is quantized into 3 “slices”.

    • Each slice selects one texture (R0/R1/R2, G0/G1/G2, B0/B1/B2).

    • The final color is a weighted mix of R/G/B contributions.
    Simplified Shader Model 3 version:


    // terrainBlendP.hlsl
    // Pixel shader : blend terrain using RGB mask + 3 slices per channel
    // Target: Shader Model 3.0 (Torque3D / BeamNG)

    // Mask
    sampler2D maskTex;

    // R channel (3 slices)
    sampler2D R0_diffuseTex;
    sampler2D R1_diffuseTex;
    sampler2D R2_diffuseTex;

    // G channel (3 slices)
    sampler2D G0_diffuseTex;
    sampler2D G1_diffuseTex;
    sampler2D G2_diffuseTex;

    // B channel (3 slices)
    sampler2D B0_diffuseTex;
    sampler2D B1_diffuseTex;
    sampler2D B2_diffuseTex;

    struct PS_INPUT
    {
    float4 position : POSITION0;
    float2 uv : TEXCOORD0;
    float3 normal : TEXCOORD1;
    };

    // Simple 3-slice quantization of a 0..1 value
    int getSliceIndex( float v )
    {
    if (v < 0.3333f)
    return 0;
    else if (v < 0.6667f)
    return 1;
    else
    return 2;
    }

    float4 main( PS_INPUT IN ) : COLOR0
    {
    float3 m = tex2D( maskTex, IN.uv ).rgb;

    int idxR = getSliceIndex( m.r );
    int idxG = getSliceIndex( m.g );
    int idxB = getSliceIndex( m.b );

    float4 colR, colG, colB;

    // R
    if (idxR == 0) colR = tex2D( R0_diffuseTex, IN.uv );
    else if (idxR == 1) colR = tex2D( R1_diffuseTex, IN.uv );
    else colR = tex2D( R2_diffuseTex, IN.uv );

    // G
    if (idxG == 0) colG = tex2D( G0_diffuseTex, IN.uv );
    else if (idxG == 1) colG = tex2D( G1_diffuseTex, IN.uv );
    else colG = tex2D( G2_diffuseTex, IN.uv );

    // B
    if (idxB == 0) colB = tex2D( B0_diffuseTex, IN.uv );
    else if (idxB == 1) colB = tex2D( B1_diffuseTex, IN.uv );
    else colB = tex2D( B2_diffuseTex, IN.uv );

    float wR = m.r;
    float wG = m.g;
    float wB = m.b;
    float total = max(wR + wG + wB, 0.0001f);

    float4 outColor = (colR * wR + colG * wG + colB * wB) / total;
    return outColor;
    }

    (I plan to later add proper normal mapping and lighting once the basic mask logic works.)

    5. What actually happens in BeamNG
    When I:

    1. Launch the map,

    2. Open the World Editor,

    3. Create a Mesh / Static Object and point it to terrainMesh.dae,
    … the mesh appears, but:

    • BeamNG is not using my HLSL logic (no ID mask behavior),

    • It looks like a regular material is used (standard texture behaviour),

    • In previous tests with a more “modern” HLSL syntax (Texture2D, SamplerState, .Sample() and SV_POSITION), nothing happened either → so I rewrote it for SM3, but the result stays the same.
    In other words: the engine loads the DAE and a material, but it behaves like a basic material, not like my CustomMaterial(TerrainMesh_Mat) using TerrainMesh_Shader.

    I suspect I’m either:

    • placing the materials.cs in the wrong place,

    • missing some required syntax / naming convention for CustomMaterial,

    • or missing some BeamNG-specific step for custom HLSL shaders on static meshes.
    6. What I’d like to ask the devs / community
    1. Is this CustomMaterial + ShaderData approach still supported for map objects / static mesh terrain in the current BeamNG versions?

    2. Is there a recommended path / location for materials.cs when using a custom shader on a static mesh (.dae) in a level?
      • For example: is levels/myMap/art/shapes/TerrainMesh/materials.cs the right place?
    3. Are there any special requirements for CustomMaterial to take precedence over a default Material or auto-generated material for mapTo = "TerrainMesh"?
      • Do I need to avoid having any other Material with the same mapTo?
    4. For Shader Model 3.0 (pixVersion = 3.0), is the sampler2D / tex2D approach and the semantics (POSITION0, COLOR0, etc.) correct for BeamNG?

    5. Is there maybe a better / more modern way (using the built-in terrain system, or a more recent material system) to implement an ID-mask-driven terrain shader for static mesh terrain?
    My main objective is to have a robust, automatable mask-driven terrain material system for huge OSM-based maps, so any advice, best practices, or example of a working CustomMaterial + HLSL setup for BeamNG would be incredibly helpful.

    Thanks a lot for reading, and for any pointers you can share!
    If needed, I can provide a minimal test mod (DAE + textures + materials.cs + HLSL) to reproduce the issue.

    Best regards,
     

    Attached Files:

    • id_colors_terrain.png
    • terrain_maskid.png
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice