模型

前面我们已经看到过,只要给元素传入一组顶点,就可以渲染出来。这一组顶点的数据,就是元素的模型 model。

但是实际上只有顶点是不够的,如果我们要比较逼真的点光源漫反射效果,还要计算出每个面的法向量 normal。

  1. const {Scene} = spritejs;
  2. const {Mesh3d, shaders} = spritejs.ext3d;
  3. const container = document.getElementById('container');
  4. const scene = new Scene({
  5. container,
  6. width: 600,
  7. height: 600,
  8. });
  9. const layer = scene.layer3d('fglayer', {
  10. camera: {
  11. fov: 35,
  12. z: 8,
  13. },
  14. });
  15. const program = layer.createProgram({
  16. ...shaders.NORMAL,
  17. cullFace: null,
  18. });
  19. function normalize(v) {
  20. const len = Math.hypot(...v);
  21. return [v[0] / len, v[1] / len, v[2] / len];
  22. }
  23. // 两个向量的叉积就是这个向量的法向量
  24. function getNormal(a, b, c) {
  25. const ab = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
  26. const bc = [b[0] - c[0], b[1] - c[1], b[2] - c[2]];
  27. return normalize([
  28. ab[1] * bc[2] - ab[2] * bc[1],
  29. ab[0] * bc[2] - ab[2] * bc[0],
  30. ab[0] * bc[1] - ab[1] * bc[0],
  31. ]);
  32. }
  33. const p = 1 / Math.sqrt(2);
  34. const position = [
  35. -1, 0, -p,
  36. 1, 0, -p,
  37. 0, 1, p,
  38. -1, 0, -p,
  39. 1, 0, -p,
  40. 0, -1, p,
  41. 1, 0, -p,
  42. 0, -1, p,
  43. 0, 1, p,
  44. -1, 0, -p,
  45. 0, 1, p,
  46. 0, -1, p,
  47. ];
  48. const normal = [];
  49. for(let i = 0; i < position.length; i += 9) {
  50. const a = [position[i], position[i + 1], position[i + 2]],
  51. b = [position[i + 3], position[i + 4], position[i + 5]],
  52. c = [position[i + 6], position[i + 7], position[i + 8]];
  53. const norm = getNormal(a, b, c);
  54. normal.push(...norm, ...norm, ...norm);
  55. }
  56. const model = {
  57. position,
  58. normal,
  59. };
  60. const sprite = new Mesh3d(program, {
  61. model,
  62. });
  63. layer.append(sprite);
  64. sprite.animate([
  65. {rotateY: 0},
  66. {rotateY: 360},
  67. ], {
  68. duration: 7000,
  69. iterations: Infinity,
  70. });
  71. sprite.animate([
  72. {rotateZ: 0},
  73. {rotateZ: 360},
  74. ], {
  75. duration: 17000,
  76. iterations: Infinity,
  77. });
  78. sprite.animate([
  79. {rotateX: 0},
  80. {rotateX: -360},
  81. ], {
  82. duration: 11000,
  83. iterations: Infinity,
  84. });
  85. layer.setOrbit();

我们可以通过两个向量的叉积计算出这两个向量所在平面的法向量。

不过这样计算顶点和法向量也挺麻烦的,所以3D扩展内置了一些几何体,直接创建这些几何体的元素,就不用自己手动计算了。

目前支持的内置几何体包括以下几类:

  • Cube - 立方体
  • Cylinder - 圆柱体
  • Plane - 平面
  • Sphere - 球体

你可以像操作普通的2D元素那样给这些对象设置属性,比如:

  1. cube.attr({
  2. width: 1.0,
  3. height: 0.5,
  4. depth: 0.5,
  5. });

批量绘制

我们可以通过给program传额外的attribute来做一些特别的事情,如果我们传的数据对象有instanced属性,那么引擎会启用批量绘制。

要动态传给元素model数据,我们可以在创建program的时候传入一个attributes配置,这个配置的内容是一组getter。

  1. const fragment = `
  2. precision highp float;
  3. precision highp int;
  4. varying vec3 vNormal;
  5. varying vec4 vColor;
  6. uniform vec4 lighting;
  7. void main() {
  8. vec3 normal = normalize(vNormal);
  9. float l = dot(normal, normalize(lighting.xyz));
  10. gl_FragColor.rgb = vColor.rgb + l * lighting.w;
  11. gl_FragColor.a = vColor.a;
  12. }
  13. `;
  14. const vertex = `
  15. precision highp float;
  16. precision highp int;
  17. attribute vec3 position;
  18. attribute vec3 normal;
  19. attribute vec4 color;
  20. attribute vec3 offset;
  21. attribute vec2 random;
  22. uniform mat4 modelViewMatrix;
  23. uniform mat4 projectionMatrix;
  24. uniform mat3 normalMatrix;
  25. varying vec3 vNormal;
  26. varying vec4 vColor;
  27. void rotate2d(inout vec2 v, float a){
  28. mat2 m = mat2(cos(a), -sin(a), sin(a), cos(a));
  29. v = m * v;
  30. }
  31. void main() {
  32. vNormal = normalize(normalMatrix * normal);
  33. vColor = color;
  34. vec3 pos = position;
  35. rotate2d(pos.xz, random.x * 6.28);
  36. rotate2d(pos.xy, random.y * 6.28);
  37. gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0) + vec4(offset, 1.0);
  38. }
  39. `;
  40. const {Scene} = spritejs;
  41. const {Cube} = spritejs.ext3d;
  42. const container = document.getElementById('container');
  43. const scene = new Scene({
  44. container,
  45. displayRatio: 2,
  46. });
  47. const layer = scene.layer3d('fglayer', {
  48. camera: {
  49. fov: 35,
  50. pos: [0, 0, 7],
  51. },
  52. });
  53. layer.camera.lookAt([0, -0.5, 0]);
  54. const program = layer.createProgram({
  55. vertex,
  56. fragment,
  57. cullFace: null,
  58. uniforms: {
  59. lighting: {value: [-0.3, 0.8, 0.6, 0.1]},
  60. },
  61. }, {
  62. attributes: {
  63. offset(node, geometry) {
  64. const data = new Float32Array(5 * 5 * 3);
  65. for(let i = 0; i < 5; i++) {
  66. const p = -5 + 2 * i;
  67. for(let j = 0; j < 5; j++) {
  68. const q = -5 + 2 * j;
  69. data.set([p, q, 0], (i * 5 + j) * 3);
  70. }
  71. }
  72. return {instanced: 1, size: 3, data};
  73. },
  74. random(node, geometry) {
  75. const data = new Float32Array(5 * 5 * 2);
  76. for(let i = 0; i < 25; i++) {
  77. data.set([Math.random() * 2 - 1, Math.random() * 2 - 1], i * 2);
  78. }
  79. return {instanced: 1, size: 2, data};
  80. },
  81. },
  82. });
  83. const cube = new Cube(program);
  84. cube.attributes.pos = [0, 0, 0];
  85. cube.attributes.colors = 'red red blue blue orange orange';
  86. cube.attributes.scale = 0.5;
  87. layer.append(cube);
  88. cube.animate([
  89. {rotateY: 0},
  90. {rotateY: -360},
  91. ], {
  92. duration: 5000,
  93. iterations: Infinity,
  94. });

共享模型

有时候,我们要创建许多个相同模型的对象,如果我们直接将model传给这些对象,那么它们之间的数据是不共享的,也就是说,我们可以改变一个元素的几何体数据,不会影响另一个元素。

但是,如果我们确定它们可以共享几何形状,那么我们可以先创建Geometry对象:

  1. const model = {
  2. position,
  3. normal,
  4. };
  5. const geometry = new Geometry({
  6. position: {size: 3, data: new Float32Array(model.position)},
  7. normal: {size: 3, data: new Float32Array(model.normal)},
  8. });
  9. const s1 = new Mesh3d(program, {model: geometry});
  10. const s2 = new Mesh3d(program, {model: geometry}); // s1、s2 共享几何形状

如果要创建很多个元素,共享几何形状能够减少内存消耗。

💡如果使用cloneNode,被clone的对象和clone对象会自动共享几何形状。

如果我们要取消几何形状的共享(因为共享几何形状如果改变颜色,所有的元素会一起改变),我们可以调用对应元素的remesh方法,该方法刷新它的geometrymesh信息。