{"id":13576299,"url":"https://github.com/stackgl/glsl-lighting-walkthrough","last_synced_at":"2025-04-07T17:10:55.472Z","repository":{"id":31816297,"uuid":"35382964","full_name":"stackgl/glsl-lighting-walkthrough","owner":"stackgl","description":":bulb: phong shading tutorial with glslify","archived":false,"fork":false,"pushed_at":"2015-05-22T14:22:36.000Z","size":791,"stargazers_count":463,"open_issues_count":1,"forks_count":31,"subscribers_count":26,"default_branch":"master","last_synced_at":"2025-03-31T15:18:49.457Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://stack.gl/glsl-lighting-walkthrough/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stackgl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-05-10T18:34:28.000Z","updated_at":"2025-03-13T15:14:39.000Z","dependencies_parsed_at":"2022-08-17T20:31:25.418Z","dependency_job_id":null,"html_url":"https://github.com/stackgl/glsl-lighting-walkthrough","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackgl%2Fglsl-lighting-walkthrough","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackgl%2Fglsl-lighting-walkthrough/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackgl%2Fglsl-lighting-walkthrough/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stackgl%2Fglsl-lighting-walkthrough/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stackgl","download_url":"https://codeload.github.com/stackgl/glsl-lighting-walkthrough/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694876,"owners_count":20980733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T15:01:08.945Z","updated_at":"2025-04-07T17:10:55.446Z","avatar_url":"https://github.com/stackgl.png","language":"JavaScript","funding_links":[],"categories":["JavaScript","Learning Resources"],"sub_categories":["Articles • Tutorials"],"readme":"## glsl-lighting-walkthrough\n\n[![final](http://i.imgur.com/9kQcKBP.png)](http://stack.gl/glsl-lighting-walkthrough/)\n\n[(live demo)](http://stack.gl/glsl-lighting-walkthrough/)\n\nThis article provides an overview of the various steps involved in lighting a mesh with a custom GLSL shader. Some of the features of the demo:\n\n- per-pixel lighting\n- flat \u0026 smooth normals\n- gamma correction for working in linear space\n- normal \u0026 specular maps for detail\n- attenuation for point light falloff\n- Oren-Nayar diffuse for rough surfaces\n- Phong reflectance model for specular highlights\n\nIt is not intended as a full-blown beginner's guide, and assumes prior knowledge of WebGL and stackgl rendering. Although it is implemented with stackgl, the same concepts and shader code could be used in ThreeJS and other frameworks.\n\nIf you have questions, comments or improvements, please [post a new issue](https://github.com/stackgl/glsl-lighting-walkthrough/issues).\n\n## contents\n\n- [running from source](#running-from-source)\n- [code overview](#code-overview)\n- [shaders](#shaders)\n- [phong](#phong)\n  - [standard derivatives](#standard-derivatives)\n  - [vertex shader](#vertex-shader)\n  - [flat normals](#flat-normals)\n  - [smooth normals](#smooth-normals)\n  - [gamma correction](#gamma-correction)\n  - [normal mapping](#normal-mapping)\n  - [light attenuation](#light-attenuation)\n  - [diffuse](#diffuse)\n  - [specular](#specular)\n  - [final color](#final-color)\n\n## running from source\n\nTo run from source:\n\n```sh\ngit clone https://github.com/stackgl/glsl-lighting-walkthrough.git\ncd glsl-lighting-walkthrough\n\nnpm install\nnpm run start\n```\n\nAnd then open `http://localhost:9966` to see the demo. Changes to the source will live-reload the browser for development.\n\nTo build:\n\n```sh\nnpm run build\n```\n\n## code overview\n\nThe code is using Babelify for ES6 template strings, destructuring, and arrow functions. It is organized like so:\n\n- [index.js](index.js) - loads images, then boots up the app\n- [lib/app.js](lib/app.js) - sets up a WebGL render loop and draws the scene\n- [lib/scene.js](lib/scene.js) - sets up textures, positions the light and draws meshes\n- [lib/create-sphere.js](lib/create-sphere.js) - create a 3D sphere for the light source\n- [lib/create-torus.js](lib/create-torus.js) - creates a 3D torus with a phong shader\n\n## shaders\n\n[glslify](https://github.com/stackgl/glslify) is used to modularize the shaders and pull some common functions from [npm](https://www.npmjs.com/).\n\nWe use a \"basic\" material for our light indicator, so that it appears at a constant color regardless of depth and lighting:\n\n- [shaders/basic.frag](lib/shaders/basic.frag)\n- [shaders/basic.vert](lib/shaders/basic.vert)\n\nWe use a \"phong\" material for our torus, which we will explore in more depth below.\n\n- [shaders/phong.frag](lib/shaders/phong.frag)\n- [shaders/phong.vert](lib/shaders/phong.vert)\n\nThere are many ways to skin a cat; this is just one approach to phong shading. \n\n## phong\n\n### standard derivatives\n\nOur phong shader uses standard derivatives, so we need to enable the extension before we create it. The JavaScript code looks like this:\n\n```js\n//enable the extension\nvar ext = gl.getExtension('OES_standard_derivatives')\nif (!ext)\n  throw new Error('derivatives not supported')\n\nvar shader = createShader(gl, vert, frag)\n...\n```\n\nAnd, in our fragment shader we need to enable it explicitly:\n\n```glsl\n#extension GL_OES_standard_derivatives : enable\nprecision highp float;\n\nvoid main() {\n  ...\n}\n```\n\nThe extension is used in two places in our final shader:\n\n- [glsl-face-normal](https://www.npmjs.com/package/glsl-face-normal) for flat shading (optional)\n- [glsl-perturb-normal](https://www.npmjs.com/package/glsl-perturb-normal) for normal-mapping\n\n### vertex shader\n\n![white](http://i.imgur.com/J24k2iu.png)\n\nOur vertex shader needs to pass the texture coordinates and view space position to the fragment shader. \n\nA basic vertex shader looks like this:\n\n```glsl\nattribute vec4 position;\nattribute vec2 uv;\n\nuniform mat4 projection;\nuniform mat4 view;\nuniform mat4 model;\n\nvarying vec2 vUv;\nvarying vec3 vViewPosition;\n\nvoid main() {\n  //determine view space position\n  mat4 modelViewMatrix = view * model;\n  vec4 viewModelPosition = modelViewMatrix * position;\n  \n  //pass varyings to fragment shader\n  vViewPosition = viewModelPosition.xyz;\n  vUv = uv;\n\n  //determine final 3D position\n  gl_Position = projection * viewModelPosition;\n}\n```\n\n### flat normals\n\n![flat](http://i.imgur.com/YvuhBGk.png)\n\nIf you want flat shading, you don't need to submit normals as a vertex attribute. Instead, you can use [glsl-face-normal](https://www.npmjs.com/package/glsl-face-normal) to estimate them in the fragment shader:\n\n```glsl\n#pragma glslify: faceNormals = require('glsl-face-normal')\n\nvarying vec3 vViewPosition;\n\nvoid main() {\n  vec3 normal = faceNormals(vViewPosition);\n  gl_FragColor = vec4(normal, 1.0);\n}\n```\n\n### smooth normals\n\n![smooth](http://i.imgur.com/hnYlRG5.png)\n\nFor smooth normals, we use the object space normals from [torus-mesh](https://www.npmjs.com/package/torus-mesh) and pass them to the fragment shader to have them interpolated between vertices.\n\nTo transform the object normals into view space, we multiply them by a \"normal matrix\" - the inverse transpose of the model view matrix.\n\nSince this doesn't change vertex to vertex, you can do it CPU-side and pass it as a uniform to the vertex shader. \n\nOr, you can just simply compute the normal matrix in the vertex step. GLSL ES does not provide built-in `transpose()` or `inverse()`, so we need to require them from npm:\n\n- [glsl-inverse](https://www.npmjs.com/package/glsl-inverse)\n- [glsl-transpose](https://www.npmjs.com/package/glsl-transpose)\n\n```glsl\n//object normals\nattribute vec3 normal;\nvarying vec3 vNormal;\n\n#pragma glslify: transpose = require('glsl-transpose')\n#pragma glslify: inverse = require('glsl-inverse')\n\nvoid main() {\n  ...\n\n  // Rotate the object normals by a 3x3 normal matrix.\n  mat3 normalMatrix = transpose(inverse(mat3(modelViewMatrix)));\n  vNormal = normalize(normalMatrix * normal);\n}\n```\n\n### gamma correction\n\nWhen dealing with PNG and JPG textures, it's important to remember that they most likely have gamma correction applied to them already, and so we need to account for it when doing any work in linear space.\n\nWe can use `pow(value, 2.2)` and `pow(value, 1.0 / 2.2)` to convert to and from the gamma-corrected space. Or, [glsl-gamma](https://github.com/stackgl/glsl-gamma) can be used for convenience.\n\n```glsl\n#pragma glslify: toLinear = require('glsl-gamma/in')\n#pragma glslify: toGamma  = require('glsl-gamma/out')\n\nvec4 textureLinear(sampler2D uTex, vec2 uv) {\n  return toLinear(texture2D(uTex, uv));\n}\n\nvoid main() {\n  //sample sRGB and account for gamma\n  vec4 diffuseColor = textureLinear(texDiffuse, uv);\n\n  //operate on RGB in linear space\n  ...\n  \n  //output final color to sRGB space\n  color = toGamma(color);\n}\n```\n\nFor details, see [GPU Gems - The Importance of Being Linear](http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html).\n\n### normal mapping\n\n![normalmap](http://i.imgur.com/cJce72J.png)\n\nWe can use normal maps to add detail to the shading without additional topology.\n\nA normal map typically stores a unit vector `[X,Y,Z]` in an image's `[R,G,B]` channels, respectively. The 0-1 colors are expanded into the -1 to 1 range, representing the unit vector.\n\n```glsl\n  // ... fragment shader ...\n\n  //sample texture and expand to -1 .. 1\n  vec3 normalMap = textureLinear(texNormal, uv) * 2.0 - 1.0;\n\n  //some normal maps use an inverted green channel\n  normalMap.y *= -1.0;\n\n  //determine perturbed surface normal\n  vec3 V = normalize(vViewPosition); \n  vec3 N = perturb(normalMap, normal, -V, vUv);\n```\n\n### light attenuation\n\n![attenuation](http://i.imgur.com/qZUMbUd.png)\n\nFor lighting, we need to determine the vector from the view space surface position to the view space light position. Then we can account for attenuation (falloff based on the distance from light), diffuse, and specular. \n\nThe relevant bits of the fragment shader:\n\n```glsl\nuniform mat4 view;\n\n#pragma glslify: attenuation = require('./attenuation')\n\nvoid main() {\n  ...\n\n  //determine surface to light vector\n  vec4 lightPosition = view * vec4(light.position, 1.0);\n  vec3 lightVector = lightPosition.xyz - vViewPosition;\n\n  //calculate attenuation\n  float lightDistance = length(lightVector);\n  float falloff = attenuation(light.radius, light.falloff, lightDistance);\n\n  //light direction\n  vec3 L = normalize(lightVector);\n\n  ...\n}\n```\n\nOur chosen [attenuation function](lib/shaders/madams-attenuation.glsl) is by Tom Madams, but there are many others that we could choose from.\n\n```glsl\nfloat attenuation(float r, float f, float d) {\n  float denom = d / r + 1.0;\n  float attenuation = 1.0 / (denom*denom);\n  float t = (attenuation - f) / (1.0 - f);\n  return max(t, 0.0);\n}\n```\n\n### diffuse\n\n![diffuse](http://i.imgur.com/pfqQCN7.png)\n\nWith our light direction, surface normal, and view direction, we can start to work on diffuse lighting. The color is multiplied by falloff to create the effect of a distant light.\n\nFor rough surfaces, [glsl-diffuse-oren-nayar](https://www.npmjs.com/package/glsl-diffuse-oren-nayar) looks a bit better than [glsl-diffuse-lambert](https://www.npmjs.com/package/glsl-diffuse-lambert). \n\n```glsl\n#pragma glslify: computeDiffuse = require('glsl-diffuse-oren-nayar')\n\n  ...\n\n  //diffuse term\n  vec3 diffuse = light.color * computeDiffuse(L, V, N, roughness, albedo) * falloff;\n  \n  //texture color\n  vec3 diffuseColor = textureLinear(texDiffuse, uv).rgb;\n```\n\nThese shading functions are known as [bidirectional reflectance distribution functions](http://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function) (BRDF).\n\n### specular\n\n![specular](http://i.imgur.com/lDimd4U.png)\n\nSimilarly, we can apply specular with one of the following BRDFs:\n\n- [glsl-specular-blinn-phong](https://www.npmjs.com/package/glsl-specular-blinn-phong)\n- [glsl-specular-phong](https://www.npmjs.com/package/glsl-specular-phong)\n- [glsl-specular-ward](https://www.npmjs.com/package/glsl-specular-ward)\n- [glsl-specular-gaussian](https://www.npmjs.com/package/glsl-specular-gaussian)\n- [glsl-specular-beckmann](https://www.npmjs.com/package/glsl-specular-beckmann)\n- [glsl-specular-cook-torrance](https://www.npmjs.com/package/glsl-specular-cook-torrance)\n\nWhich one you choose depends on the material and aesthetic you are working with. In our case, `glsl-specular-phong` looks pretty good.\n\nThe above screenshot is scaled by 100x for demonstration, using `specularScale` to drive the strength. The specular is also affected by the light attenuation.\n\n```glsl\n#pragma glslify: computeSpecular = require('glsl-specular-phong')\n\n  ...\n  \n  float specularStrength = textureLinear(texSpecular, uv).r;\n  float specular = specularStrength * computeSpecular(L, V, N, shininess);\n  specular *= specularScale;\n  specular *= falloff;\n```\n\n### final color\n\n![final](http://i.imgur.com/ZN5FmKz.png)\n\nWe now calculate the final color in the following manner. \n\n```glsl\n  ...\n  //compute final color\n  vec3 color = diffuseColor * (diffuse + light.ambient) + specular;\n```\n\nOur final color is going straight to the screen, so we should re-apply the gamma correction we removed earlier. If the color was going through a post-processing pipeline, we could continue operating in linear space until the final step. \n\n```glsl\n  ...\n  //output color\n  gl_FragColor.rgb = toGamma(color);\n  gl_FragColor.a   = 1.0;\n```\n\nThe [final result](http://stack.gl/glsl-lighting-walkthrough/). \n\n## Further Reading\n\n- [Tom Dalling - Modern OpenGL Series](http://www.tomdalling.com/blog/category/modern-opengl/)\n- [GPU Gems - The Importance of Being Linear](http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html)\n- [Normal Mapping Without Precomputed Tangents](http://www.thetenthplanet.de/archives/1180)\n\n## License\n\nMIT. See [LICENSE.md](http://github.com/stackgl/glsl-lighting-walkthrough/blob/master/LICENSE.md) for details.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstackgl%2Fglsl-lighting-walkthrough","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstackgl%2Fglsl-lighting-walkthrough","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstackgl%2Fglsl-lighting-walkthrough/lists"}