Browse Source

add custom geometry article

Gregg Tavares 6 years ago
parent
commit
d556d32cb4

+ 80 - 0
threejs/lessons/resources/cube-triangles.svg

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+    <g transform="matrix(1,0,0,1,-15.4061,33.6656)">
+        <path d="M282.782,166.14L106.405,130.681L106.405,362.129L282.782,415.893L282.782,166.14Z" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,86.2198,-67.9602)">
+        <path d="M320.18,156.154L143.561,130.681L143.561,354.814L320.18,391.547L320.18,156.154Z" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,208.218L208.602,106.592" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-8.41256)">
+        <path d="M280.195,208.218L419.219,96.606" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-9.30311)">
+        <path d="M280.195,458.862L302.628,436.773L419.219,332.889" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,439.666L208.602,330.725" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-7.03923,-23.0436)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">0</text>
+    </g>
+    <g transform="matrix(1,0,0,1,222.707,32.1174)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">1</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-6.79509,-286.708)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">2</text>
+    </g>
+    <g transform="matrix(1,0,0,1,190.583,-269.056)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">3</text>
+    </g>
+    <g transform="matrix(1,0,0,1,135.636,-167.597)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">4</text>
+    </g>
+    <g transform="matrix(1,0,0,1,359.556,-103.144)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">5</text>
+    </g>
+    <g transform="matrix(1,0,0,1,359.263,-351.098)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">7</text>
+    </g>
+    <g transform="matrix(1,0,0,1,140.837,-399.263)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">6</text>
+    </g>
+    <g transform="matrix(1,0,0,1,7.70655,-21.9953)">
+        <path d="M83.292,417.79L259.669,221.801" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        <path d="M398.693,346.316L222.074,84.559" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        <path d="M83.292,186.444L222.074,308.849" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        <path d="M83.292,186.444L398.693,110.189" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        <path d="M83.292,417.79L398.693,346.316" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        <g transform="matrix(1,0,0,1,78.7109,30.7734)">
+            <path d="M180.958,439.917L319.982,79.416" style="fill:none;stroke:rgb(217,217,217);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+        </g>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,105.803)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,337.251)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,391.015)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,265.043)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-0.379413,228.311)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,140.771)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,29.4054)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-0.379413,3.36831)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+</svg>

+ 70 - 0
threejs/lessons/resources/cube-vertex-positions.svg

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+    <g transform="matrix(1,0,0,1,-15.4061,33.6656)">
+        <path d="M282.782,166.14L106.405,130.681L106.405,362.129L282.782,415.893L282.782,166.14Z" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,86.2198,-67.9602)">
+        <path d="M320.18,156.154L143.561,130.681L143.561,354.814L320.18,391.547L320.18,156.154Z" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,208.218L208.602,106.592" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-8.41256)">
+        <path d="M280.195,208.218L419.219,96.606" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-9.30311)">
+        <path d="M280.195,458.862L302.628,436.773L419.219,332.889" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,439.666L208.602,330.725" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-7.03923,-23.0436)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">0</text>
+    </g>
+    <g transform="matrix(1,0,0,1,222.707,32.1174)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">1</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-6.79509,-286.708)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">2</text>
+    </g>
+    <g transform="matrix(1,0,0,1,190.583,-269.056)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">3</text>
+    </g>
+    <g transform="matrix(1,0,0,1,135.636,-167.597)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">4</text>
+    </g>
+    <g transform="matrix(1,0,0,1,359.556,-103.144)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">5</text>
+    </g>
+    <g transform="matrix(1,0,0,1,359.263,-351.098)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">7</text>
+    </g>
+    <g transform="matrix(1,0,0,1,140.837,-399.263)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">6</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,105.803)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,337.251)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,391.015)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,265.043)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-0.379413,228.311)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,140.771)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,29.4054)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-0.379413,3.36831)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+</svg>

