Custom shaders with Three.JS: Uniforms, textures and lighting

If you’re familiar to WebGL and GLSL programming and have started using three.js, you’ll eventually run into a situation where you want to code your own shader, but at the same time use the resources that this library provides you with. In this post, I’ll show you how to setup a custom shader with a three.js geometry, pass it your own uniforms, bind a texture to a particular uniform and receive all lights that you’ve added to the scene.

Source code for demo

The source code below has been tested with three.js r84 and can be visualized here.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

<html>

<head>

<script src="three.min.js"></script>

<script src="render.js"></script>

<script id="vertShader" type="shader">

varying vec2 vUv;

varying vec3 vecPos;

varying vec3 vecNormal;

  

void main() {

  vUv = uv;

  // Since the light is in camera coordinates,

  // I'll need the vertex position in camera coords too

  vecPos = (modelViewMatrix * vec4(position, 1.0)).xyz;

  // That's NOT exacly how you should transform your

  // normals but this will work fine, since my model

  // matrix is pretty basic

  vecNormal = (modelViewMatrix * vec4(normal, 0.0)).xyz;

  gl_Position = projectionMatrix *

                vec4(vecPos, 1.0);

}

</script>

<script id="fragShader" type="shader">

precision highp float;

  

varying vec2 vUv;

varying vec3 vecPos;

varying vec3 vecNormal;

  

uniform float lightIntensity;

uniform sampler2D textureSampler;

 

struct PointLight {

  vec3 color;

  vec3 position; // light position, in camera coordinates

  float distance; // used for attenuation purposes. Since

                  // we're writing our own shader, it can

                  // really be anything we want (as long as

                  // we assign it to our light in its

                  // "distance" field

};

 

uniform PointLight pointLights[NUM_POINT_LIGHTS];

  

void main(void) {

  // Pretty basic lambertian lighting...

  vec4 addedLights = vec4(0.0,

                          0.0,

                          0.0,

                          1.0);

  for(int l = 0; l < NUM_POINT_LIGHTS; l++) {

      vec3 lightDirection = normalize(vecPos

                            - pointLights[l].position);

      addedLights.rgb += clamp(dot(-lightDirection,

                               vecNormal), 0.0, 1.0)

                         * pointLights[l].color

                         * lightIntensity;

  }

  gl_FragColor = texture2D(textureSampler, vUv)

                 * addedLights;

}

</script>

</head>

<body style="margin: 0px;" onload="init()"></body>

</html>

And this is the source for the render.js file:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

101

102

103

104

105

106

107

// standard global variables

var scene, camera, renderer, textureLoader, light;

 

// Character 3d object

var character = null;

 

// FUNCTIONS

function init() {

  // SCENE

  scene = new THREE.Scene();

  textureLoader = new THREE.TextureLoader();

 

  // CAMERA

  var SCREEN_WIDTH = window.innerWidth;

  var SCREEN_HEIGHT = window.innerHeight;

  var VIEW_ANGLE = 45;

  var ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT;

  var NEAR = 0.1;

  var FAR = 1000;

  camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT,

                                       NEAR, FAR);

  scene.add(camera);

  camera.position.set(0,0,5);

  camera.lookAt(scene.position);

 

  // RENDERER

  renderer = new THREE.WebGLRenderer({

    antialias:true,

    alpha: true

  });

  renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);

  var container = document.body;

  container.appendChild( renderer.domElement );

 

  // Create light

  light = new THREE.PointLight(0xffffff, 1.0);

  // We want it to be very close to our character

  light.position.set(0.0, 0.0, 0.1);

  scene.add(light);

 

  // Create character

  character = buildCharacter();

  scene.add(character);

 

  // Start animation

  animate();

}

 

var buildCharacter = (function() {

  var _geo = null;

 

  // Share the same geometry across all planar objects

  function getPlaneGeometry() {

    if(_geo == null) {

      _geo = new THREE.PlaneGeometry(1.0, 1.0);

    }

 

    return _geo;

  };

 

  return function() {

    var g = getPlaneGeometry();

    var creatureImage = textureLoader.load('texture.png');

    creatureImage.magFilter = THREE.NearestFilter;

 

    var mat = new THREE.ShaderMaterial({

        uniforms: THREE.UniformsUtils.merge([

            THREE.UniformsLib['lights'],

            {

                lightIntensity: {type: 'f', value: 1.0},

                textureSampler: {type: 't', value: null}

            }

        ]),

        vertexShader: document.

                      getElementById('vertShader').text,

        fragmentShader: document.

                        getElementById('fragShader').text,

        transparent: true,

        lights: true

    });

    // THREE.UniformsUtils.merge() call THREE.clone() on

    // each uniform. We don't want our texture to be

    // duplicated, so I assign it to the uniform value

    // right here.

    mat.uniforms.textureSampler.value = creatureImage;

 

    var obj = new THREE.Mesh(g, mat);

 

    return obj;

  }

})();

 

function animate() {

  // Update light profile

  var timestampNow = new Date().getTime()/1000.0;

  var lightIntensity = 0.75 +

                       0.25 * Math.cos(timestampNow *

                                       Math.PI);

 

  character.material.uniforms

           .lightIntensity.value = lightIntensity;

  light.color.setHSL(lightIntensity, 1.0, 0.5);

 

  // Render scene

  renderer.render(scene, camera);

  requestAnimationFrame(animate);

}

There’s nothing special about the init() function: It sets up the webgl renderer, creates the scene, the camera and a point light and adds an object to the scene. The function getPlaneGeometry() just instantiates a THREE.PlaneGeometry, which contains vertices, normals, and texture coordinates. The anonymous function on line 61 will create our mesh object. In three.js, a mesh is a “renderable” type formed by combining a geometry (from which three.js will create a vertex buffer object) and a material (which is basically a shader with metadata, such as uniforms). Here, we create a ShaderMaterial, which allows us to specify our custom vertex and fragment shaders. The “uniforms” parameter takes an object with the format:

1

2

3

4

UNIFORM_NAME: {

  type: UNIFORM_TYPE,

  value: UNIFORM_VALUE

}

For a guide on accepted formats, consult here. In this example, I created a float uniform named lightIntensity and a texture sampler uniform named, well, textureSampler.

Finally, on function animate I update the color uniform to a simple sinusoidal function. Three.js will automatically send the current value of each uniform to the GPU every time you call render.

Vertex shader and implicit attributes and uniforms

If you paid close attention to the vertex shader code, you’ve probably noticed that the declaration for the “position” attribute seems to be missing. The same also applies to the projectionMatrix and modelViewMatrix. In fact, three.js modifies the vertex shader given to it by appending the declaration of several attributes and uniforms. To understand the fields that are automatically created by three.js, refer to the WebGLProgram documentation.

Texture Sampler Uniform

In three.js, texture sampler uniforms have a type "t" and must be assigned to THREE.Texture objects. If your texture is an image, there’s a helper function that simplifies this task:

1

2

3

// It accepts other formats too

var loader = new THREE.TextureLoader;

loader.load("path/to/image.png")

We will draw this guy inside our polygon. Kudos for Stephen
We will draw this guy inside our polygon. Kudos for Stephen “Redshrike” Challener and William.Thompsonj

After I’ve chosen the texture image (remember to use an image whose dimensions are powers of two), our shader material is initialized in the matvariable.

Here I’ve loaded the image file and created a THREE.Texture object. I also set the magnification filter to nearest (I want my texture to be pixelated as I scale it up). More information on the filter constants used by three.js can be found on the texture constants documentation. You can also see the fields for the THREE.Texture object here.

Another important property introduced is the transparent flag, since my texture has some regions with alpha=0. By setting the transparent flag to true, three.js will automatically call gl.enable(GL_BLEND) when this object is about to be drawn. It also defers the rendering of transparent objects to after opaque objects are drawn. Also, three.js draws these objects ordered from the farthest to the closest ones.

Now that we’ve seen how to create a shader material, let’s take a look at the shader code itself.

Dealing with lights

In order to deal with light objects in your shader, you have to manually setup the required uniforms both on the material object and in your shader code.

Setting up the material

The first thing you have to do is to enable the lights flag in the material object by setting this field to true.

You also have to include the required light objects in the uniform list of your material object. Since three.js already has a public field with all light uniforms (and there’s a lot of them) in THREE.UniformsLib['lights'], you can merge them to your uniform object with the function THREE.UniformUtils.merge:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

var mat = new THREE.ShaderMaterial({

    uniforms: THREE.UniformsUtils.merge([

        THREE.UniformsLib['lights'],

        {

            lightIntensity: {type: 'f', value: 1.0},

            textureSampler: {type: 't', value: null}

        }

    ]),

    vertexShader: document.

                  getElementById('vertShader').text,

    fragmentShader: document.

                    getElementById('fragShader').text,

    transparent: true,

    lights: true

});

// THREE.UniformsUtils.merge() call THREE.clone() on

// each uniform. We don't want our texture to be

// duplicated, so I assign it to the uniform value

// right here.

mat.uniforms.textureSampler.value = creatureImage;

Create some lights

In order to test our demo, we create a point light right in front of our object. This must be done before the character creation, making sure that THREE.UniformsLib['lights'] contain the required light objects.

1

2

3

4

light = new THREE.PointLight(0xffffff, 1.0);

// We want it to be very close to our character

light.position.set(0.0, 0.0, 0.1);

scene.add(light);

Setting up the shader

In our demo, three.js is automatically sending all your lights to our light uniforms. Since we are using a point light, we’ll need to create the following uniform:

1

2

3

4

5

6

7

8

9

10

11

struct PointLight {

  vec3 color;

  vec3 position; // light position, in camera coordinates

  float distance; // used for attenuation purposes. Since

                  // we're writing our own shader, it can

                  // really be anything we want (as long

                  // as we assign it to our light in its

                  // "distance" field

};

 

uniform PointLight pointLights[NUM_POINT_LIGHTS];

Notice that the PointLight structure is in (a partial) format expected by three.js to provide the data relative to Point Lights to the shader program. In the UniformsLib documentation you can see a list of all such uniforms provided by the THREE.UniformsLib. You can also see their shader implementation here. Notice that the constant NUM_POINT_LIGHTS is automatically created by three.js, so no need to worry about it.

And that’s it! This is how the final character looks like:

Whoa! Now you look like a badass, sir!
Whoa! Now you look like a badass, sir!

Adding lights at any moment (aka adding lights at runtime)

So you want to add a light after your shader programs were compiled and are running (for instance, you may have a candle being lit in the middle of the gameplay). Two steps are required. First, add the light to the scene, as explained previously. Then, for each material (make sure you have access to them), set its needsUpdate flag to true. That should get your shader recompiled to take into account the new number of lights.

1

2

3

4

var light = new THREE.PointLight(0xffffff, 1.0);

light.position.set(0.0,0.0,0.1);

scene.add(light);

material.needsUpdate = true;

If you have access to your character (and any objects that contain a material), you can access its material in the “material” field:

1

character.material.needsUpdate = true;

Notice that you will have to do that to all materials in your scene (or at least those you think that will be affected). Also, you only have to perform shader recompilation if the number of similar lights is changed, since you would be changing the NUM_*_LIGHTS constant. Keep in mind that, if you add a point light and remove a spotlight, you will still need to update your material, as both the number of spotlights and point lights have changed.

Final thoughts

This is a pretty basic example that shows how to setup a custom shader with textures and three.js lights. Although the final effect could have been easily achieved without touching any shader, I hope this will prove useful to someone willing to write a shader that’s not available in the standard three.js material list.

Considering the fact that most cool effects are achievable by multipass rendering, I’ll eventually extend this post in the future to cover that subject as well.

 

[출처] https://csantosbh.wordpress.com/2014/01/09/custom-shaders-with-three-js-uniforms-textures-and-lighting/

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
60 Is there a limit of vertices in WebGL? 웹지엘의 제약사항 졸리운_곰 2017.08.16 553
59 JavaScript Performance Monitor file 졸리운_곰 2017.08.16 371
58 Monitor Rendering Performance Within Three.js file 졸리운_곰 2017.08.16 541
57 List of WebGL frameworks 졸리운_곰 2017.08.16 499
56 From Unity to Three.js file 졸리운_곰 2017.08.16 1750
55 Collada dae to three.js json convertor sample file 졸리운_곰 2017.08.16 579
54 THREE.WebGLShader: Shader couldn't compile - Chrome Version 44.0.2403.125 #6929 file 졸리운_곰 2017.08.10 554
53 Uniforms types 졸리운_곰 2017.08.10 547
» Custom shaders with Three.JS: Uniforms, textures and lighting file 졸리운_곰 2017.08.10 699
51 WebGL and ThreeJS Using Blender Models file 졸리운_곰 2017.08.05 355
50 Importing a Modeled Mesh From Blender to Three.js file 졸리운_곰 2017.08.05 458
49 Using SketchUp Models OrcaXS edited this page on 27 Apr · 7 revisions 졸리운_곰 2017.08.05 459
48 Three.js Loading .mtl and .obj - object stays white file 졸리운_곰 2017.08.05 541
47 [three.js] How to Build a First Person Shooter in the Browser with Three.js and WebGL/HTML5 Canvas file 졸리운_곰 2017.07.22 534
46 [three.js] WebGLRenderer 졸리운_곰 2017.07.22 490
45 Clone an Object3D model from a Collada load call 졸리운_곰 2017.07.22 345
44 [three.js] Quaternion 4원수, 사원수 회전 졸리운_곰 2017.07.08 919
43 COLLADA, TinyXML, and OpenGL file 졸리운_곰 2017.07.06 502
42 [github][gist] Mouse-Picking Collada Models with THREE.js file 졸리운_곰 2017.07.06 584
41 Mouse-Picking Collada Models with three.js, Part II file 졸리운_곰 2017.07.06 338
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED