{T}

性能分析

性能分析是优化 Three.js 应用的第一步,通过系统化的监控和分析找出性能瓶颈,为后续优化提供数据支撑。

概述

Three.js 应用性能受多种因素影响,包括渲染效率、内存使用、CPU 计算等。性能分析的核心目标是:

  1. 识别瓶颈:找出影响性能的关键因素
  2. 量化评估:通过具体数据评估优化效果
  3. 持续监控:实时监控运行时性能状态
  4. 预防问题:在问题发生前发现潜在风险

性能分析流程

plaintext
┌─────────────┐
│  建立基准   │ ──> 测试初始性能指标
└──────┬──────┘


┌─────────────┐
│  监控采集   │ ──> 使用工具收集运行数据
└──────┬──────┘


┌─────────────┐
│  分析定位   │ ──> 识别性能瓶颈位置
└──────┬──────┘


┌─────────────┐
│  优化改进   │ ──> 针对性优化处理
└──────┬──────┘


┌─────────────┐
│  验证效果   │ ──> 对比优化前后数据
└─────────────┘

系统架构

性能分析系统组成

plaintext
┌────────────────────────────────────────────────────────┐
│                   性能分析系统架构                       │
├────────────────────────────────────────────────────────┤
│                                                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │  数据采集层  │  │  分析处理层  │  │  可视化层    │ │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤ │
│  │ • Stats.js   │  │ • FPS 计算   │  │ • 实时面板   │ │
│  │ • Performance│  │ • 渲染计时   │  │ • 控制台报告 │ │
│  │   API        │  │ • 内存分析   │  │ • 图表展示   │ │
│  │ • WebGL Info │  │ • 泄漏检测   │  │ • 导出报告   │ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│                                                        │
└────────────────────────────────────────────────────────┘

核心监控指标

指标类型指标名称说明目标值
帧率FPS每秒渲染帧数≥ 60 FPS
帧率帧时间单帧渲染时间≤ 16.67ms
渲染Draw Calls每帧绘制调用次数尽可能少
渲染三角形数量每帧渲染的三角形数根据设备调整
内存JS 堆内存JavaScript 内存使用无持续增长
内存GPU 内存纹理、几何体占用< 显存限制
资源几何体数量场景中几何体数合理范围
资源纹理数量场景中纹理数合理范围

性能指标参考

FPS 性能标准

性能等级FPS 范围用户体验建议操作
优秀≥ 60流畅,无卡顿保持现状
良好45-59基本流畅可接受,持续监控
一般30-44有轻微卡顿需要优化
较差15-29明显卡顿必须优化
极差< 15无法使用紧急优化

设备性能分级

设备类型三角形数Draw Calls纹理内存建议
高端 PC< 5M< 2000< 512MB可使用完整特效
中端 PC< 2M< 1000< 256MB适度降级
低端 PC< 500K< 500< 128MB大幅降级
高端移动< 500K< 300< 128MB简化效果
中端移动< 200K< 150< 64MB基础效果
低端移动< 50K< 50< 32MB最简配置

Stats.js 性能监控

基础使用

Stats.js 是最常用的性能监控工具,可以实时显示 FPS、帧时间和内存使用。

javascript
import Stats from 'three/addons/libs/stats.module.js';
 
// 创建性能监视器
const stats = new Stats();
stats.showPanel(0);  // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
 
// 在渲染循环中更新
function animate() {
  stats.begin();
  
  // 渲染代码
  renderer.render(scene, camera);
  
  stats.end();
  
  requestAnimationFrame(animate);
}
 
animate();

多面板配置

Stats.js 提供多种监控面板:

面板 ID类型说明适用场景
0FPS帧率监控基础性能监控
1MS帧渲染时间分析渲染耗时
2MB内存使用内存泄漏检测
3+Custom自定义指标特定功能监控
javascript
const stats = new Stats();
 
// FPS 面板
stats.showPanel(0);
 