+ 80 - 0
threejs/lessons/resources/cube-vertex-winding-order.svg

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+    <g transform="matrix(1,0,0,1,86.2198,-67.9602)">
+        <path d="M320.18,156.154L143.561,130.681L143.561,354.814L320.18,391.547L320.18,156.154Z" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,208.218L208.602,106.592" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-8.41256)">
+        <path d="M280.195,208.218L419.219,96.606" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-12.8192,-9.30311)">
+        <path d="M280.195,458.862L302.628,436.773L419.219,332.889" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,21.179,-43.8717)">
+        <path d="M69.82,439.666L208.602,330.725" style="fill:none;stroke:black;stroke-width:1px;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-7.03923,-23.0436)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">0</text>
+    </g>
+    <g transform="matrix(1,0,0,1,222.707,32.1174)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">1</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-6.79509,-286.708)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">2</text>
+    </g>
+    <g transform="matrix(1,0,0,1,190.583,-269.056)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">3</text>
+    </g>
+    <g transform="matrix(1,0,0,1,135.636,-167.597)">
+        <text x="57.318px" y="454.45px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">4</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-15.4061,33.6656)">
+        <path d="M282.782,166.14L106.405,130.681L106.405,362.129L282.782,415.893L282.782,166.14Z" style="fill:rgb(255,194,42);stroke:black;stroke-width:1px;"/>
+    </g>
+    <path d="M90.999,395.795L267.376,199.806L267.376,449.558L90.999,395.795Z" style="fill:rgb(255,171,5);"/>
+    <g transform="matrix(1,0,0,1,7.70655,-21.9953)">
+        <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        <g transform="matrix(-0.196629,0.980478,-0.660407,-0.13244,382.15,164.163)">
+            <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        </g>
+        <g transform="matrix(-0.742064,-0.670329,0.451504,-0.499822,102.708,520.923)">
+            <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        </g>
+    </g>
+    <g transform="matrix(-1,1.22465e-16,-1.22465e-16,-1,350.325,626.093)">
+        <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        <g transform="matrix(-0.301494,0.953468,-0.642214,-0.203073,387.947,187.32)">
+            <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        </g>
+        <g transform="matrix(-0.742064,-0.670329,0.451504,-0.499822,102.708,520.923)">
+            <path d="M103.055,232.065L95.79,232.065L105.179,208.278L114.569,232.065L107.304,232.065L107.304,366.977L103.055,366.977L103.055,232.065Z" style="fill:rgb(31,180,0);"/>
+        </g>
+    </g>
+    <g transform="matrix(1,0,0,1,7.70655,-21.9953)">
+        <path d="M83.292,417.79L259.669,221.801" style="fill:none;stroke:rgb(51,51,51);stroke-width:1px;stroke-dasharray:5,5,0,0;"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,105.803)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-139.162,337.251)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,391.015)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,265.043)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,37.2155,140.771)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,176.041,29.4054)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-0.379413,3.36831)">
+        <circle cx="230.16" cy="58.543" r="6.114"/>
+    </g>
+</svg>

+ 152 - 0
threejs/lessons/resources/heightmap-points.svg

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+    <g transform="matrix(1,0,0,1,-100,-100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(235,235,235);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-100,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(245,245,245);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-100,100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(219,219,219);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-100,200)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(229,229,229);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-100,300)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(204,204,204);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,0,-100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(223,223,223);"/>
+    </g>
+    <rect x="100" y="100" width="100" height="100" style="fill:rgb(205,205,205);"/>
+    <g transform="matrix(1,0,0,1,0,100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(193,193,193);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,0,200)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(182,182,182);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,0,300)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(189,189,189);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,100,-100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(219,219,219);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,100,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(187,187,187);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,100,100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(164,164,164);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,100,200)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(138,138,138);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,100,300)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(173,173,173);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,200,-100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(203,203,203);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,200,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(194,194,194);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,200,100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(171,171,171);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,200,200)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(156,156,156);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,200,300)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(194,194,194);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,300,-100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(209,209,209);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,300,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(217,217,217);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,300,100)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(196,196,196);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,300,200)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(172,172,172);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,300,300)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(186,186,186);"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-50,-90)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-50,10)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-50,110)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-50,210)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,-50,310)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,50,-90)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,50,10)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,50,110)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,50,210)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,50,310)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,150,-90)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,150,10)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,150,110)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,150,210)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,150,310)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,250,-90)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,250,10)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,250,110)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,250,210)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,250,310)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,350,-90)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,350,10)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,350,110)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,350,210)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1,0,0,1,350,310)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+</svg>

