Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to mix textures and shadow maps on the same geometry? #1135

Closed
LiquidSnakeX opened this issue Jan 19, 2012 · 12 comments
Closed

How to mix textures and shadow maps on the same geometry? #1135

LiquidSnakeX opened this issue Jan 19, 2012 · 12 comments
Labels

Comments

@LiquidSnakeX
Copy link

Evening Mentlegen,

I'm trying to build terrain and texture it with several colormaps and also the standard Three.js shadowmapping. A plane is deformed in the JS based on a heightmap, then I have a few textures applied in a fragment shader, based on the height of the terrain, and need to keep that custom functionality. So far I have the terrain deforming correctly and can get either the colormaps, or the shadowmapping, but not both.

Would I have to write a new shader that combines the shadowmapping and custom texturing, or is there another way?
I'm really just looking for the cleanest way to go about this, using as much of Three's built-in functionality as possible.
Can anyone... shine some light on this?

@alteredq
Copy link
Contributor

You just need to add few snippets to your custom ShaderMaterial at proper places:

// uniforms

THREE.UniformsLib[ "shadowmap" ],

// fragment shader

THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
THREE.ShaderChunk[ "shadowmap_fragment" ],

// vertex shader

THREE.ShaderChunk[ "shadowmap_pars_vertex" ],
THREE.ShaderChunk[ "shadowmap_vertex" ],

Have a look at how normal map shader integrates shadows:

https://github.com/mrdoob/three.js/blob/master/src/extras/ShaderUtils.js#L94

Or maybe even simpler check how standard Lambert material is done:

https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLShaders.js#L1235

@LiquidSnakeX
Copy link
Author

That does look simple enough but I have no idea where to place the snippets, do they go in:
my JS code?
my shader code?
an edited copy of the three.js shaders?
passed as parameters to an instance of THREE.MeshShaderMaterial?

Sorry for the n00bness, but I'm not familiar with the context that your linked code is being used in,
it just seems really abstract, could you give a generic example of how to use those snippets?
Thanks for your patience

@alteredq
Copy link
Contributor

THREE.ShaderChunk snippets are just strings, so you use them when constructing shader source strings (these strings are then parameters of ShaderMaterial).

This wouldn't work if for example you use DOM elements to have whole shader string in one piece, you need to be able to "inject" snippets at the right places. A pattern we use to construct long strings for shader sources is to concatenate array of single line strings with join, snippets are then placed in such array.

THREE.UniformsLib snippets are JS objects, which you merge with your own uniforms objects using THREE.UniformsUtils.merge.

@LiquidSnakeX
Copy link
Author

Alright that makes sense, I think I know how to do it now, thanks man

Quick question, how do I label posts as "question"?
It feels kinda wrong having something down as a general bug/issue when it's really just my own n00bishness :P

@LiquidSnakeX
Copy link
Author

I maybe closed this issue a little prematurely, even though I'm pretty sure I understand how this should work in
theory, I just can't get my head around implementing it.

Now I'm using that "concatenated snippets array" technique to create the shader, usingTHREE.UniformsUtils.merge anywhere seems to stop my textures from loading and replaces them with a black color, without even adding any new code to the actual vertex or fragment shaders. Like this:

example

This guy seems to be having a similar issue.

Here's how I'm trying it, can anyone tell me what I'm doing wrong,
or even better give me an example of it being done right?

var terrain = new THREE.Mesh(
        new THREE.PlaneGeometry(1000, 1000, heightMapSize-1, heightMapSize-1),
        new THREE.ShaderMaterial({
            uniforms: terrainShader.uniforms,
            vertexShader: terrainShader.vertexShader,
            fragmentShader: terrainShader.fragmentShader
            })
    );
var terrainShader = {

    uniforms: THREE.UniformsUtils.merge([
        THREE.UniformsLib[ "shadowmap" ],
        {
            texture_dirt: { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture('images/dirt.jpg') },
            texture_grass: { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture('images/grass.jpg') },
            texture_rock: { type: "t", value: 2, texture: THREE.ImageUtils.loadTexture('images/rock.jpg') }
        }
    ]),



    vertexShader: [
        "varying vec2 vUv;",
        "varying vec3 vPosition;",
        "varying float heightFactor;",

        THREE.ShaderChunk[ "shadowmap_pars_vertex" ],

        "void main(void) {",

            "vUv = uv;",
            "vPosition = position;",

            THREE.ShaderChunk[ "shadowmap_vertex" ],

            "gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1);",

        "}"

    ].join("\n"),



    fragmentShader: [
        "uniform sampler2D texture_dirt;",
        "uniform sampler2D texture_grass;",
        "uniform sampler2D texture_rock;",

        "varying vec2 vUv;",
        "varying vec3 vPosition;",

        THREE.ShaderChunk[ "shadowmap_pars_fragment" ],

        "void main() {",

            "// Texture loading",
            "vec4 diffuseDirt = texture2D( texture_dirt, vUv );",
            "vec4 diffuseGrass = texture2D( texture_grass, vUv );",
            "vec4 diffuseRock = texture2D( texture_rock, vUv );",
            "vec4 diffuseSand = vec4(.8, .8, .7, 1.0);",
            "vec4 diffuseSnow = vec4(.8, .9, 1.0, 1.0);",

            "vec4 color = diffuseDirt; //default texture",
            "float heightFactor = 8.0;",

            "//add sand",
            "color = mix(diffuseSand, color, min(abs(5.0 - vPosition.z) / 5.0/heightFactor, 1.0));",
            "//starts at 5.0 for 5.0 units in both directions",

            "//add dirt",
            "color = mix(diffuseDirt, color, min(abs(10.0*heightFactor - vPosition.z) / 5.0/heightFactor, 1.0));",

            "//add grass",
            "color = mix(diffuseGrass, color, min(abs(10.0*heightFactor - vPosition.z) / 5.0/heightFactor, 1.0));",

            "//add rock",
            "color = mix(diffuseRock, color, min(abs(25.0*heightFactor - vPosition.z) / 12.0/heightFactor, 1.0));",

            "//add snow",
            "color = mix(diffuseSnow, color, min(abs(30.0*heightFactor - vPosition.z) / 10.0/heightFactor, 1.0));",

            THREE.ShaderChunk[ "shadowmap_fragment" ],

            "gl_FragColor = color;",

        "}"

    ].join("\n")
};