// MS 面板(渲染时间)
stats.showPanel(1);
 
// MB 面板(内存使用)
stats.showPanel(2);
 
// 切换面板(点击)
stats.dom.style.position = 'absolute';
stats.dom.style.left = '0px';
stats.dom.style.top = '0px';
document.body.appendChild(stats.dom);

自定义监控面板

javascript
// 创建自定义面板
const stats = new Stats();
stats.showPanel(3);  // 自定义面板
 
// 自定义监控逻辑
const customPanel = stats.addPanel(
  new Stats.Panel('Objects', '#ff8', '#221')
);
 
let objectCount = 0;
 
function animate() {
  stats.begin();
  
  // 更新自定义面板
  customPanel.update(objectCount, 1000);
  
  // 渲染逻辑...
  objectCount = scene.children.length;
  
  stats.end();
  
  requestAnimationFrame(animate);
}

浏览器开发工具

Performance 面板

使用浏览器 Performance API 进行精确的性能分析。

javascript
// 使用 performance API 标记时间点
performance.mark('render-start');
 
renderer.render(scene, camera);
 
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');
 
// 获取测量结果
const measures = performance.getEntriesByName('render');
console.log('渲染时间:', measures[0].duration, 'ms');
 
// 清理
performance.clearMarks();
performance.clearMeasures();

性能分析函数

javascript
// 高精度性能分析
function measurePerformance(label, fn) {
  performance.mark(`${label}-start`);
  
  const result = fn();
  
  performance.mark(`${label}-end`);
  performance.measure(label, `${label}-start`, `${label}-end`);
  
  const measure = performance.getEntriesByName(label)[0];
  console.log(`${label}: ${measure.duration.toFixed(2)}ms`);
  
  performance.clearMarks();
  performance.clearMeasures();
  
  return result;
}
 
// 使用示例
measurePerformance('scene-update', () => {
  scene.traverse(obj => {
    // 更新逻辑
  });
});

Memory 面板

javascript
// 监控内存使用
if (performance.memory) {
  console.log('已使用堆大小:', performance.memory.usedJSHeapSize / 1024 / 1024, 'MB');
  console.log('总堆大小:', performance.memory.totalJSHeapSize / 1024 / 1024, 'MB');
  console.log('堆限制:', performance.memory.jsHeapSizeLimit / 1024 / 1024, 'MB');
}
 
// 定期监控
setInterval(() => {
  if (performance.memory) {
    const used = performance.memory.usedJSHeapSize / 1024 / 1024;
    console.log(`内存使用: ${used.toFixed(2)} MB`);
  }
}, 1000);

内存监控类

javascript
class MemoryMonitor {
  constructor() {
    this.samples = [];
    this.maxSamples = 100;
    this.warningThreshold = 100 * 1024 * 1024; // 100MB
  }
  
  sample() {
    if (performance.memory) {
      const memory = {
        timestamp: Date.now(),
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize
      };
      
      this.samples.push(memory);
      
      if (this.samples.length > this.maxSamples) {
        this.samples.shift();
      }
      
      // 检查阈值
      if (memory.used > this.warningThreshold) {
        console.warn(`内存使用过高: ${(memory.used / 1024 / 1024).toFixed(2)} MB`);
      }
      
      return memory;
    }
    return null;
  }
  
  getStats() {
    if (this.samples.length === 0) return null;
    
    const used = this.samples.map(s => s.used);
    return {
      current: used[used.length - 1] / 1024 / 1024,
      average: used.reduce((a, b) => a + b) / used.length / 1024 / 1024,
      peak: Math.max(...used) / 1024 / 1024,
      min: Math.min(...used) / 1024 / 1024
    };
  }
}
 
// 使用
const memoryMonitor = new MemoryMonitor();
setInterval(() => memoryMonitor.sample(), 1000);

Three.js 内置分析

Renderer 信息

WebGLRenderer 提供了丰富的渲染统计信息。

