3D 扩展

SpriteJSNext可以通过3D扩展库来渲染3D元素。

安装

加载3D渲染库的方式非常简单,你可以直接通过CND加载

  1. <script src="https://unpkg.com/spritejs@3/dist/spritejs.es.min.js"></script>
  2. <script src="https://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.js"></script>

只要确保sprite-extend-3d.js加载在spritejs的JS之后即可。

💡考虑到最佳性能,sprite-extend-3d.js默认适配chrome59及以上浏览器,所以你只要用spritejs的ES编译版本即可。如果你希望支持较早的浏览器,sprite-extend-3d.js仍可支持早期版本,不过你需要在项目中修改babel配置,重新编译适配旧浏览器的版本。

加载之后,可以通过spritejs.ext3d来访问3D的API,并且可以通过scene.layer3d(layerID)来创建3D的渲染层。

  1. const {Scene} = spritejs;
  2. const {Cube, shaders} = spritejs.ext3d;
  3. const container = document.getElementById('container');
  4. const scene = new Scene({container});
  5. const layer = scene.layer3d('fglayer', {
  6. camera: {
  7. fov: 35, // Field of view
  8. },
  9. });
  10. layer.camera.attributes.pos = [3, 3, 5];
  11. const program = layer.createProgram({
  12. ...shaders.NORMAL_GEOMETRY,
  13. cullFace: null,
  14. });
  15. const cube = new Cube(program, {
  16. colors: 'red red blue blue green green',
  17. });
  18. layer.append(cube);
  19. layer.setOrbit(); // 开启旋转控制

当然,你也可以通过模块方式加载sprite-extend-3d

  1. import {Scene} from 'spritejs';
  2. import {Cube, shaders} from 'sprite-extend-3d';

坐标和相机

与2D的layer不同,layer3d采用WebGL坐标系,画布中心点的位置是[0,0,0],水平向右是x轴,垂直向上是y轴,垂直于屏幕向外的是z轴。

起步 - 图1

元素是否显示出来,显示在什么位置,与透视相机有关。

一个layer对应一个默认的透视相机,创建layer的时候可以初始化它,在后续渲染的时候也可以修改它的属性。上面的例子中,我们把相机放在坐标[3, 3, 5]的位置。

透视相机有一些参数,如下:

  • near: 相机可以拍摄到的最近距离,默认为 0.1
  • far: 相机可以拍摄到的最远距离,默认为 100
  • fov: 视野宽度,默认是45度
  • aspect: 宽高比,默认是1:1,但是如果preserveAspect配置不为false,layer会根据resolution来设置相机的宽高比
  • preserveAspect: 默认为true,根据画布宽高比来保持相机宽高比,这样元素就不会被拉伸或压缩

默认情况下,相机的方向是朝着z轴负向无穷远处,而相机有一个方法叫lookAt,传入一个坐标,可以让相机朝向该位置拍摄。

绘制几何体

在3D的layer中,只要有顶点坐标就可以非常方便地绘制几何体。

  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: 5,
  13. },
  14. });
  15. const program = layer.createProgram({
  16. ...shaders.NORMAL_GEOMETRY,
  17. cullFace: null,
  18. });
  19. const p = 1 / Math.sqrt(2);
  20. const model = {
  21. position: [
  22. -1, 0, -p,
  23. 1, 0, -p,
  24. 0, 1, p,
  25. -1, 0, -p,
  26. 1, 0, -p,
  27. 0, -1, p,
  28. 1, 0, -p,
  29. 0, -1, p,
  30. 0, 1, p,
  31. -1, 0, -p,
  32. 0, 1, p,
  33. 0, -1, p],
  34. };
  35. const sprite = new Mesh3d(program, {
  36. model,
  37. colors: 'red blue green orange',
  38. });
  39. layer.append(sprite);
  40. sprite.animate([
  41. {rotateY: 0},
  42. {rotateY: 360},
  43. ], {
  44. duration: 7000,
  45. iterations: Infinity,
  46. });
  47. sprite.animate([
  48. {rotateZ: 0},
  49. {rotateZ: 360},
  50. ], {
  51. duration: 17000,
  52. iterations: Infinity,
  53. });
  54. sprite.animate([
  55. {rotateX: 0},
  56. {rotateX: -360},
  57. ], {
  58. duration: 11000,
  59. iterations: Infinity,
  60. });
  61. layer.setOrbit();

上面的代码中,我们传入一组顶点坐标,绘制了一个正四面体。

绘制模型

SpriteJSNext的3D扩展支持ThreeJS的json数据模型,因此只需要将模型以model参数传给Mesh3D对象即可。

  1. const {Scene} = spritejs;
  2. const {Mesh3d, shaders} = spritejs.ext3d;
  3. const container = document.getElementById('container');
  4. const scene = new Scene({
  5. container,
  6. displayRatio: 2,
  7. });
  8. const layer = scene.layer3d('fglayer', {
  9. camera: {
  10. fov: 45,
  11. pos: [-2, 2, 2],
  12. },
  13. directionalLight: [0.5, 1.0, -0.3, 0.15],
  14. });
  15. const program = layer.createProgram(shaders.NORMAL);
  16. const model = layer.loadModel('https://s2.ssl.qhres.com/static/bf607b5f64a91492.json');
  17. const macow = new Mesh3d(program, {model});
  18. layer.append(macow);
  19. layer.setOrbit({target: [0, 0.7, 0]});

注意上面的代码里layer.loadModel是个异步方法,但是我们并不用等到model数据真正加载下来,可以直接把model(此时是一个promise)赋给Mesh3d元素,等数据加载完毕后,元素就会被渲染出来。

有了模型,我们可以把纹理加上:

  1. const {Scene} = spritejs;
  2. const {Mesh3d, shaders} = spritejs.ext3d;
  3. const container = document.getElementById('container');
  4. const scene = new Scene({
  5. container,
  6. displayRatio: 2,
  7. });
  8. const layer = scene.layer3d('fglayer', {
  9. camera: {
  10. fov: 45,
  11. pos: [-2, 2, 2],
  12. },
  13. directionalLight: [0.5, 1.0, -0.3, 0.15],
  14. });
  15. const texture = layer.createTexture('https://p1.ssl.qhimg.com/t01b4bd0e2fb9f47550.jpg');
  16. const program = layer.createProgram({
  17. ...shaders.NORMAL_TEXTURE,
  18. texture,
  19. });
  20. const model = layer.loadModel('https://s2.ssl.qhres.com/static/bf607b5f64a91492.json');
  21. const macow = new Mesh3d(program, {model});
  22. layer.append(macow);
  23. layer.setOrbit({target: [0, 0.7, 0]});

我们只要通过layer.createTexture创建texture对象,并将该对象赋给program(同时要将programe的shader类型改为NORMAL_TEXTURE),这样元素的纹理就能显示出来。注意createTexture方法也是一个异步方法,但我们同样不用等待图片加载完毕再创建元素。

光照

SpriteJSNext默认支持几种常见的光源,我们可以设置环境光(ambientColor),方向光(directionalLight)和点光源(pointLight)。

  1. /* globals dat */
  2. const {Scene, Color} = spritejs;
  3. const {Cube, shaders} = spritejs.ext3d;
  4. const container = document.getElementById('container');
  5. const scene = new Scene({container});
  6. const layer = scene.layer3d('fglayer', {
  7. ambientColor: '#ff000080',
  8. directionalLight: [1, 0, 0, 0.5],
  9. pointLightColor: 'blue',
  10. pointLightPosition: [5, 3, 6],
  11. camera: {
  12. fov: 35, // 相机的视野
  13. pos: [3, 3, 5], // 相机的位置
  14. },
  15. });
  16. const camera = layer.camera;
  17. camera.lookAt([0, 0, 0]);
  18. const program = layer.createProgram({
  19. ...shaders.NORMAL_GEOMETRY,
  20. cullFace: null,
  21. });
  22. const cube = new Cube(program, {
  23. colors: 'grey',
  24. });
  25. layer.append(cube);
  26. layer.setOrbit();
  27. const initGui = () => {
  28. const gui = new dat.GUI();
  29. const palette = {
  30. ambientColor: 'rgba(255, 0, 0, 0.5)', // CSS string
  31. pointLightColor: '#0000ff',
  32. };
  33. gui.addColor(palette, 'ambientColor').onChange((val) => {
  34. const color = new Color(val);
  35. program.uniforms.ambientColor.value = color;
  36. layer.gl.clearColor(...color);
  37. layer.forceUpdate();
  38. });
  39. gui.addColor(palette, 'pointLightColor').onChange((val) => {
  40. const color = new Color(val);
  41. program.uniforms.pointLightColor.value = color;
  42. layer.forceUpdate();
  43. });
  44. };
  45. initGui();

与元素交互

SpriteJSNext的3D扩展中,与元素交互也非常简单,在前面的例子我们已经见过。

一种交互是通过鼠标或触屏旋转角度和缩放,只需要一条语句:

  1. layer.setOrbit({target: [x, y, z]});

另一种交互是让元素支持点击事件,也只需要一条语句:

  1. layer.setRaycast();

有了这条语句之后,我们就可以在元素上像2D那样注册鼠标或触屏事件了。

  1. layer.addEventListener('click', (evt) => {
  2. if(evt.target === cube) {
  3. const colors = [];
  4. for(let i = 0; i < 3; i++) {
  5. const randomColor = `hsl(${Math.floor(360 * Math.random())}, 50%, 50%)`;
  6. colors.push(randomColor, randomColor);
  7. }
  8. evt.target.attributes.colors = colors;
  9. } else if(evt.target !== layer) {
  10. evt.target.attributes.colors = `hsl(${Math.floor(360 * Math.random())}, 50%, 50%)`;
  11. }
  12. });