+ 79 - 0
threejs/lessons/resources/heightmap-triangles.svg

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+    <g transform="matrix(1.76,0,0,1.76,-176,-176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(235,235,235);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-176,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(245,245,245);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-176,176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(219,219,219);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,0,-176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(223,223,223);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,0,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(205,205,205);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,0,176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(193,193,193);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,176,-176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(219,219,219);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,176,0)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(187,187,187);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,176,176)">
+        <rect x="100" y="100" width="100" height="100" style="fill:rgb(164,164,164);"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-88,-158.4)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-4.26326e-14,-68.8)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-88,17.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,-88,193.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,88,-158.4)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,88,17.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,88,193.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,264,-158.4)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,264,17.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <g transform="matrix(1.76,0,0,1.76,264,193.6)">
+        <circle cx="100" cy="140" r="10"/>
+    </g>
+    <rect x="88" y="88" width="176" height="176" style="fill:none;stroke:black;stroke-width:1px;"/>
+    <path d="M88,90L264,264" style="fill:none;stroke:black;stroke-width:1px;"/>
+    <path d="M88,264L264,88" style="fill:none;stroke:black;stroke-width:1px;"/>
+    <g transform="matrix(1,0,0,1,-14.5571,13.91)">
+        <text x="59.142px" y="53.754px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">2</text>
+    </g>
+    <g transform="matrix(1,0,0,1,216.658,13.5467)">
+        <text x="59.142px" y="53.754px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">3</text>
+    </g>
+    <g transform="matrix(1,0,0,1,-18.3071,267.82)">
+        <text x="59.142px" y="53.754px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">0</text>
+    </g>
+    <g transform="matrix(1,0,0,1,215.978,267.476)">
+        <text x="59.142px" y="53.754px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">1</text>
+    </g>
+    <g transform="matrix(1,0,0,1,143.634,141.23)">
+        <text x="59.142px" y="53.754px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:50px;">4</text>
+    </g>
+</svg>

+ 493 - 0
threejs/lessons/threejs-custom-geometry.md