javascript
// 渲染器信息
console.log('渲染器:', renderer.info.render);
console.log('几何体数量:', renderer.info.memory.geometries);
console.log('纹理数量:', renderer.info.memory.textures);
console.log('程序数量:', renderer.info.programs);
 
// 在动画循环中监控
function animate() {
  requestAnimationFrame(animate);
  
  console.log('Draw Calls:', renderer.info.render.calls);
  console.log('三角形数量:', renderer.info.render.triangles);
  console.log('点数量:', renderer.info.render.points);
  
  renderer.render(scene, camera);
}

renderer.info 属性详解

属性路径类型说明
info.render.callsNumber每帧 Draw Calls 数量
info.render.trianglesNumber渲染的三角形数量
info.render.pointsNumber渲染的点数量
info.render.linesNumber渲染的线段数量
info.render.frameNumber帧计数
info.memory.geometriesNumber几何体数量
info.memory.texturesNumber纹理数量
info.programsArrayShader 程序列表

WebGL 信息

获取 WebGL 上下文的详细信息。

javascript
// WebGL 上下文信息
const gl = renderer.getContext();
 
console.log('WebGL 版本:', gl.getParameter(gl.VERSION));
console.log('GLSL 版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
console.log('厂商:', gl.getParameter(gl.VENDOR));
console.log('渲染器:', gl.getParameter(gl.RENDERER));
 
// 纹理单元数量
console.log('纹理单元:', gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS));
 
// 顶点属性数量
console.log('顶点属性:', gl.getParameter(gl.MAX_VERTEX_ATTRIBS));
 
// 最大纹理尺寸
console.log('最大纹理尺寸:', gl.getParameter(gl.MAX_TEXTURE_SIZE));
 
// 各向异性过滤
const ext = gl.getExtension('EXT_texture_filter_anisotropic');
if (ext) {
  console.log('最大各向异性:', gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT));
}

WebGL 能力检测

javascript
function getWebGLCapabilities(renderer) {
  const gl = renderer.getContext();
  
  return {
    version: gl.getParameter(gl.VERSION),
    glslVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
    vendor: gl.getParameter(gl.VENDOR),
    renderer: gl.getParameter(gl.RENDERER),
    maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
    maxTextureUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
    maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
    maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
    maxVertexUniformVectors: gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
    maxFragmentUniformVectors: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)
  };
}
 
// 使用
const capabilities = getWebGLCapabilities(renderer);
console.table(capabilities);

自定义性能分析器

帧率计算器

javascript
class FPSCounter {
  constructor() {
    this.frames = 0;
    this.lastTime = performance.now();
    this.fps = 0;
    this.history = [];
    this.maxHistory = 60;
  }
  
  update() {
    this.frames++;
    
    const currentTime = performance.now();
    const delta = currentTime - this.lastTime;
    
    if (delta >= 1000) {
      this.fps = this.frames * 1000 / delta;
      this.history.push(this.fps);
      
      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }
      
      this.frames = 0;
      this.lastTime = currentTime;
    }
  }
  
  getAverageFPS() {
    if (this.history.length === 0) return 0;
    return this.history.reduce((a, b) => a + b) / this.history.length;
  }
  
  getMinFPS() {
    if (this.history.length === 0) return 0;
    return Math.min(...this.history);
  }
  
  getMaxFPS() {
    if (this.history.length === 0) return 0;
    return Math.max(...this.history);
  }
  
  getStats() {
    return {
      current: this.fps,
      average: this.getAverageFPS(),
      min: this.getMinFPS(),
      max: this.getMaxFPS()
    };
  }
}
 
// 使用
const fpsCounter = new FPSCounter();
 
function animate() {
  requestAnimationFrame(animate);
  
  fpsCounter.update();
  
  console.log(`FPS: ${fpsCounter.fps.toFixed(1)}`);
  console.log(`平均 FPS: ${fpsCounter.getAverageFPS().toFixed(1)}`);
  
  renderer.render(scene, camera);
}