(the object terrainShader is defined before the terrain constructor in my code)

@alteredq
Copy link
Contributor

Fragment shader should be like this:

            // ...

            "//add rock",
            "color = mix(diffuseRock, color, min(abs(25.0*heightFactor - vPosition.z) / 12.0/heightFactor, 1.0));",

            "//add snow",
            "color = mix(diffuseSnow, color, min(abs(30.0*heightFactor - vPosition.z) / 10.0/heightFactor, 1.0));",

            "gl_FragColor = color;",

            THREE.ShaderChunk[ "shadowmap_fragment" ],

@LiquidSnakeX
Copy link
Author

I actually figured that out since I last posted, and got the shadow-mapping working, but the issue of the black terrain is still there whenever I use THREE.UniformsUtils.merge(). It stops my textures from loading, or maybe just the uniforms representing them, hence the black color where they should be.

I've worked around the problem in the meantime by manually adding the shadowMap uniforms after my own uniforms like shown below, but that stops me from using THREE.UniformsUtils.merge() which could be very handy for creating shaders, if it didn't obliterate my own texture uniforms that is! Any idea why it's doing this?

uniforms: {
    texture_dirt: { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture('images/dirt.jpg') },
    texture_grass: { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture('images/grass.jpg') },
    texture_rock: { type: "t", value: 2, texture: THREE.ImageUtils.loadTexture('images/rock.jpg') },

    //shadowmap
    shadowMap: { type: "tv", value: 6, texture: [] },
    shadowMapSize: { type: "v2v", value: [] },
    shadowBias: { type: "fv1", value: [] },
    shadowDarkness: { type: "fv1", value: [] },
    shadowMatrix: { type: "m4v", value: [] }
}

@alteredq
Copy link
Contributor

I don't know, looks ok. The only difference I see is that you use uniforms directly, instead of cloning them. This would make troubles if you used the same shader multiple times. Each instance of ShaderMaterial needs own instance of uniforms.

@plopidou
Copy link

Snake,

I also managed to get it working. Not in the "clean way", though.

(warning, rather big chunk of code incoming...)

I will post the code I am currently using here - maybe this will allow us to .. errm.. share ideas & comments.. :-)

Also, a screenshot of the result (no vegetation this time as I chose to focus on the terrain for now): http://imageupload.org/?d=B9967F4F1

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="screen.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>

<div id="c"></div>

<ul id="light">
    <li><span>X</span> <input id="lx" type="range" value="1.0" step="0.05" min="-10" max="10"><span class="val"></span></li>
    <li><span>Y</span> <input id="ly" type="range" value="1.0" step="0.05" min="-10" max="10"><span class="val"></span></li>
    <li><span>Z</span> <input id="lz" type="range" value="-1.0" step="0.05" min="-10" max="10"><span class="val"></span></li>
    <li><span>I</span> <input id="li" type="range" value="1.0" step="0.02" min="0.0" max="2.0"><span class="val"></span></li>
</ul>



<script type="text/javascript" src="http://github.com/mrdoob/three.js/raw/dev/build/Three.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>

<script type="text/javascript">

THREE.ShaderLib['terrain'] = {

uniforms: THREE.UniformsUtils.merge([


    THREE.UniformsLib[ "fog" ],
    THREE.UniformsLib[ "lights" ],
    THREE.UniformsLib[ "shadowmap" ],
    {
        "enableAO"                                  : { type: "i", value: 0 },
        "enableDiffuse"                     : { type: "i", value: 0 },
        "enableSpecular"                        : { type: "i", value: 0 },
        "enableReflection"                  : { type: "i", value: 0 },
        "tDiffuseGrass"                     : { type: "t", value: 0, texture: null },
        "tCube"                                     : { type: "t", value: 1, texture: null },
        "tNormal"                                   : { type: "t", value: 2, texture: null },
        "tSpecular"                               : { type: "t", value: 3, texture: null },
        "tAO"                                         : { type: "t", value: 4, texture: null },
        "tDisplacement"                         : { type: "t", value: 5, texture: null },
        "tDiffuseGround"                    : { type: "t", value: 6, texture: null },
        "tDiffuseCliff"                     : { type: "t", value: 7, texture: null },   
        "tHeightMap"                            : { type: "t", value: 8, texture: null },   
        "tSlopeMap"                             : { type: "t", value: 9, texture: null },   
        "tDiffuseGravel"                        : { type: "t", value: 10, texture: null },  
        "uNormalScale"                          : { type: "f", value: 1.0 },
        "uDisplacementBias"                 : { type: "f", value: 0.0 },
        "uDisplacementScale"                : { type: "f", value: 1.0 },
        "uDiffuseColor"                         : { type: "c", value: new THREE.Color( 0xeeeeee ) },
        "uSpecularColor"                        : { type: "c", value: new THREE.Color( 0x111111 ) },
        "uAmbientColor"                         : { type: "c", value: new THREE.Color( 0x050505 ) },
        "uShininess"                                : { type: "f", value: 30 },
        "uOpacity"                                  : { type: "f", value: 1 },
        "uReflectivity"                         : { type: "f", value: 0.5 },
        "uOffset"                                   : { type: "v2", value: new THREE.Vector2( 0, 0 ) },
        "uRepeat"                                       : { type: "v2", value: new THREE.Vector2( 1, 1 ) }
    }   

]),

fragmentShader: [

    "uniform vec3 uAmbientColor;",
    "uniform vec3 uDiffuseColor;",
    "uniform vec3 uSpecularColor;",
    "uniform float uShininess;",
    "uniform float uOpacity;",
    "uniform bool enableDiffuse;",
    "uniform bool enableSpecular;",
    "uniform bool enableAO;",
    "uniform bool enableReflection;",

    "uniform sampler2D tDiffuseGrass;",
    "uniform sampler2D tDiffuseGround;",
    "uniform sampler2D tDiffuseCliff;",
    "uniform sampler2D tDiffuseGravel;",

    "uniform sampler2D tNormal;",
    "uniform sampler2D tHeightMap;",
    "uniform sampler2D tSlopeMap;",

    "uniform sampler2D tSpecular;",
    "uniform sampler2D tAO;",

    "uniform samplerCube tCube;",
    "uniform float uNormalScale;",
    "uniform float uReflectivity;",

    "varying vec3 vTangent;",
    "varying vec3 vBinormal;",
    "varying vec3 vNormal;",
    "varying vec2 vUv;",
    "varying vec3 vPosition;",

    "uniform vec3 ambientLightColor;",
    "#if MAX_DIR_LIGHTS > 0",
    "uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];",
    "uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];",
    "#endif",
    "#if MAX_POINT_LIGHTS > 0",
    "uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];",
    "varying vec4 vPointLight[ MAX_POINT_LIGHTS ];",
    "#endif",
    "varying vec3 vViewPosition;",
    THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
    THREE.ShaderChunk[ "fog_pars_fragment" ],
    "void main() {",
    "gl_FragColor = vec4( vec3( 1.0 ), uOpacity );",
    "vec3 specularTex = vec3( 1.0 );",
    "vec3 normalTex = texture2D( tNormal, vUv ).xyz * 2.0 - 1.0;",
    "normalTex.xy *= uNormalScale;",
    "normalTex = normalize( normalTex );",
    "if( enableDiffuse )",

        //"gl_FragColor = gl_FragColor * vec4 (texture2D( tDiffuseGrass, (vUv * 8.0) ).rgb * 0.8, 1.0 );",
        "gl_FragColor = gl_FragColor;",

        "vec4 colorGround = texture2D( tDiffuseGround, (vUv * 8.0) );",
        "vec4 colorGrass = texture2D( tDiffuseGrass, (vUv * 18.0) );",
        "vec4 colorCliff = texture2D( tDiffuseCliff, (vUv * 10.0) );",
        "vec4 colorGravel = texture2D( tDiffuseGravel, (vUv * 12.0) );",

        "vec4 color = colorGrass * 2.4;",
        "color = mix( colorGround, color, min(abs(40.0 - vPosition.z) / 250.0, 1.0) );",

        "vec4 slope = texture2D( tSlopeMap, vUv );",
        "color = mix( color, colorCliff, slope.r * 1.8 );",
        "color = mix( color, colorGravel, slope.g * 0.9 );",

        "gl_FragColor = gl_FragColor * color;",


    "if( enableAO )",
        "gl_FragColor.xyz = gl_FragColor.xyz * texture2D( tAO, vUv ).xyz;", "if( enableSpecular )",
        "specularTex = texture2D( tSpecular, vUv ).xyz;",   "mat3 tsb = mat3( vTangent, vBinormal, vNormal );",
    "vec3 finalNormal = tsb * normalTex;",
    "vec3 normal = normalize( finalNormal );",
    "vec3 viewPosition = normalize( vViewPosition );",
    "#if MAX_POINT_LIGHTS > 0",
    "vec3 pointDiffuse = vec3( 0.0 );",
        "vec3 pointSpecular = vec3( 0.0 );",
        "for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {",
        "vec3 pointVector = normalize( vPointLight[ i ].xyz );",
        "vec3 pointHalfVector = normalize( vPointLight[ i ].xyz + viewPosition );",
        "float pointDistance = vPointLight[ i ].w;",
        "float pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );",
        "float pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );",
        "float pointSpecularWeight = specularTex.r * pow( pointDotNormalHalf, uShininess );",
        "pointDiffuse += pointDistance * pointLightColor[ i ] * uDiffuseColor * pointDiffuseWeight;",
        "pointSpecular += pointDistance * pointLightColor[ i ] * uSpecularColor * pointSpecularWeight * pointDiffuseWeight;",
    "}",
    "#endif",
    "#if MAX_DIR_LIGHTS > 0",
    "vec3 dirDiffuse = vec3( 0.0 );",
        "vec3 dirSpecular = vec3( 0.0 );","for( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {",
        "vec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );",
        "vec3 dirVector = normalize( lDirection.xyz );",
    "vec3 dirHalfVector = normalize( lDirection.xyz + viewPosition );",
    "float dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );",
    "float dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );",
    "float dirSpecularWeight = specularTex.r * pow( dirDotNormalHalf, uShininess );",
    "dirDiffuse += directionalLightColor[ i ] * uDiffuseColor * dirDiffuseWeight;",
    "dirSpecular += directionalLightColor[ i ] * uSpecularColor * dirSpecularWeight * dirDiffuseWeight;","}",
    "#endif",
    "vec3 totalDiffuse = vec3( 0.0 );",
    "vec3 totalSpecular = vec3( 0.0 );",
    "#if MAX_DIR_LIGHTS > 0","totalDiffuse += dirDiffuse;",
        "totalSpecular += dirSpecular;",
        "#endif",   "#if MAX_POINT_LIGHTS > 0","totalDiffuse += pointDiffuse;",
        "totalSpecular += pointSpecular;",
        "#endif",   "gl_FragColor.xyz = gl_FragColor.xyz * ( totalDiffuse + ambientLightColor * uAmbientColor) + totalSpecular;",
        "if ( enableReflection ) {","vec3 wPos = cameraPosition - vViewPosition;",
        "vec3 vReflect = reflect( normalize( wPos ), normal );",
        "vec4 cubeColor = textureCube( tCube, vec3( -vReflect.x, vReflect.yz ) );",
        "gl_FragColor.xyz = mix( gl_FragColor.xyz, cubeColor.xyz, uReflectivity );",
        "}",
        THREE.ShaderChunk[ "shadowmap_fragment" ],
        THREE.ShaderChunk[ "fog_fragment" ],
    "}"
].join("\n"),

vertexShader: [

    "attribute vec4 tangent;",
    "uniform vec2 uOffset;",
    "uniform vec2 uRepeat;",
    "#ifdef VERTEX_TEXTURES",
    "uniform sampler2D tDisplacement;",
    "uniform float uDisplacementScale;",
    "uniform float uDisplacementBias;",
    "#endif",
    "varying vec3 vTangent;",
    "varying vec3 vBinormal;",
    "varying vec3 vNormal;",
    "varying vec2 vUv;",
    "varying vec3 vPosition;",

    "#if MAX_POINT_LIGHTS > 0",
    "uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];",
    "uniform float pointLightDistance[ MAX_POINT_LIGHTS ];",
    "varying vec4 vPointLight[ MAX_POINT_LIGHTS ];",
    "#endif",
    "varying vec3 vViewPosition;",

    THREE.ShaderChunk[ "shadowmap_pars_vertex" ],

    "void main() {",
    "vPosition = position;",
    "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
    "vViewPosition = -mvPosition.xyz;",
    "vNormal = normalize( normalMatrix * normal );",
    "vTangent = normalize( normalMatrix * tangent.xyz );",  
    "vBinormal = cross( vNormal, vTangent ) * tangent.w;",
    "vBinormal = normalize( vBinormal );",  
    "vUv = uv * uRepeat + uOffset;",    
    "#if MAX_POINT_LIGHTS > 0",
    "for( int i = 0; i < MAX_POINT_LIGHTS; i++ ) {",    
    "vec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );",  
    "vec3 lVector = lPosition.xyz - mvPosition.xyz;",   
    "float lDistance = 1.0;",   
    "if ( pointLightDistance[ i ] > 0.0 )",
        "lDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );",   
        "lVector = normalize( lVector );",  
        "vPointLight[ i ] = vec4( lVector, lDistance );",
        "}",    
    "#endif",   
    "#ifdef VERTEX_TEXTURES",
    "vec3 dv = texture2D( tDisplacement, uv ).xyz;",
        "float df = uDisplacementScale * dv.x + uDisplacementBias;",
        "vec4 displacedPosition = vec4( vNormal.xyz * df, 0.0 ) + mvPosition;",
        "gl_Position = projectionMatrix * displacedPosition;",  
    "#else",
        "gl_Position = projectionMatrix * mvPosition;", 
    "#endif",
    THREE.ShaderChunk[ "shadowmap_vertex" ],
    "}" 
].join("\n")

};

</script>

<script type="text/javascript">
// ==================
// Light manipulation
var update_light = function(x,y,z,i){
    dirLight.position.set(x,y,z).normalize();
    dirLight.intensity = i;
}

$('#light').delegate(
    'input',
    'change',
    function(){
        $(this).next('span.val').text(
            parseFloat($(this).val(),10).toFixed(2)
        );
        update_light(
            parseFloat( $('#lx').val(), 10 ),
            parseFloat( $('#ly').val(), 10 ),
            parseFloat( $('#lz').val(), 10 ),
            parseFloat( $('#li').val(), 10 )
        );
    }
);
// ==================


var scene = new THREE.Scene();
scene.fog = new THREE.Fog(
    0x828be7,
    600,
    6000
);
//THREE.ColorUtils.adjustHSV( scene.fog.color, 0.02, -0.15, -0.65 );

var axes = new THREE.AxisHelper();
axes.position.y = 450;
scene.add(axes);

var camera = new THREE.PerspectiveCamera(
    44,
    $(document).width() / $(document).height(),
    1,
    10000
);
camera.position.set(
    0,
    400,
    0
);
scene.add(camera);

controls = new THREE.FirstPersonControls(camera);
controls.movementSpeed = 500;
controls.lookSpeed = 0.08;


var clock = new THREE.Clock();

var ambient = new THREE.AmbientLight(0xaaaaaa);
scene.add(ambient);

var dirLight = new THREE.DirectionalLight(
    0xffffff,
    1.0
);
update_light(
    parseFloat( $('#lx').val(), 10 ),
    parseFloat( $('#ly').val(), 10 ),
    parseFloat( $('#lz').val(), 10 ),
    parseFloat( $('#li').val(), 10 )
);

dirLight.shadowCameraVisible = true;
dirLight.castShadow = true;
scene.add(dirLight);

$('#lx').next('span.val').text(
    parseFloat($('#lx').val(),10).toFixed(2)
);
$('#ly').next('span.val').text(
    parseFloat($('#ly').val(),10).toFixed(2)
);
$('#lz').next('span.val').text(
    parseFloat($('#lz').val(),10).toFixed(2)
);

renderer = new THREE.WebGLRenderer({
    clearColor:0x828be7,
    clearAlpha:1,
    antialias:true,
    alpha:true
});

var _shadowMapWidth = 1024;
var _shadowMapHeight = 1024;

renderer.shadowCameraNear = 3;
renderer.shadowCameraFar = camera.far;
renderer.shadowCameraFov = 50;

renderer.shadowMapBias = 0.0039;
//renderer.shadowMapDarkness = 0.5;
renderer.shadowMapDarkness = 1.0;
renderer.shadowMapWidth = _shadowMapWidth;
renderer.shadowMapHeight = _shadowMapHeight;

renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;

var sceneHUD,
        cameraOrtho,
        hudMaterial;

function createHUD() {

    cameraOrtho = new THREE.OrthographicCamera(
        - 10000,
        10000,
        10000,
        - 10000,
        -10,
        10000
    );

    cameraOrtho.position.y = 0;
    cameraOrtho.position.z = 0;

    var shader = {

        uniforms: {
            tDiffuse: { type: "t", value: 0, texture: null },
            opacity:  { type: "f", value: 1.0 }
        },

        vertexShader: [
            "varying vec2 vUv;",
            "void main() {",
                "vUv = vec2( uv.x, 1.0 - uv.y );",
                "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
            "}"
        ].join("\n"),

        fragmentShader: [
            "uniform float opacity;",
            "uniform sampler2D tDiffuse;",
            "varying vec2 vUv;",
            "void main() {",
                "vec4 texel = texture2D( tDiffuse, vUv );",
                "gl_FragColor = opacity * texel;",
            "}"
        ].join("\n")

    };
    var uniforms = new THREE.UniformsUtils.clone( shader.uniforms );

    hudMaterial = new THREE.ShaderMaterial({
        vertexShader: shader.vertexShader,
        fragmentShader: shader.fragmentShader,
        uniforms: uniforms
    });

    var hudGeo = new THREE.PlaneGeometry(
        _shadowMapWidth / 2,
        _shadowMapHeight / 2
    );

    var hudMesh = new THREE.Mesh(
        hudGeo,
        hudMaterial
    );
    hudMesh.position.x = ( $(document).width() - _shadowMapWidth / 2 ) * -0.5;
    hudMesh.position.y = ( $(document).height() - _shadowMapHeight / 2 ) * -0.5;

    sceneHUD = new THREE.Scene();
    sceneHUD.add( hudMesh );

    cameraOrtho.lookAt( sceneHUD.position );

}
createHUD();

var terrain_def = {
    'depth':65,
    'width':65
};
var terrain_ratio = 64;
var terrain_geometry = new THREE.PlaneGeometry(
    terrain_def.width * terrain_ratio,
    terrain_def.depth * terrain_ratio,
    terrain_def.width - 1,
    terrain_def.depth - 1
);

var heightmap_img = new Image();
heightmap_img.onload = function(){
    generate_terrain();
};
heightmap_img.src = 'heightmap.png';

var generate_terrain = function(){

    var c_height = document.createElement('canvas');
        c_height.height = terrain_def.depth;
        c_height.width = terrain_def.width;
    height_ctx = c_height.getContext('2d');
    height_ctx.drawImage(heightmap_img, 0, 0);

    var height_img_data = height_ctx.getImageData(0, 0, terrain_def.width, terrain_def.depth).data;
    var z, index = 0;

    for( var i = 0, l = height_img_data.length; i<l; i+=4){
            z = height_img_data[i]/5 + height_img_data[i+1];
            terrain_geometry.vertices[index].position.z = z * 2.0;
            index = index + 1;
    }

    terrain_geometry.computeVertexNormals();
    terrain_geometry.computeFaceNormals();
    terrain_geometry.computeTangents();

    terrain_geometry.__dirtyVertices = true;
    terrain_geometry.__dirtyNormals = true;

    var ambient = 0xcccccc,
            diffuse = 0xcccccc,
            specular = 0x080810,
            shininess = 0.5;

    var shader = THREE.ShaderLib['terrain'];
    var uniforms = THREE.UniformsUtils.clone( shader.uniforms );

    uniforms[ "tNormal" ].texture = THREE.ImageUtils.loadTexture( "normalmap.png" );
    uniforms[ "uNormalScale" ].value = - 0.75;
    uniforms[ "tHeightMap" ].texture = THREE.ImageUtils.loadTexture( "heightmap.png" );
    uniforms[ "tSlopeMap" ].texture = THREE.ImageUtils.loadTexture( "slopemap.png" );
    uniforms[ "tDiffuseGrass" ].texture = THREE.ImageUtils.loadTexture( "textures/grass3.jpg" );
    uniforms[ "tDiffuseGrass" ].texture.wrapS = THREE.RepeatWrapping;
    uniforms[ "tDiffuseGrass" ].texture.wrapT = THREE.RepeatWrapping;
    uniforms[ "tDiffuseCliff" ].texture = THREE.ImageUtils.loadTexture( "textures/cliff2.jpg" );
    uniforms[ "tDiffuseCliff" ].texture.wrapS = THREE.RepeatWrapping;
    uniforms[ "tDiffuseCliff" ].texture.wrapT = THREE.RepeatWrapping;
    uniforms[ "tDiffuseGround" ].texture = THREE.ImageUtils.loadTexture( "textures/ground.jpg" );
    uniforms[ "tDiffuseGround" ].texture.wrapS = THREE.RepeatWrapping;
    uniforms[ "tDiffuseGround" ].texture.wrapT = THREE.RepeatWrapping;
    uniforms[ "tDiffuseGravel" ].texture = THREE.ImageUtils.loadTexture( "textures/gravel.jpg" );
    uniforms[ "tDiffuseGravel" ].texture.wrapS = THREE.RepeatWrapping;
    uniforms[ "tDiffuseGravel" ].texture.wrapT = THREE.RepeatWrapping;

    uniforms[ "enableAO" ].value = false;
    uniforms[ "enableDiffuse" ].value = true;
    uniforms[ "enableSpecular" ].value = false;


    uniforms[ "uDiffuseColor" ].value.setHex( diffuse );
    uniforms[ "uSpecularColor" ].value.setHex( specular );
    uniforms[ "uAmbientColor" ].value.setHex( ambient );

    uniforms[ "uShininess" ].value = shininess;

    var parameters = {
        fragmentShader: shader.fragmentShader,
        vertexShader: shader.vertexShader,
        uniforms: uniforms,
        lights: true,
        fog: true
    };
    var material = new THREE.ShaderMaterial( parameters );



    var terrain_mesh = new THREE.Mesh(
        terrain_geometry,
        material
    );
    terrain_mesh.rotation.x = -90 * Math.PI / 180;
    terrain_mesh.position.y = -500;

    terrain_mesh.castShadow = true;
    terrain_mesh.receiveShadow = true;

    scene.add( terrain_mesh );


};
renderer.setSize(
    $(document).width(),
    $(document).height()
);
$('#c').html(renderer.domElement);
renderer.render(
    scene,
    camera
);

function render(){
    controls.update(clock.getDelta());
    renderer.render(scene,camera);
    //DEBUG shadowmap
    //hudMaterial.uniforms.tDiffuse.texture = renderer.shadowMap[ 0 ];
    //renderer.render(sceneHUD,cameraOrtho);
};
function animate(){
    window.requestAnimationFrame(animate);
    render();
};

animate();

</script>

</body>
</html>

What I have done is to copy/paste the normal fragment chunk from the lib, and edit it here and there... so far it is working, but I do realise it is a little shaky.

Textures are fine, normals are fine (apart from Crazybump apparently generating funny "edges" on the normals texture). But I cannot get the shadow maps to work. More precisely, I cannot get the terrain cast shadows unto itself. :S

Could it be:

. That I am using a directional light?
. That I could very well be using a directional light, but am not using it properly?

Cheers + plop!

@LiquidSnakeX
Copy link
Author

I still have the problem of THREE.UniformUtils.merge() causing havoc and failing silently and Plopidou told me he's also having similar troubles with it, so I'll look into it and open a new issue if needed.

Here's a workaround that gets the desired effect by adding the uniforms directly, without THREE.UniformUtils.merge()

The following code takes a flat plane, deforms it based on a 128x128px heightmap image, uses the heightmap to bake a lightmap for soft shadowing (thanks to this example), applies textures based on altitude and combines all that with the standard fog and shadowmapping to give this result: terrain.jpg
The code is split among several files to separate the boilerplate stuff from the juiciness. Hope this helps somebody:

<!DOCTYPE html>
<html>
    <head>
        <title>Terrain</title>
        <meta http-equiv="content-type" content="text/html;charset=utf-8" />
        <style type="text/css">
        body{color: grey; font-family: "Lucida Sans Unicode", Lucida Grande, sans-serif; margin: 0px 0px; padding: 0px 0px; }
        #header{width: 800px; margin: 5px auto;}
        #viewport{border: 2px solid grey; border-radius: 2px; background-color: white; width: 800px; height: 600px; margin: 0 auto; padding: 0;}
        .blue{color: cornflowerblue; padding-left: 2em; padding-right: 2em; opacity: 0.5;}
        </style>

        <!--built on three.js r47, may not work with newer builds-->
        <script src="https://raw.github.com/mrdoob/three.js/master/build/Three.js"></script>
        <script src="createShaders.js" type="text/javascript"></script>
        <script src="getHeightData.js" type="text/javascript"></script>
        <script src="lightmap.js" type="text/javascript"></script>
        <script src="program.js" type="text/javascript"></script>
    </head>

    <body onload="initialize();">
        <div id="header">
            <h3>Blended and Shadowed Terrain from Heightmap</h3>
            <p>
            Move: w a s d r f<span class="blue">|</span>
            Look: arrow keys / drag mouse<span class="blue">|</span>
            Roll: q e
            </p>
        </div>
        <div id="viewport"></div>
    </body>
</html>

program.js:

//----Variables----//
    //DOM element to attach the renderer to
    var viewport;
    var viewportWidth = 800;
    var viewportHeight = 600;

    //placeholder for built-in three.js controls
    var controls;

    //camera attributes
    var view_angle = 45, //45 is standard, 90 is very wide
        aspect = viewportWidth / viewportHeight,
        near = 0.1, //near clip-plane
        far = 10000; //far clip-plane

    //renderer, scene and cameras
        var renderer = new THREE.WebGLRenderer({ antialias: true });
        var scene = new THREE.Scene();

        var cameraRig = new THREE.Object3D();
        var camera = new THREE.PerspectiveCamera(
        view_angle,
        aspect,
        near,
        far
        );

    //lights
        var sun = new THREE.DirectionalLight( 0xffffff );
        var sunPosition;

    //terrain
        var terrainHeightmap = new Image();
        terrainHeightmap.src = "images/heightmap_128.png";
        var heightMapSize = 128; //set to dimensions of the square heightmap
        var heightMapArea = heightMapSize * heightMapSize;
        var terrainHeightData;
        var heightFactor = 10.0; //how far heightmap translates to vertex displacement
        var tileFactor = 3.0; //how many times texures are tiled across the terrain
        var shadowmapIntensity = 165; //255 total - intensity of baked shadowmap

    //allows entering degrees rather than radians when defining rotation
        var degrees, radians;
        function degrees(degrees) {
            radians = degrees * (Math.PI / 180);
            return radians;
        }

//----Initialization----//
    function initialize() {
        //renderer
            viewport = document.getElementById('viewport');
            viewport.appendChild(renderer.domElement);
            renderer.setSize(viewportWidth, viewportHeight);
            renderer.shadowMapEnabled = true;

        //scene
            scene.fog = new THREE.Fog( 0x6495ED, 250, 2500 );

        //camera
            camera.position.set(0, 350, 950);
            //camera.lookAt(scene.position); //doesn't work when controls are applied

        //controls
            controls = new THREE.FlyControls( camera ); //pass object to be controlled
            controls.movementSpeed = 4;
            controls.domElement = viewport;
            controls.rollSpeed = 0.025;
            controls.autoForward = false;
            controls.dragToLook = true;

        //lights
            sun.castShadow = true;
            sun.shadowCameraVisible = false; //set true to see shadow frustum
            sunPosition = new THREE.Vector3(1000,800,0);
            sun.intensity = 1.8;
            sun.position.set(sunPosition.x, sunPosition.y, sunPosition.z);
            sun.shadowCameraNear = 100;
            sun.shadowCameraFar = 2500;
            sun.shadowBias = 0.0001;
            sun.shadowDarkness = 0.35;
            sun.shadowMapWidth = 1024; //512px by default
            sun.shadowMapHeight = 1024; //512px by default

        //terrain
            var terrainHeightData = getHeightData(terrainHeightmap,heightMapSize,heightMapSize);
            var terrainShadowmap = new THREE.Texture(
                generateTexture(terrainHeightData, heightMapSize, heightMapSize, shadowmapIntensity, sunPosition),
                new THREE.UVMapping(),
                THREE.ClampToEdgeWrapping,
                THREE.ClampToEdgeWrapping
            );
            terrainShadowmap.needsUpdate = true;

            //sets uniforms to be passed to the shader
            var terrainUniforms = THREE.UniformsUtils.clone( terrainShader.uniforms );
            terrainUniforms[ "tileFactor" ].value = tileFactor;
            terrainUniforms[ "heightFactor" ].value = heightFactor;
            terrainUniforms[ "texture_shadow" ].texture = terrainShadowmap;
            terrainUniforms[ "texture_dirt" ].texture = THREE.ImageUtils.loadTexture( "images/dirt.jpg" );
            terrainUniforms[ "texture_grass" ].texture = THREE.ImageUtils.loadTexture( "images/grass.jpg" );
            terrainUniforms[ "texture_rock" ].texture = THREE.ImageUtils.loadTexture( "images/rock.jpg" );

            //allows textures to be tiled
            terrainUniforms[ "texture_grass" ].texture.wrapS = THREE.RepeatWrapping;
            terrainUniforms[ "texture_grass" ].texture.wrapT = THREE.RepeatWrapping;
            terrainUniforms[ "texture_dirt" ].texture.wrapS = THREE.RepeatWrapping;
            terrainUniforms[ "texture_dirt" ].texture.wrapT = THREE.RepeatWrapping;
            terrainUniforms[ "texture_rock" ].texture.wrapS = THREE.RepeatWrapping;
            terrainUniforms[ "texture_rock" ].texture.wrapT = THREE.RepeatWrapping;

            var terrain = new THREE.Mesh(
                new THREE.PlaneGeometry(1000, 1000, heightMapSize-1, heightMapSize-1),
                new THREE.ShaderMaterial({
                    fog: true,
                    lights: true,
                    //wireframe: true,
                    uniforms: terrainUniforms,
                    vertexShader: terrainShader.vertexShader,
                    fragmentShader: terrainShader.fragmentShader
                })
            );

            //deforms the terrain plane, based on pixel data from the heightmap
            for(var i = 0, l = terrain.geometry.vertices.length; i < l; i++ ) {
                terrain.geometry.vertices[i].position.z = terrainHeightData[i] * heightFactor;
            }

            terrain.position.set(0,0,0);
            terrain.rotation.set(degrees(-90),0,0);
            //terrain.doubleSided = true;
            terrain.receiveShadow = true;
            terrain.castShadow = true;

        //lights, camera, action
            scene.add(sun);
            scene.add(camera);
            scene.add(terrain);

            update();
    }