@@ -0,0 +1,493 @@
+Title: Three.js Custom Geometry
+Description: How to make your own geometry.
+
+A [previous article](threejs-primitives.html) gave a tour of
+the various built in primitives included in THREE.js. In this
+article we'll cover making our own geometry.
+
+Just to be clear, if you are serious about making 3D content
+the most common way is to use a 3D modeling package like
+[blender](https://blender.org),
+[Maya](https://www.autodesk.com/products/maya/overview),
+[3D Studio Max](https://www.autodesk.com/products/3ds-max/overview),
+[Cinema4D](https://www.maxon.net/en-us/), etc...
+You'd build a model and then export to [gLTF](threejs-load-gltf.html)
+or [.obj](threejs-load-obj.html) and load them up.
+Which ever one you choose expect to spend 2 or 3 weeks going through
+their respective tutorials as all of them have a learning curve
+to be useful.
+
+Still, there are times when we might want to generate our own
+3D geometry in code instead of using a modeling package.
+
+First let's just make a cube. Even though three.js already
+provides us with `BoxGeometry` and `BoxBufferGeometry` a
+cube is easy to understand so let's start there.
+
+There are 2 ways to make custom geometry in THREE.js one
+is with the `Geometry` class, the other is `BufferGeometry`.
+Each has their advantages. `Geometry` is arguably easier to
+use but slower and uses more memory. For few 1000s triangles
+it's a great choice but for 10s of thousands of triangles
+it might be better to use `BufferGeometry`.
+
+`BufferGeometry` is arguably harder to use but uses less
+memory and is faster. If quick rule of thumb might be
+if you're going to generate more than 10000 triangles
+consider using `BufferGeometry`.
+
+Note when I say `Geometry` is slower I mean it is slower to
+start and slower to modify but it is not slower to draw so
+if you're not planning on modifying your geometry then 
+as long as it's not too large there will only be slightly more
+delay for your program to start using `Geometry` vs using
+`BufferGeometry`. We'll go over both eventually. For now
+though let's use geometry as it's easier to understand IMO.
+
+First let's make a cube with `Geometry`. We'll start
+with an example from [the article on responsiveness](threejs-responsive.html).
+
+Let's remove the part that uses `BoxGeometry` and replace it with
+a `Geometry`.
+
+```js
+-const boxWidth = 1;
+-const boxHeight = 1;
+-const boxDepth = 1;
+-const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
++const geometry = new THREE.Geometry();
+```
+
+Now let's add the 8 corners of a cube. Here are the 8 corners.
+
+<div class="threejs_center"><img src="resources/cube-vertex-positions.svg" style="width: 500px"></div>
+
+Centered around the origin we can add the vertex positions like this
+
+```js
+const geometry = new THREE.Geometry();
++geometry.vertices.push(
++  new THREE.Vector3(-1, -1, -1),  // 0
++  new THREE.Vector3( 1, -1, -1),  // 1
++  new THREE.Vector3(-1,  1, -1),  // 2
++  new THREE.Vector3( 1,  1, -1),  // 3
++  new THREE.Vector3(-1, -1,  1),  // 4
++  new THREE.Vector3( 1, -1,  1),  // 5
++  new THREE.Vector3(-1,  1,  1),  // 6
++  new THREE.Vector3( 1,  1,  1),  // 7
++);
+```
+
+We then need to make triangles, 2 for each face of the cube
+
+<div class="threejs_center"><img src="resources/cube-triangles.svg" style="width: 500px"></div>
+
+We do that by creating `Face3` objects and specifying the indices
+of the 3 vertices that make up that face.
+
+The order we specify the vertices is important. To be pointing toward the
+outside of the cube they must be specified in a clockwise direction
+when that triangle is facing the camera.
+
+<div class="threejs_center"><img src="resources/cube-vertex-winding-order.svg" style="width: 500px"></div>
+
+Following that pattern we can specify the 12 triangles that make
+the cube like this
+
+```js
+geometry.faces.push(
+   // front
+   new THREE.Face3(0, 2, 3),
+   new THREE.Face3(0, 3, 1),
+   // right
+   new THREE.Face3(1, 3, 7),
+   new THREE.Face3(1, 7, 5),
+   // back
+   new THREE.Face3(5, 7, 6),
+   new THREE.Face3(5, 6, 4),
+   // left
+   new THREE.Face3(4, 6, 2),
+   new THREE.Face3(4, 2, 0),
+   // top
+   new THREE.Face3(2, 6, 7),
+   new THREE.Face3(2, 7, 3),
+   // bottom
+   new THREE.Face3(4, 0, 1),
+   new THREE.Face3(4, 1, 5),
+);
+```
+
+A few other minor changes to the original code and it should
+work.
+
+These cubes are twice as large as the `BoxGeometry` we were
+using before so let's move the camera back a little
+
+```js
+const fov = 75;
+const aspect = 2;  // the canvas default
+const near = 0.1;
+-const far = 5;
++const far = 100;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+-camera.position.z = 2;
++camera.position.z = 5;
+```
+
+and let's separate them a little more and I changed their colors just because
+
+```js
+const cubes = [
+-  makeInstance(geometry, 0x44aa88,  0),
+-  makeInstance(geometry, 0x8844aa, -2),
+-  makeInstance(geometry, 0xaa8844,  2),
++  makeInstance(geometry, 0x44FF44,  0),
++  makeInstance(geometry, 0x4444FF, -4),
++  makeInstance(geometry, 0xFF4444,  4),
+];
+```
+
+One last thing is we haven't added normals yet so we
+can't do any lighting. Let's change the material
+to something that doesn't need lights.
+
+```js
+function makeInstance(geometry, color, x) {
+-  const material = new THREE.MeshPhongMaterial({color});
++  const material = new THREE.MeshBasicMaterial({color});
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+
+  ...
+```
+
+and we get cubes we made ourselves.
+
+{{{example url="../threejs-custom-geometry-cube.html" }}}
+
+We can specify a color per face by setting the `color` property of
+each face.
+
+```js
+geometry.faces[ 0].color = geometry.faces[ 1].color = new THREE.Color('red');
+geometry.faces[ 2].color = geometry.faces[ 3].color = new THREE.Color('yellow');
+geometry.faces[ 4].color = geometry.faces[ 5].color = new THREE.Color('green');
+geometry.faces[ 6].color = geometry.faces[ 7].color = new THREE.Color('cyan');
+geometry.faces[ 8].color = geometry.faces[ 9].color = new THREE.Color('blue');
+geometry.faces[10].color = geometry.faces[11].color = new THREE.Color('magenta');
+```
+
+note we need to tell the material we want to use `FaceColors`
+
+```js
+-const material = new THREE.MeshBasicMaterial({color});
++const material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
+```
+
+{{{example url="../threejs-custom-geometry-cube-face-colors.html" }}}
+
+We can instead set the color of each individual vertex by setting the `vertexColors`
+property to an array of the 3 colors for the 3 vertices.
+
+```js
+geometry.faces.forEach((face, ndx) => {
+  face.vertexColors = [
+    (new THREE.Color()).setHSL(ndx / 12      , 1, 0.5),
+    (new THREE.Color()).setHSL(ndx / 12 + 0.1, 1, 0.5),
+    (new THREE.Color()).setHSL(ndx / 12 + 0.2, 1, 0.5),
+  ];
+});
+```
+
+and we need to tell the material to use vertex colors
+
+```js
+-const material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
++const material = new THREE.MeshBasicMaterial({vertexColors: THREE.VertexColors});
+```
+
+{{{example url="../threejs-custom-geometry-cube-vertex-colors.html" }}}
+
+To use lighting we need normals. Normals are vectors that specify direction.
+Just like the colors we can specify a normal for the face by setting the `normal`
+property on each face with 
+
+```js
+face.normal = new THREE.Vector3(...)`
+```
+
+or we can specify a normal for each vertex by setting the `vertexNormals`
+property with something like
+
+```js
+face.vertexNormals = [
+  new THREE.Vector3(...),
+  new THREE.Vector3(...),
+  new THREE.Vector3(...),
+]
+```
+
+but often it's much easier to just ask THREE.js to compute normals
+for us based on the positions we specified.
+
+For face normals we'd call `Geometry.computeFaceNormals` as in
+
+```js
+geometry.computeFaceNormals();
+```
+
+Removing the vertex color stuff and changing the material back to `MeshPhongMaterial`
+
+```js
++const material = new THREE.MeshBasicMaterial({vertexColors: THREE.VertexColors});
+-const material = new THREE.MeshPhongMaterial({color});
+```
+
+and now our cubes can be lit.
+
+{{{example url="../threejs-custom-geometry-cube-face-normals.html" }}}
+
+Using face normals will always give us a faceted look. We can use
+vertex normals for a smoother look by calling `Geometry.computeVertexNormals`
+
+```js
+-geometry.computeFaceNormals();
++geometry.computeVertexNormals();
+```
+
+Unfortunately a cube is not a good candidate for vertex normals since it 
+means each vertex gets its normal from the
+normals of all the faces it shares. 
+
+{{{example url="../threejs-custom-geometry-cube-vertex-normals.html" }}}
+
+Adding texture coordinates, sometimes called UVs, is done via an array of 
+layers of parallel arrays to the `faces` array which is set via `Geometry.faceVertexUvs`.
+For our cube we could do something like
+
+```js
+ geometry.faceVertexUvs[0].push(
+   // front
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // right
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // back
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // left
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // top
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // bottom
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+);
+```
+
+It's important to notice `faceVertexUvs` is an array of layers. Each layer
+is another set of UV coordinates. By default there is one layer of UV coordinates,
+layer 0, so we just add our UVs to that layer.
+
+Let's [add a texture](threejs-textures.html) to our material and switch back
+back to compute face normals
+
+```js
+-geometry.computeVertexNormals();
++geometry.computeFaceNormals();
+
++const loader = new THREE.TextureLoader();
++const texture = loader.load('resources/images/star.png');
+
+function makeInstance(geometry, color, x) {
+-  const material = new THREE.MeshPhongMaterial({color});
++  const material = new THREE.MeshPhongMaterial({color, map: texture});
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+
+  ...
+```
+
+{{{example url="../threejs-custom-geometry-cube-texcoords.html" }}}
+
+Putting that all together lets make a simple heightmap based
+terrain mesh.
+
+A heightmap based terrain is where you have a 2D array of heights
+that you apply them to a grid. An easy way to get a 2D array of heights
+is to draw them in an image editing program. Here's an image I drew.
+It's 64x64 pixels
+
+<div class="threejs_center"><img src="../resources/images/heightmap-64x64.png" style="width: 512px; image-rendering: pixelated;"></div>
+
+We'll load that and then generate a heightmap mesh from it. 
+We can use the `ImageLoader` to load the image.
+
+```js
+const imgLoader = new THREE.ImageLoader();
+imgLoader.load('resources/images/heightmap-64x64.png', createHeightmap);
+
+function createHeightmap(image) {
+  // extract the data from the image by drawing it to a canvas
+  // and calling getImageData
+  const ctx = document.createElement('canvas').getContext('2d');
+  const {width, height} = image;
+  ctx.canvas.width = width;
+  ctx.canvas.height = height;
+  ctx.drawImage(image, 0, 0);
+  const {data} = ctx.getImageData(0, 0, width, height);
+
+  const geometry = new THREE.Geometry();
+```
+
+We extracted the data from the image, now we'll make a grid of cells.
+The cells are the squares formed by the center points of each pixel
+from the image
+
+<div class="threejs_center"><img src="resources/heightmap-points.svg" style="width: 500px"></div>
+
+For each cell we'll generate 5 vertices. One for each corner of the cell
+and one at the center point of the cell with the average height of the 4
+corner heights. 
+
+```js
+const cellsAcross = width - 1;
+const cellsDeep = height - 1;
+for (let z = 0; z < cellsDeep; ++z) {
+  for (let x = 0; x < cellsAcross; ++x) {
+    // compute row offsets into the height data
+    // we multiply by 4 because the data is R,G,B,A but we
+    // only care about R
+    const base0 = (z * width + x) * 4;
+    const base1 = base0 + (width * 4);
+
+    // look up the height for the for points
+    // around this cell
+    const h00 = data[base0] / 32;
+    const h01 = data[base0 + 4] / 32;
+    const h10 = data[base1] / 32;
+    const h11 = data[base1 + 4] / 32;
+    // compute the average height
+    const hm = (h00 + h01 + h10 + h11) / 4;
+
+    // the corner positions
+    const x0 = x;
+    const x1 = x + 1;
+    const z0 = z;
+    const z1 = z + 1;
+
+    // remember the first index of these 5 vertices
+    const ndx = geometry.vertices.length;
+
+    // add the 4 corners for this cell and the midpoint
+    geometry.vertices.push(
+      new THREE.Vector3(x0, h00, z0),
+      new THREE.Vector3(x1, h01, z0),
+      new THREE.Vector3(x0, h10, z1),
+      new THREE.Vector3(x1, h11, z1),
+      new THREE.Vector3((x0 + x1) / 2, hm, (z0 + z1) / 2),
+    );
+```
+
+We'll then make 4 triangles from those 5 vertices
+
+<div class="threejs_center"><img src="resources/heightmap-triangles.svg" style="width: 500px"></div>
+
+```js
+    // create 4 triangles
+    geometry.faces.push(
+      new THREE.Face3(ndx    , ndx + 4, ndx + 1),
+      new THREE.Face3(ndx + 1, ndx + 4, ndx + 3),
+      new THREE.Face3(ndx + 3, ndx + 4, ndx + 2),
+      new THREE.Face3(ndx + 2, ndx + 4, ndx + 0),
+    );
+
+    // add the texture coordinates for each vertex of each face.
+    const u0 = x / cellsAcross;
+    const v0 = z / cellsAcross;
+    const u1 = (x + 1) / cellsDeep;
+    const v1 = (z + 1) / cellsDeep;
+    const um = (u0 + u1) / 2;
+    const vm = (v0 + v1) / 2;
+    geometry.faceVertexUvs[0].push(
+      [ new THREE.Vector2(u0, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v0) ],
+      [ new THREE.Vector2(u1, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v1) ],
+      [ new THREE.Vector2(u1, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v1) ],
+      [ new THREE.Vector2(u0, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v0) ],
+    );
+  }
+}
+```
+
+and finish it up
+
+```js
+  geometry.computeFaceNormals();
+
+  // center the geometry
+  geometry.translate(width / -2, 0, height / -2);
+
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/images/star.png');
+
+  const material = new THREE.MeshPhongMaterial({color: 'green', map: texture});
+
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+}
+```
+
+A few minor changes to make it easier to view.
+
+* include the `OrbitControls`
+
+```html
+<script src="resources/threejs/r103/three.js"></script>
++<script src="resources/threejs/r103/js/controls/OrbitControls.js"></script>
+```
+
+```js
+const fov = 75;
+const aspect = 2;  // the canvas default
+const near = 0.1;
+-const far = 100;
++const far = 200;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+-camera.position.z = 5;
++camera.position.set(20, 20, 20);
+
++const controls = new THREE.OrbitControls(camera, canvas);
++controls.target.set(0, 0, 0);
++controls.update();
+```
+
+add 2 lights
+
+```js
+-{
++function addLight(...pos) {
+  const color = 0xFFFFFF;
+  const intensity = 1;
+  const light = new THREE.DirectionalLight(color, intensity);
+-  light.position.set(-1, 2, 4\);
++  light.position.set(...pos);
+  scene.add(light);
+}
+
++addLight(-1, 2, 4);
++addLight(1, 2, -2);
+```
+
+and we deleted the code related to spinning the cubes.
+
+{{{example url="../threejs-custom-geometry-heightmap.html" }}}
+
+I hope that was a useful instruction to making your own
+geometry using `Geometry`.
+
+In another article we'll go over `BufferGeometry`.

+ 1 - 0
threejs/lessons/toc.html

@@ -49,6 +49,7 @@
     <li><a href="/threejs/lessons/threejs-shadows.html">Shadows</a></li>
     <li><a href="/threejs/lessons/threejs-fog.html">Fog</a></li>
     <li><a href="/threejs/lessons/threejs-rendertargets.html">Render Targets</a></li>
+    <li><a href="/threejs/lessons/threejs-custom-geometry.html">Custom Geometry</a></li>
   </ul>
   <li>Reference</li>
   <ul>

BIN
threejs/resources/images/heightmap-64x64.png


BIN
threejs/resources/images/star.png


+ 153 - 0
threejs/threejs-custom-geometry-cube-face-colors.html

@@ -0,0 +1,153 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube - Face Colors</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  geometry.faces[ 0].color = geometry.faces[ 1].color = new THREE.Color('red');
+  geometry.faces[ 2].color = geometry.faces[ 3].color = new THREE.Color('yellow');
+  geometry.faces[ 4].color = geometry.faces[ 5].color = new THREE.Color('green');
+  geometry.faces[ 6].color = geometry.faces[ 7].color = new THREE.Color('cyan');
+  geometry.faces[ 8].color = geometry.faces[ 9].color = new THREE.Color('blue');
+  geometry.faces[10].color = geometry.faces[11].color = new THREE.Color('magenta');
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44FF44,  0),
+    makeInstance(geometry, 0x4444FF, -4),
+    makeInstance(geometry, 0xFF4444,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 148 - 0
threejs/threejs-custom-geometry-cube-face-normals.html

@@ -0,0 +1,148 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube - face normals</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  geometry.computeFaceNormals();
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44FF44,  0),
+    makeInstance(geometry, 0x4444FF, -4),
+    makeInstance(geometry, 0xFF4444,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 172 - 0
threejs/threejs-custom-geometry-cube-texcoords.html

@@ -0,0 +1,172 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube - Texture Coordinates</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  geometry.faceVertexUvs[0].push(
+   // front
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // right
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // back
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // left
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // top
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+   // bottom
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(0, 1), new THREE.Vector2(1, 1) ],
+   [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(1, 0) ],
+);
+
+  geometry.computeFaceNormals();
+
+  const loader = new THREE.TextureLoader();
+  const texture = loader.load('resources/images/star.png');
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color, map: texture});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x88FF88,  0),
+    makeInstance(geometry, 0x8888FF, -4),
+    makeInstance(geometry, 0xFF8888,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 156 - 0
threejs/threejs-custom-geometry-cube-vertex-colors.html

@@ -0,0 +1,156 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube - Vertex Colors</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  geometry.faces.forEach((face, ndx) => {
+    face.vertexColors = [
+      (new THREE.Color()).setHSL(ndx / 12      , 1, 0.5),
+      (new THREE.Color()).setHSL(ndx / 12 + 0.1, 1, 0.5),
+      (new THREE.Color()).setHSL(ndx / 12 + 0.2, 1, 0.5),
+    ];
+  });
+
+  geometry.computeFaceNormals();
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshBasicMaterial({vertexColors: THREE.VertexColors});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44FF44,  0),
+    makeInstance(geometry, 0x4444FF, -4),
+    makeInstance(geometry, 0xFF4444,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 148 - 0
threejs/threejs-custom-geometry-cube-vertex-normals.html

@@ -0,0 +1,148 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube - Vertex Normals</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  geometry.computeVertexNormals();
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44FF44,  0),
+    makeInstance(geometry, 0x4444FF, -4),
+    makeInstance(geometry, 0xFF4444,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 146 - 0
threejs/threejs-custom-geometry-cube.html

@@ -0,0 +1,146 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Cube</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 100;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 5;
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const geometry = new THREE.Geometry();
+  geometry.vertices.push(
+    new THREE.Vector3(-1, -1, -1),  // 0
+    new THREE.Vector3( 1, -1, -1),  // 1
+    new THREE.Vector3(-1,  1, -1),  // 2
+    new THREE.Vector3( 1,  1, -1),  // 3
+    new THREE.Vector3(-1, -1,  1),  // 4
+    new THREE.Vector3( 1, -1,  1),  // 5
+    new THREE.Vector3(-1,  1,  1),  // 6
+    new THREE.Vector3( 1,  1,  1),  // 7
+  );
+
+  /*
+       6----7
+      /|   /|
+     2----3 |
+     | |  | |
+     | 4--|-5
+     |/   |/
+     0----1
+  */
+
+  geometry.faces.push(
+     // front
+     new THREE.Face3(0, 2, 3),
+     new THREE.Face3(0, 3, 1),
+     // right
+     new THREE.Face3(1, 3, 7),
+     new THREE.Face3(1, 7, 5),
+     // back
+     new THREE.Face3(5, 7, 6),
+     new THREE.Face3(5, 6, 4),
+     // left
+     new THREE.Face3(4, 6, 2),
+     new THREE.Face3(4, 2, 0),
+     // top
+     new THREE.Face3(2, 6, 7),
+     new THREE.Face3(2, 7, 3),
+     // bottom
+     new THREE.Face3(4, 0, 1),
+     new THREE.Face3(4, 1, 5),
+  );
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshBasicMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44FF44,  0),
+    makeInstance(geometry, 0x4444FF, -4),
+    makeInstance(geometry, 0xFF4444,  4),
+  ];
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = 1 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 184 - 0
threejs/threejs-custom-geometry-heightmap.html

@@ -0,0 +1,184 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Custom Geometry - Heightmap</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r103/three.js"></script>
+<script src="resources/threejs/r103/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 200;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.set(20, 20, 20);
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  function addLight(...pos) {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(...pos);
+    scene.add(light);
+  }
+
+  addLight(-1, 2, 4);
+  addLight(1, 2, -2);
+
+  const imgLoader = new THREE.ImageLoader();
+  imgLoader.load('resources/images/heightmap-64x64.png', createHeightmap);
+
+  function createHeightmap(image) {
+    // extract the data from the image by drawing it to a canvas
+    // and calling getImageData
+    const ctx = document.createElement('canvas').getContext('2d');
+    const {width, height} = image;
+    ctx.canvas.width = width;
+    ctx.canvas.height = height;
+    ctx.drawImage(image, 0, 0);
+    const {data} = ctx.getImageData(0, 0, width, height);
+
+    const geometry = new THREE.Geometry();
+
+    const cellsAcross = width - 1;
+    const cellsDeep = height - 1;
+    for (let z = 0; z < cellsDeep; ++z) {
+      for (let x = 0; x < cellsAcross; ++x) {
+        // compute row offsets into the height data
+        // we multiply by 4 because the data is R,G,B,A but we
+        // only care about R
+        const base0 = (z * width + x) * 4;
+        const base1 = base0 + (width * 4);
+
+        // look up the height for the for points
+        // around this cell
+        const h00 = data[base0] / 32;
+        const h01 = data[base0 + 4] / 32;
+        const h10 = data[base1] / 32;
+        const h11 = data[base1 + 4] / 32;
+        // compute the average height
+        const hm = (h00 + h01 + h10 + h11) / 4;
+
+        // the corner positions
+        const x0 = x;
+        const x1 = x + 1;
+        const z0 = z;
+        const z1 = z + 1;
+
+        // remember the first index of these 5 vertices
+        const ndx = geometry.vertices.length;
+
+        // add the 4 corners for this cell and the midpoint
+        geometry.vertices.push(
+          new THREE.Vector3(x0, h00, z0),
+          new THREE.Vector3(x1, h01, z0),
+          new THREE.Vector3(x0, h10, z1),
+          new THREE.Vector3(x1, h11, z1),
+          new THREE.Vector3((x0 + x1) / 2, hm, (z0 + z1) / 2),
+        );
+
+        //      2----3
+        //      |\  /|
+        //      | \/4|
+        //      | /\ |
+        //      |/  \|
+        //      0----1
+
+        // create 4 triangles
+        geometry.faces.push(
+          new THREE.Face3(ndx    , ndx + 4, ndx + 1),
+          new THREE.Face3(ndx + 1, ndx + 4, ndx + 3),
+          new THREE.Face3(ndx + 3, ndx + 4, ndx + 2),
+          new THREE.Face3(ndx + 2, ndx + 4, ndx + 0),
+        );
+
+        // add the texture coordinates for each vertex of each face.
+        const u0 = x / cellsAcross;
+        const v0 = z / cellsAcross;
+        const u1 = (x + 1) / cellsDeep;
+        const v1 = (z + 1) / cellsDeep;
+        const um = (u0 + u1) / 2;
+        const vm = (v0 + v1) / 2;
+        geometry.faceVertexUvs[0].push(
+          [ new THREE.Vector2(u0, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v0) ],
+          [ new THREE.Vector2(u1, v0), new THREE.Vector2(um, vm), new THREE.Vector2(u1, v1) ],
+          [ new THREE.Vector2(u1, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v1) ],
+          [ new THREE.Vector2(u0, v1), new THREE.Vector2(um, vm), new THREE.Vector2(u0, v0) ],
+        );
+      }
+    }
+
+    geometry.computeFaceNormals();
+
+    // center the geometry
+    geometry.translate(width / -2, 0, height / -2);
+
+    const loader = new THREE.TextureLoader();
+    const texture = loader.load('resources/images/star.png');
+
+    const material = new THREE.MeshPhongMaterial({color: 'green', map: texture});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+  }
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  function render() {
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+