渲染时间分析器

javascript
class RenderProfiler {
  constructor() {
    this.timings = {};
    this.enabled = true;
    this.history = {};
  }
  
  start(label) {
    if (!this.enabled) return;
    this.timings[label] = performance.now();
  }
  
  end(label) {
    if (!this.enabled || !this.timings[label]) return;
    
    const duration = performance.now() - this.timings[label];
    
    // 记录历史
    if (!this.history[label]) {
      this.history[label] = [];
    }
    this.history[label].push(duration);
    
    // 限制历史长度
    if (this.history[label].length > 60) {
      this.history[label].shift();
    }
    
    console.log(`${label}: ${duration.toFixed(2)}ms`);
    delete this.timings[label];
  }
  
  profile(label, fn) {
    this.start(label);
    const result = fn();
    this.end(label);
    return result;
  }
  
  getStats(label) {
    const history = this.history[label];
    if (!history || history.length === 0) return null;
    
    return {
      average: history.reduce((a, b) => a + b) / history.length,
      min: Math.min(...history),
      max: Math.max(...history),
      samples: history.length
    };
  }
  
  reset() {
    this.timings = {};
    this.history = {};
  }
}
 
// 使用
const profiler = new RenderProfiler();
 
function animate() {
  requestAnimationFrame(animate);
  
  profiler.start('total');
  
  profiler.start('update');
  updateScene();
  profiler.end('update');
  
  profiler.start('render');
  renderer.render(scene, camera);
  profiler.end('render');
  
  profiler.end('total');
  
  // 获取统计信息
  console.log(profiler.getStats('render'));
}

场景分析器

javascript
class SceneAnalyzer {
  constructor(scene) {
    this.scene = scene;
  }
  
  analyze() {
    const result = {
      meshes: 0,
      triangles: 0,
      vertices: 0,
      materials: new Set(),
      geometries: new Set(),
      textures: new Set(),
      lights: 0,
      cameras: 0
    };
    
    this.scene.traverse((object) => {
      if (object.isMesh) {
        result.meshes++;
        
        if (object.geometry) {
          result.geometries.add(object.geometry);
          
          if (object.geometry.index) {
            result.triangles += object.geometry.index.count / 3;
          } else {
            result.triangles += object.geometry.attributes.position.count / 3;
          }
          
          result.vertices += object.geometry.attributes.position.count;
        }
        
        if (object.material) {
          if (Array.isArray(object.material)) {
            object.material.forEach(m => result.materials.add(m));
          } else {
            result.materials.add(object.material);
          }
        }
      }
      
      if (object.isLight) result.lights++;
      if (object.isCamera) result.cameras++;
    });
    
    // 分析纹理
    result.materials.forEach(material => {
      const textureProps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap'];
      textureProps.forEach(prop => {
        if (material[prop]) {
          result.textures.add(material[prop]);
        }
      });
    });
    
    return {
      meshes: result.meshes,
      triangles: Math.round(result.triangles),
      vertices: result.vertices,
      uniqueMaterials: result.materials.size,
      uniqueGeometries: result.geometries.size,
      uniqueTextures: result.textures.size,
      lights: result.lights,
      cameras: result.cameras
    };
  }
}
 
// 使用
const analyzer = new SceneAnalyzer(scene);
const analysis = analyzer.analyze();
 
console.log('场景分析:', analysis);

性能分析最佳实践

分段分析策略

javascript
// 分段性能分析
class PerformanceAnalyzer {
  constructor() {
    this.sections = new Map();
  }
  
  startSection(name) {
    this.sections.set(name, {
      start: performance.now(),
      end: null,
      duration: null
    });
  }
  
  endSection(name) {
    const section = this.sections.get(name);
    if (section) {
      section.end = performance.now();
      section.duration = section.end - section.start;
    }
  }
  
  getReport() {
    const report = {};
    this.sections.forEach((value, key) => {
      report[key] = value.duration ? value.duration.toFixed(2) + 'ms' : 'incomplete';
    });
    return report;
  }
}
 
// 使用
const analyzer = new PerformanceAnalyzer();
 
function animate() {
  analyzer.startSection('frame');
  
  analyzer.startSection('physics');
  updatePhysics();
  analyzer.endSection('physics');
  
  analyzer.startSection('animation');
  updateAnimations();
  analyzer.endSection('animation');
  
  analyzer.startSection('render');
  renderer.render(scene, camera);
  analyzer.endSection('render');
  
  analyzer.endSection('frame');
  
  console.log(analyzer.getReport());
}

性能报告生成

javascript
function generatePerformanceReport() {
  const report = {
    timestamp: new Date().toISOString(),
    renderer: {
      info: {
        render: renderer.info.render,
        memory: renderer.info.memory
      },
      capabilities: {
        maxTextures: renderer.capabilities.maxTextures,
        maxVertexTextures: renderer.capabilities.maxVertexTextures,
        maxTextureSize: renderer.capabilities.maxTextureSize,
        precision: renderer.capabilities.precision
      }
    },
    memory: performance.memory ? {
      used: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
      total: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
      limit: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB'
    } : 'Not available',
    performance: {
      fps: fpsCounter.getStats(),
      frameTime: profiler.getStats('frame')
    }
  };
  
  return report;
}
 
// 生成报告
const report = generatePerformanceReport();
console.table(report.renderer.info.render);
console.table(report.memory);

API 参考

Stats.js API

方法参数说明
showPanel(id)id: Number显示指定面板(0: FPS, 1: MS, 2: MB)
begin()-开始计时
end()-结束计时
update()-更新显示(替代 begin/end)
addPanel(panel)panel: Panel添加自定义面板

Performance API

方法参数说明
performance.mark(name)name: String创建命名时间戳
performance.measure(name, start, end)name, start, end测量两个标记间的时间
performance.getEntriesByName(name)name: String获取指定名称的测量结果
performance.clearMarks()-清除所有标记
performance.clearMeasures()-清除所有测量

WebGLRenderer.info 属性

属性类型说明
info.render.callsNumberDraw Calls 数量
info.render.trianglesNumber三角形数量
info.render.pointsNumber点数量
info.render.linesNumber线段数量
info.memory.geometriesNumber几何体数量
info.memory.texturesNumber纹理数量

常见问题

Q1: FPS 为什么不稳定?

原因分析:

  • 垃圾回收(GC)导致的卡顿
  • 复杂场景导致的渲染时间波动
  • JavaScript 主线程阻塞

解决方案:

javascript
// 1. 避免在渲染循环中创建对象
// 不推荐
function animate() {
  const position = new THREE.Vector3();  // 每帧创建新对象
  // ...
}
 
// 推荐
const position = new THREE.Vector3();  // 复用对象
function animate() {
  position.set(0, 0, 0);
  // ...
}
 
// 2. 使用对象池
class ObjectPool {
  constructor(factory, size = 10) {
    this.pool = Array(size).fill(null).map(() => factory());
    this.index = 0;
  }
  
  get() {
    const obj = this.pool[this.index % this.pool.length];
    this.index++;
    return obj;
  }
}

Q2: Draw Calls 过高如何优化?

优化策略:

场景优化方法效果
多个相同几何体InstancedMesh大幅减少
多个不同几何体几何体合并中等减少
多个不同材质材质共享小幅减少
静态场景预合并大幅减少
javascript
// 使用 InstancedMesh 优化相同几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial();
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
 
// 设置实例矩阵
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
  matrix.setPosition(Math.random() * 10, Math.random() * 10, Math.random() * 10);
  mesh.setMatrixAt(i, matrix);
}
 
scene.add(mesh);
// Draw Calls: 1(而不是 1000)

Q3: 如何检测内存泄漏?

检测方法:

javascript
// 1. 使用 Memory 监控
class MemoryLeakDetector {
  constructor() {
    this.samples = [];
  }
  
  sample() {
    if (performance.memory) {
      this.samples.push({
        time: Date.now(),
        used: performance.memory.usedJSHeapSize
      });
    }
  }
  
  detectLeak() {
    if (this.samples.length < 20) return false;
    
    const recent = this.samples.slice(-10);
    const older = this.samples.slice(-20, -10);
    
    const recentAvg = recent.reduce((a, b) => a + b.used, 0) / recent.length;
    const olderAvg = older.reduce((a, b) => a + b.used, 0) / older.length;
    
    // 如果内存持续增长超过 20%
    return recentAvg > olderAvg * 1.2;
  }
}
 
// 2. 使用 Chrome DevTools Memory 面板
// - 录制堆快照
// - 对比多个快照
// - 查找 Detached DOM 节点

Q4: 移动端性能优化建议?

优化策略:

javascript
// 1. 检测设备性能
function getDevicePerformance() {
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  
  if (!gl) return 'low';
  
  const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
  const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
  
  // 检测是否为低端设备
  if (renderer.includes('Intel')) return 'medium';
  if (renderer.includes('NVIDIA') || renderer.includes('AMD')) return 'high';
  
  return 'medium';
}
 
// 2. 根据设备调整参数
const devicePerformance = getDevicePerformance();
 
const config = {
  high: {
    pixelRatio: Math.min(window.devicePixelRatio, 2),
    shadows: true,
    antialias: true,
    maxTriangles: 2000000
  },
  medium: {
    pixelRatio: Math.min(window.devicePixelRatio, 1.5),
    shadows: true,
    antialias: false,
    maxTriangles: 500000
  },
  low: {
    pixelRatio: 1,
    shadows: false,
    antialias: false,
    maxTriangles: 100000
  }
}[devicePerformance];
 
// 应用配置
renderer.setPixelRatio(config.pixelRatio);
renderer.shadowMap.enabled = config.shadows;

Q5: 如何平衡画质和性能?

动态降级策略:

javascript
class PerformanceManager {
  constructor() {
    this.targetFPS = 60;
    this.currentQuality = 'high';
    this.qualityLevels = ['low', 'medium', 'high'];
    this.fpsHistory = [];
  }
  
  update(fps) {
    this.fpsHistory.push(fps);
    if (this.fpsHistory.length > 60) {
      this.fpsHistory.shift();
    }
    
    const avgFPS = this.fpsHistory.reduce((a, b) => a + b) / this.fpsHistory.length;
    
    // 自动降级
    if (avgFPS < this.targetFPS * 0.8) {
      this.degradeQuality();
    }
    // 自动升级
    else if (avgFPS > this.targetFPS * 0.95) {
      this.upgradeQuality();
    }
  }
  
  degradeQuality() {
    const currentIndex = this.qualityLevels.indexOf(this.currentQuality);
    if (currentIndex > 0) {
      this.currentQuality = this.qualityLevels[currentIndex - 1];
      this.applyQuality(this.currentQuality);
      console.log('质量降级:', this.currentQuality);
    }
  }
  
  upgradeQuality() {
    const currentIndex = this.qualityLevels.indexOf(this.currentQuality);
    if (currentIndex < this.qualityLevels.length - 1) {
      this.currentQuality = this.qualityLevels[currentIndex + 1];
      this.applyQuality(this.currentQuality);
      console.log('质量升级:', this.currentQuality);
    }
  }
  
  applyQuality(level) {
    switch (level) {
      case 'low':
        renderer.setPixelRatio(1);
        renderer.shadowMap.enabled = false;
        break;
      case 'medium':
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
        renderer.shadowMap.enabled = true;
        break;
      case 'high':
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.shadowMap.enabled = true;
        break;
    }
  }
}

相关链接