//----Update----//
    function update() {
        //requests the browser to call update at it's own pace
        requestAnimationFrame( update );
        /*
        //crude sunset simulation
        if(sun.intensity > 0.2) {
        sun.intensity -= 0.001;
        sun.shadowDarkness -= 0.00015;
        sun.position.y -= 2.5;
        }
        */
        controls.update( 1 );
        draw();
    }

//----Draw----//
    function draw() {
        renderer.render(scene, camera);
    }

createShaders.js:

//----creatShaders----//

/*
Creates shader strings by concatenating many single-line snippets,
THREE.ShaderChunk[] takes snippets from the Three.js library,
custom.ShaderChunk[] takes snippets from the object below
*/

//custom shaders
var custom = {
    ShaderChunk: {

        terrainBlend_pars_fragment: [
        "uniform sampler2D texture_dirt;",
        "uniform sampler2D texture_grass;",
        "uniform sampler2D texture_rock;",
        "uniform sampler2D texture_shadow;",
        "uniform float heightFactor;",
        "uniform float tileFactor;",
        ].join("\n"),

        terrainBlend_fragment: [
        "// Texture loading",
        "vec4 dirt = texture2D( texture_dirt, vUv * tileFactor );",
        "vec4 grass = texture2D( texture_grass, vUv * tileFactor );",
        "vec4 rock = texture2D( texture_rock, vUv * tileFactor );",
        "vec4 shadow = texture2D( texture_shadow, vUv );",
        "vec4 sand = vec4(.8, .8, .7, 1.0);",
        "vec4 snow = vec4(.8, .9, 1.0, 1.0);",
        "vec4 color = dirt; //default texture",

        "//add sand",
        "color = mix(sand, color, min(abs(2.0*heightFactor - mvPosition.z) / 4.0/heightFactor, 1.0));",
        "//starts at 5.0 for 5.0 units in both directions",
        "//add dirt",
        "color = mix(dirt, color, min(abs(10.0*heightFactor - mvPosition.z) / 5.0/heightFactor, 1.0));",
        "//add grass",
        "color = mix(grass, color, min(abs(10.0*heightFactor - mvPosition.z) / 5.0/heightFactor, 1.0));",
        "//add rock",
        "color = mix(rock, color, min(abs(20.0*heightFactor - mvPosition.z) / 10.0/heightFactor, 1.0));",
        "//add snow",
        "color = mix(snow, color, min(abs(25.0*heightFactor - mvPosition.z) / 5.0/heightFactor, 1.0));",

        "color = color * shadow;",
        "gl_FragColor = color;",
        ].join("\n")
    }
}



//shader concatenation for blended terrain
var terrainShader = {

    uniforms: {
        texture_dirt: { type: "t", value: 0, texture: null },
        texture_grass: { type: "t", value: 1, texture: null },
        texture_rock: { type: "t", value: 2, texture: null },
        texture_shadow: { type: "t", value: 4, texture: null },
        heightFactor:  { type: "f", value: []},
        tileFactor: { type: "f", value: []},
        //] ),

        //common
        diffuse : { type: "c", value: new THREE.Color( 0xeeeeee ) },
        opacity : { type: "f", value: 1.0 },
        map : { type: "t", value: 0, texture: null },
        offsetRepeat : { type: "v4", value: new THREE.Vector4( 0, 0, 1, 1 ) },
        lightMap : { type: "t", value: 2, texture: null },
        envMap : { type: "t", value: 1, texture: null },
        flipEnvMap : { type: "f", value: -1 },
        useRefract : { type: "i", value: 0 },
        reflectivity : { type: "f", value: 1.0 },
        refractionRatio : { type: "f", value: 0.98 },
        combine : { type: "i", value: 0 },
        morphTargetInfluences : { type: "f", value: 0 },

        //fog
        fogDensity : { type: "f", value: 0.25 },
        fogNear : { type: "f", value: 10 },
        fogFar : { type: "f", value: 10000 },
        fogColor : { type: "c", value: new THREE.Color( 0x6495ED ) },

        //lights
        ambientLightColor : { type: "fv", value: [] },
        directionalLightDirection : { type: "fv", value: [] },
        directionalLightColor : { type: "fv", value: [] },
        pointLightColor : { type: "fv", value: [] },
        pointLightPosition : { type: "fv", value: [] },
        pointLightDistance : { type: "fv1", value: [] },

        //lambert shading
        ambient: { type: "c", value: new THREE.Color(328965) },
        wrapRGB: { type: "v3", value: new THREE.Vector3(1, 1, 1)},

        //shadowmap
        shadowMap: { type: "tv", value: 6, texture: [] },
        shadowMapSize: { type: "v2v", value: [] },
        shadowBias: { type: "fv1", value: [] },
        shadowDarkness: { type: "fv1", value: [] },
        shadowMatrix: { type: "m4v", value: [] }
    },



    vertexShader: [
        "varying vec2 vUv;",
        "varying vec3 mvPosition;",
        "varying vec3 vLightWeighting;",

        THREE.ShaderChunk[ "lights_lambert_pars_vertex" ],
        THREE.ShaderChunk[ "shadowmap_pars_vertex" ],

        "void main(void) {",

            "vUv = uv;",
            "mvPosition = position;",
            "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",

            //lambert light calculation
            "vec3 transformedNormal = normalize( normalMatrix * normal );",

            THREE.ShaderChunk[ "lights_lambert_vertex" ],
            THREE.ShaderChunk[ "shadowmap_vertex" ],

            "gl_Position = projectionMatrix * mvPosition;",

        "}"
    ].join("\n"),



    fragmentShader: [
        "varying vec2 vUv;",
        "varying vec3 mvPosition;",
        "varying vec3 vLightWeighting;",

        custom.ShaderChunk[ "terrainBlend_pars_fragment" ],
        THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
        THREE.ShaderChunk[ "fog_pars_fragment" ],

        "void main() {",

            custom.ShaderChunk[ "terrainBlend_fragment" ],
            "gl_FragColor.xyz = gl_FragColor.xyz * vLightWeighting;", //lambert light calculation
            THREE.ShaderChunk[ "shadowmap_fragment" ],
            THREE.ShaderChunk[ "fog_fragment" ],

        "}"
    ].join("\n")
};

getHeightData.js:

//getHeightData
//reads the pixels from an image onto a canvas then returns the values in an array
function getHeightData(img, heightMapWidth, heightMapHeight)
{
    var canvas = document.createElement( 'canvas' );
    canvas.width = heightMapWidth;
    canvas.height = heightMapHeight;
    var context = canvas.getContext( '2d' );

    var size = heightMapWidth * heightMapHeight, data = new Float32Array( size );
    context.drawImage(img,0,0);

    for ( var i = 0; i < size; i ++ ) {
        data[i] = 0
    }

    var imgd = context.getImageData(0, 0, heightMapWidth, heightMapHeight);
    var pix = imgd.data;

    var j=0;
    for (var i = 0, n = pix.length; i < n; i += (4)) {
        var all = pix[i]+pix[i+1]+pix[i+2];
        data[j++] = all/30;
    }

    return data;
}

lightmap.js:

//generates a lightmap based on a heightmap input
function generateTexture( data, width, height, intensity, sun ) {

    var canvas, canvasScaled, context, image, imageData,base, vector3, sun, shade;

    base = 255 - intensity;

    vector3 = new THREE.Vector3( 0, 0, 0 );

    sun.normalize();

    canvas = document.createElement( 'canvas' );
    canvas.width = width;
    canvas.height = height;

    context = canvas.getContext( '2d' );
    context.fillRect( 0, 0, width, height );

    image = context.getImageData( 0, 0, canvas.width, canvas.height );
    imageData = image.data;

    for ( var i = 0, j = 0, l = imageData.length; i < l; i += 4, j ++ ) {

        vector3.x = data[ j - 2 ] - data[ j + 2 ];
        vector3.y = 2;
        vector3.z = data[ j - width * 2 ] - data[ j + width * 2 ];
        vector3.normalize();

        shade = vector3.dot( sun );

        imageData[ i ] = ( base + shade * intensity );
        imageData[ i + 1 ] = ( base + shade * intensity );
        imageData[ i + 2 ] = ( base + shade * intensity ) ;
    }

    context.putImageData( image, 0, 0 );

    // Scaled 4x
    canvasScaled = document.createElement( 'canvas' );
    canvasScaled.width = width * 4;
    canvasScaled.height = height * 4;

    context = canvasScaled.getContext( '2d' );
    context.scale( 4, 4 );
    context.drawImage( canvas, 0, 0 );

    image = context.getImageData( 0, 0, canvasScaled.width, canvasScaled.height );
    imageData = image.data;

    for ( var i = 0, l = imageData.length; i < l; i += 4 ) {

        var v = ~~ ( Math.random() * 25 );

        imageData[ i ] += v;
        imageData[ i + 1 ] += v;
        imageData[ i + 2 ] += v;

    }

    context.putImageData( image, 0, 0 );

    return canvasScaled;
}

@Shawson
Copy link

Shawson commented Feb 2, 2012

I have the same problem if using THREE.UniformUtils.merge() - textures just seem to get clobbered- this doesn't work (i get shadows, but no textures)

            uniforms: THREE.UniformsUtils.merge([
                THREE.UniformsLib[ "shadowmap" ],
                {
                    "texture_grass": { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture('textures/grass.jpg') }, 
                    "texture_rock": { type: "t", value: 2, texture: THREE.ImageUtils.loadTexture('textures/rock.jpg') }, 
                    "texture_dirt": { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture('textures/rock.jpg') }
                }
            ]),

where as this works:

            uniforms: 
                {
                    "texture_grass": { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture('textures/grass.jpg') }, 
                    "texture_rock": { type: "t", value: 2, texture: THREE.ImageUtils.loadTexture('textures/rock.jpg') }, 
                    "texture_dirt": { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture('textures/rock.jpg') },

                    "shadowMap": { type: "tv", value: 6, texture: [] },
                    "shadowMapSize": { type: "v2v", value: [] },
                    "shadowBias": { type: "fv1", value: [] },
                    "shadowDarkness": { type: "fv1", value: [] },
                    "shadowMatrix": { type: "m4v", value: [] }
                },

@LiquidSnakeX
Copy link
Author

Code dump of the solution in my last post above, this one's as solved as it can be, thanks guys :]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants