<NME> frontend.ts
<BEF> /**
 * @license
 * Copyright 2016 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ChunkState} from 'neuroglancer/chunk_manager/base';
import {Chunk, ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend';
import {VisibleLayerInfo} from 'neuroglancer/layer';
import {EncodedMeshData, FRAGMENT_SOURCE_RPC_ID, MESH_LAYER_RPC_ID, MULTISCALE_FRAGMENT_SOURCE_RPC_ID, MULTISCALE_MESH_LAYER_RPC_ID, MultiscaleFragmentFormat, VertexPositionFormat} from 'neuroglancer/mesh/base';
import {getMultiscaleChunksToDraw, getMultiscaleFragmentKey, MultiscaleMeshManifest, validateOctree} from 'neuroglancer/mesh/multiscale';
import {PerspectivePanel} from 'neuroglancer/perspective_view/panel';
import {PerspectiveViewReadyRenderContext, PerspectiveViewRenderContext, PerspectiveViewRenderLayer} from 'neuroglancer/perspective_view/render_layer';
import {ThreeDimensionalRenderLayerAttachmentState, update3dRenderLayerAttachment} from 'neuroglancer/renderlayer';
import {forEachVisibleSegment, getObjectKey} from 'neuroglancer/segmentation_display_state/base';
import {forEachVisibleSegmentToDraw, registerRedrawWhenSegmentationDisplayState3DChanged, SegmentationDisplayState3D, SegmentationLayerSharedObject} from 'neuroglancer/segmentation_display_state/frontend';
import {makeCachedDerivedWatchableValue, WatchableValueInterface} from 'neuroglancer/trackable_value';
import {Borrowed, RefCounted} from 'neuroglancer/util/disposable';
import {getFrustrumPlanes, mat3, mat3FromMat4, mat4, scaleMat3Output, vec3, vec4} from 'neuroglancer/util/geom';
import * as matrix from 'neuroglancer/util/matrix';
import {Uint64} from 'neuroglancer/util/uint64';
import {Buffer} from 'neuroglancer/webgl/buffer';

const DEBUG_MULTISCALE_FRAGMENTS = false;

export class MeshShaderManager {
  private tempLightVec = new Float32Array(4);
  constructor() {}

  defineShader(builder: ShaderBuilder) {
    builder.addAttribute('highp vec3', 'aVertexPosition');
    builder.addAttribute('highp vec3', 'aVertexNormal');
    builder.addVarying('highp vec4', 'vColor');
    builder.addUniform('highp vec4', 'uLightDirection');
    builder.addUniform('highp vec4', 'uColor');
    builder.addUniform('highp mat3', 'uNormalMatrix');
    builder.addUniform('highp mat4', 'uModelViewProjection');
    builder.addUniform('highp uint', 'uPickID');
    builder.setVertexMain(`
gl_Position = uModelViewProjection * vec4(aVertexPosition, 1.0);
vec3 normal = uNormalMatrix * aVertexNormal;
float lightingFactor = abs(dot(normal, uLightDirection.xyz)) + uLightDirection.w;
vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
`);
  if (v.z < 0.0) v.xy = (1.0 - abs(v.yx)) * vec2(v.x > 0.0 ? 1.0 : -1.0, v.y > 0.0 ? 1.0 : -1.0);
  return normalize(v);
}
`;

function decodeSnorm8(x: number) {
  if (x >= 128) {
    x = x - 256;
  }
  return Math.max(-1, x / 127);
}

/**
 * Javascript implementation of normal decoding, for debugging.
 */
export function decodeNormalOctahedronSnorm8(normals: Uint8Array) {
  let n = normals.length;
  n -= 1;
  let out = new Float32Array(Math.floor(normals.length / 2) * 3);
  let outIndex = 0;
  for (let i = 0; i < n; i += 2) {
    const e0 = decodeSnorm8(normals[i]);
    const e1 = decodeSnorm8(normals[i + 1]);
    let v0 = e0;
    let v1 = e1;
    let v2 = 1.0 - Math.abs(e0) - Math.abs(e1);
    if (v2 < 0) {
      v0 = (1 - Math.abs(v1)) * (v0 > 0 ? 1 : -1);
      v1 = (1 - Math.abs(v0)) * (v1 > 0 ? 1 : -1);
    }
    const len = Math.sqrt(v0 ** 2 + v1 ** 2 + v2 ** 2);
    out[outIndex] = v0 / len;
    out[outIndex + 1] = v1 / len;
    out[outIndex + 2] = v2 / len;
    outIndex += 3;
  }
  return out;
}

interface VertexPositionFormatHandler {
  defineShader: (builder: ShaderBuilder) => void;
  bind:
      (gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) => void;
  endLayer: (gl: GL, shader: ShaderProgram) => void;

    fragmentChunk.normalBuffer.bindToVertexAttrib(
        shader.attribute('aVertexNormal'),
        /*components=*/ 3);
    fragmentChunk.indexBuffer.bind();
    gl.drawElements(gl.TRIANGLES, fragmentChunk.numIndices, gl.UNSIGNED_INT, 0);
  }
    },
    bind(_gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) {
      fragmentChunk.vertexBuffer.bindToVertexAttrib(
          shader.attribute('aVertexPosition'),
          /*components=*/ 3, glAttributeType, /* normalized=*/ true);
    },
    endLayer: (gl: GL, shader: ShaderProgram) => {

    fragmentChunk.normalBuffer.bindToVertexAttrib(
        shader.attribute('aVertexNormal'),
        /*components=*/ 3);
    fragmentChunk.indexBuffer.bind();
    const indexBegin = fragmentChunk.subChunkOffsets[subChunkBegin];
    const indexEnd = fragmentChunk.subChunkOffsets[subChunkEnd];
  [VertexPositionFormat.uint16]: getFloatPositionHandler(WebGL2RenderingContext.UNSIGNED_SHORT),
  [VertexPositionFormat.uint10]: {
    defineShader: (builder: ShaderBuilder) => {
      builder.addAttribute('highp uint', 'aVertexPosition');
      builder.addVertexCode(`
highp vec3 getVertexPosition() {
  return vec3(float(aVertexPosition & 1023u),
              float((aVertexPosition >> 10) & 1023u),
              float((aVertexPosition >> 20) & 1023u)) / 1023.0;
}
`);
    },
    bind(_gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) {
      fragmentChunk.vertexBuffer.bindToVertexAttribI(
          shader.attribute('aVertexPosition'),
          /*components=*/ 1, WebGL2RenderingContext.UNSIGNED_INT);
    },
    endLayer: (gl: GL, shader: ShaderProgram) => {
      gl.disableVertexAttribArray(shader.attribute('aVertexPosition'));
    }
  },
};

export class MeshShaderManager {
  private tempLightVec = new Float32Array(4);
  private vertexPositionHandler = vertexPositionHandlers[this.vertexPositionFormat];
  constructor(
      public fragmentRelativeVertices: boolean, public vertexPositionFormat: VertexPositionFormat) {
  }

  beginLayer(
      gl: GL, shader: ShaderProgram, renderContext: PerspectiveViewRenderContext,
      displayState: MeshDisplayState) {
    let {lightDirection, ambientLighting, directionalLighting} = renderContext;
    let lightVec = <vec3>this.tempLightVec;
    vec3.scale(lightVec, lightDirection, directionalLighting);
    lightVec[3] = ambientLighting;
    gl.uniform4fv(shader.uniform('uLightDirection'), lightVec);
    const silhouetteRendering = displayState.silhouetteRendering.value;
    if (silhouetteRendering > 0) {
      gl.uniform1f(shader.uniform('uSilhouettePower'), silhouetteRendering);
    }
  }

  setColor(gl: GL, shader: ShaderProgram, color: vec4) {
    gl.uniform4fv(shader.uniform('uColor'), color);
  }

  setPickID(gl: GL, shader: ShaderProgram, pickID: number) {
    gl.uniform1ui(shader.uniform('uPickID'), pickID);
  }

  beginModel(
      gl: GL, shader: ShaderProgram, renderContext: PerspectiveViewRenderContext, modelMat: mat4) {
    const {projectionParameters} = renderContext;
    gl.uniformMatrix4fv(
        shader.uniform('uModelViewProjection'), false,
        mat4.multiply(tempMat4, projectionParameters.viewProjectionMat, modelMat));
    mat3FromMat4(tempMat3, modelMat);
    scaleMat3Output(
        tempMat3, tempMat3, projectionParameters.displayDimensionRenderInfo.canonicalVoxelFactors);
    mat3.invert(tempMat3, tempMat3);
    mat3.transpose(tempMat3, tempMat3);
    gl.uniformMatrix3fv(shader.uniform('uNormalMatrix'), false, tempMat3);
  }

  drawFragmentHelper(
      gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk,
      indexBegin: number, indexEnd: number) {
    this.vertexPositionHandler.bind(gl, shader, fragmentChunk);
    const {meshData} = fragmentChunk;
    fragmentChunk.normalBuffer.bindToVertexAttrib(
        shader.attribute('aVertexNormal'),
        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);
    fragmentChunk.indexBuffer.bind();
    const {indices} = meshData;
    gl.drawElements(
        meshData.strips ? WebGL2RenderingContext.TRIANGLE_STRIP : WebGL2RenderingContext.TRIANGLES,
        indexEnd - indexBegin,
        indices.BYTES_PER_ELEMENT === 2 ? WebGL2RenderingContext.UNSIGNED_SHORT :
                                          WebGL2RenderingContext.UNSIGNED_INT,
        indexBegin * indices.BYTES_PER_ELEMENT);
  }

  drawFragment(gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk) {
    const {meshData} = fragmentChunk;
    const {indices} = meshData;
    this.drawFragmentHelper(gl, shader, fragmentChunk, 0, indices.length);
  }

  drawMultiscaleFragment(
      gl: GL,
      shader: ShaderProgram,
      fragmentChunk: MultiscaleFragmentChunk,
      subChunkBegin: number,
      subChunkEnd: number,
  ) {
    const indexBegin = fragmentChunk.meshData.subChunkOffsets[subChunkBegin];
    const indexEnd = fragmentChunk.meshData.subChunkOffsets[subChunkEnd];
    this.drawFragmentHelper(gl, shader, fragmentChunk, indexBegin, indexEnd);
  }

  endLayer(gl: GL, shader: ShaderProgram) {
    this.vertexPositionHandler.endLayer(gl, shader);
    gl.disableVertexAttribArray(shader.attribute('aVertexNormal'));
  }

  makeGetter(layer: RefCounted&{gl: GL, displayState: MeshDisplayState}) {
    const silhouetteRenderingEnabled = layer.registerDisposer(
        makeCachedDerivedWatchableValue(x => x > 0, [layer.displayState.silhouetteRendering]));
    return parameterizedEmitterDependentShaderGetter(layer, layer.gl, {
      memoizeKey:
          `mesh/MeshShaderManager/${this.fragmentRelativeVertices}/${this.vertexPositionFormat}`,
      parameters: silhouetteRenderingEnabled,
      defineShader: (builder, silhouetteRenderingEnabled) => {
        this.vertexPositionHandler.defineShader(builder);
        builder.addAttribute('highp vec2', 'aVertexNormal');
        builder.addVarying('highp vec4', 'vColor');
        builder.addUniform('highp vec4', 'uLightDirection');
        builder.addUniform('highp vec4', 'uColor');
        builder.addUniform('highp mat3', 'uNormalMatrix');
        builder.addUniform('highp mat4', 'uModelViewProjection');
        builder.addUniform('highp uint', 'uPickID');
        if (silhouetteRenderingEnabled) {
          builder.addUniform('highp float', 'uSilhouettePower');
        }
        if (this.fragmentRelativeVertices) {
          builder.addUniform('highp vec3', 'uFragmentOrigin');
          builder.addUniform('highp vec3', 'uFragmentShape');
        }
        builder.addVertexCode(glsl_decodeNormalOctahedronSnorm8);
        let vertexMain = ``;
        if (this.fragmentRelativeVertices) {
          vertexMain += `
highp vec3 vertexPosition = uFragmentOrigin + uFragmentShape * getVertexPosition();
highp vec3 normalMultiplier = 1.0 / uFragmentShape;
`;
export class FragmentChunk extends Chunk {
  vertexPositions: Float32Array;
  indices: Uint32Array;
  vertexNormals: Float32Array;
  objectKey: string;
  source: FragmentSource;
  vertexBuffer: Buffer;
gl_Position = uModelViewProjection * vec4(vertexPosition, 1.0);
vec3 origNormal = decodeNormalOctahedronSnorm8(aVertexNormal);
vec3 normal = normalize(uNormalMatrix * (normalMultiplier * origNormal));
float absCosAngle = abs(dot(normal, uLightDirection.xyz));
float lightingFactor = absCosAngle + uLightDirection.w;
vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
`;
        if (silhouetteRenderingEnabled) {
          vertexMain += `
vColor *= pow(1.0 - absCosAngle, uSilhouettePower);
`;
        }
        builder.setVertexMain(vertexMain);
        builder.setFragmentMain(`emit(vColor, uPickID);`);
      },
    });
  }
}

export interface MeshDisplayState extends SegmentationDisplayState3D {
  silhouetteRendering: WatchableValueInterface<number>;
}

export class MeshLayer extends
    PerspectiveViewRenderLayer<ThreeDimensionalRenderLayerAttachmentState> {
  protected meshShaderManager =
      new MeshShaderManager(/*fragmentRelativeVertices=*/ false, VertexPositionFormat.float32);
  private getShader = this.meshShaderManager.makeGetter(this);
  backend: SegmentationLayerSharedObject;

  constructor(
      public chunkManager: ChunkManager, public source: MeshSource,
      public displayState: MeshDisplayState) {
    super();

    registerRedrawWhenSegmentationDisplayState3DChanged(displayState, this);
    this.registerDisposer(displayState.silhouetteRendering.changed.add(this.redrawNeeded.dispatch));

    let sharedObject = this.backend = this.registerDisposer(
        new SegmentationLayerSharedObject(chunkManager, displayState, this.layerChunkProgressInfo));
    sharedObject.RPC_TYPE_ID = MESH_LAYER_RPC_ID;
    sharedObject.initializeCounterpartWithChunkManager({
      'source': source.addCounterpartRef(),
    });
    sharedObject.visibility.add(this.visibility);
    this.registerDisposer(displayState.renderScaleHistogram.visibility.add(this.visibility));
  }

  get isTransparent() {
    const {displayState} = this;
    return displayState.objectAlpha.value < 1.0 || displayState.silhouetteRendering.value > 0;
  }

  get transparentPickEnabled() {
    return this.displayState.transparentPickEnabled.value;
  }

  get gl() {
    return this.chunkManager.chunkQueueManager.gl;
  }

  draw(
      renderContext: PerspectiveViewRenderContext,
      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {
    if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) {
      // No need for a separate pick ID pass.
      return;
    }
    const {gl, displayState, meshShaderManager} = this;
    if (displayState.objectAlpha.value <= 0.0) {
      // Skip drawing.
      return;
    }
    const modelMatrix = update3dRenderLayerAttachment(
        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,
        attachment);
    if (modelMatrix === undefined) {
      return;
    }
    const {shader} = this.getShader(renderContext.emitter);
    if (shader === null) return;
    shader.bind();
    meshShaderManager.beginLayer(gl, shader, renderContext, this.displayState);
    meshShaderManager.beginModel(gl, shader, renderContext, modelMatrix);

    const manifestChunks = this.source.chunks;

    let totalChunks = 0, presentChunks = 0;
    const {renderScaleHistogram} = this.displayState;
    const fragmentChunks = this.source.fragmentSource.chunks;

    forEachVisibleSegmentToDraw(
        displayState, this, renderContext.emitColor,
        renderContext.emitPickID ? renderContext.pickIDs : undefined,
        (objectId, color, pickIndex) => {
          const key = getObjectKey(objectId);
          const manifestChunk = manifestChunks.get(key);
          ++totalChunks;
          if (manifestChunk === undefined) return;
          ++presentChunks;
          if (renderContext.emitColor) {
            meshShaderManager.setColor(gl, shader, color!);
          }
          if (renderContext.emitPickID) {
            meshShaderManager.setPickID(gl, shader, pickIndex!);
          }
          totalChunks += manifestChunk.fragmentIds.length;

          for (const fragmentId of manifestChunk.fragmentIds) {
            const {key: fragmentKey} = this.source.getFragmentKey(key, fragmentId);
            const fragment = fragmentChunks.get(fragmentKey);
            if (fragment !== undefined && fragment.state === ChunkState.GPU_MEMORY) {
              meshShaderManager.drawFragment(gl, shader, fragment);
              ++presentChunks;
            }
          }
        });

    if (renderContext.emitColor) {
      renderScaleHistogram.begin(
          this.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber);
      renderScaleHistogram.add(
          Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, presentChunks,
          totalChunks - presentChunks);
    }
    meshShaderManager.endLayer(gl, shader);
  }

  isReady() {
    const {displayState, source} = this;
    let ready = true;
    const fragmentChunks = source.fragmentSource.chunks;
    forEachVisibleSegment(displayState.segmentationGroupState.value, objectId => {
      const key = getObjectKey(objectId);
      const manifestChunk = source.chunks.get(key);
      if (manifestChunk === undefined) {
        ready = false;
        return;
      }
      for (const fragmentId of manifestChunk.fragmentIds) {
        const {key: fragmentKey} = this.source.getFragmentKey(key, fragmentId);
        const fragmentChunk = fragmentChunks.get(fragmentKey);
        if (fragmentChunk === undefined || fragmentChunk.state !== ChunkState.GPU_MEMORY) {
          ready = false;
          return;
        }
      }
    });
    return ready;
  }
}

export class ManifestChunk extends Chunk {
  fragmentIds: string[];

  constructor(source: MeshSource, x: any) {
    super(source);
    this.fragmentIds = x.fragmentIds;
  }
}

export class FragmentChunk extends Chunk {
  source: FragmentSource;
  vertexBuffer: Buffer;
  indexBuffer: Buffer;
  normalBuffer: Buffer;
  meshData: EncodedMeshData;

  constructor(source: FragmentSource, x: any) {
    super(source);
    this.meshData = x;
  }

  copyToGPU(gl: GL) {
    super.copyToGPU(gl);
    copyMeshDataToGpu(gl, this);
  }

  freeGPUMemory(gl: GL) {
    super.freeGPUMemory(gl);
    freeGpuMeshData(this);
  }
}

export interface MeshSourceOptions {}

export class MeshSource extends ChunkSource {
  fragmentSource = this.registerDisposer(new FragmentSource(this.chunkManager, this));
  chunks: Map<string, ManifestChunk>;

  constructor(chunkManager: Borrowed<ChunkManager>, options: MeshSourceOptions) {
    super(chunkManager, options);
  }

  initializeCounterpart(rpc: RPC, options: any) {
    this.fragmentSource.initializeCounterpart(this.chunkManager.rpc!, {});
    options['fragmentSource'] = this.fragmentSource.addCounterpartRef();
    super.initializeCounterpart(rpc, options);
  }
  getChunk(x: any) {
    return new ManifestChunk(this, x);
  }
  getFragmentKey(objectKey: string, fragmentId: string) {
    return {key:`${objectKey}/${fragmentId}`, fragmentId: fragmentId}
  }
}

@registerSharedObjectOwner(FRAGMENT_SOURCE_RPC_ID)
export class FragmentSource extends ChunkSource {
  chunks: Map<string, FragmentChunk>;
  get key() {
    return this.meshSource.key;
  }
  constructor(chunkManager: ChunkManager, public meshSource: MeshSource) {
    super(chunkManager);
  }
  getChunk(x: any) {
    return new FragmentChunk(this, x);
  }
}


function hasFragmentChunk(
    fragmentChunks: Map<string, MultiscaleFragmentChunk>, objectKey: string, lod: number,
    chunkIndex: number) {
  const fragmentChunk = fragmentChunks.get(getMultiscaleFragmentKey(objectKey, lod, chunkIndex));
  return fragmentChunk !== undefined && fragmentChunk.state === ChunkState.GPU_MEMORY;
}

export class MultiscaleMeshLayer extends
    PerspectiveViewRenderLayer<ThreeDimensionalRenderLayerAttachmentState> {
  protected meshShaderManager = new MeshShaderManager(
      /*fragmentRelativeVertices=*/ this.source.format.fragmentRelativeVertices,
      this.source.format.vertexPositionFormat);
  private getShader = this.meshShaderManager.makeGetter(this);
  backend: SegmentationLayerSharedObject;

  constructor(
      public chunkManager: ChunkManager, public source: MultiscaleMeshSource,
  subChunkOffsets: Uint32Array;
  vertexPositions: Float32Array;
  indices: Uint32Array;
  vertexNormals: Float32Array;
  source: MultiscaleFragmentSource;
  vertexBuffer: Buffer;
  indexBuffer: Buffer;
        new SegmentationLayerSharedObject(chunkManager, displayState, this.layerChunkProgressInfo));
    sharedObject.RPC_TYPE_ID = MULTISCALE_MESH_LAYER_RPC_ID;
    sharedObject.initializeCounterpartWithChunkManager({
      'source': source.addCounterpartRef(),
    });
    sharedObject.visibility.add(this.visibility);
    this.registerDisposer(displayState.renderScaleHistogram.visibility.add(this.visibility));
  }

  get isTransparent() {
    const {displayState} = this;
    return displayState.objectAlpha.value < 1.0 || displayState.silhouetteRendering.value > 0;
  }

  get transparentPickEnabled() {
    return this.displayState.transparentPickEnabled.value;
  }

  get gl() {
    return this.chunkManager.chunkQueueManager.gl;
  }

  draw(
      renderContext: PerspectiveViewRenderContext,
      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {
    if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) {
      // No need for a separate pick ID pass.
      return;
    }
    const {gl, displayState, meshShaderManager} = this;
    if (displayState.objectAlpha.value <= 0.0) {
      // Skip drawing.
      return;
    }
    const modelMatrix = update3dRenderLayerAttachment(
        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,
        attachment);
    if (modelMatrix === undefined) return;
    const {shader} = this.getShader(renderContext.emitter);
    if (shader === null) return;
    shader.bind();
    meshShaderManager.beginLayer(gl, shader, renderContext, this.displayState);

    const {renderScaleHistogram} = this.displayState;
    if (renderContext.emitColor) {
      renderScaleHistogram.begin(
          this.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber);
    }

    mat3FromMat4(tempMat3, modelMatrix);
    scaleMat3Output(
        tempMat3, tempMat3,
        renderContext.projectionParameters.displayDimensionRenderInfo.voxelPhysicalScales);
    const scaleMultiplier = Math.pow(Math.abs(mat3.determinant(tempMat3)), 1 / 3);

    const {chunks} = this.source;
    const fragmentChunks = this.source.fragmentSource.chunks;

    const {projectionParameters} = renderContext;

    const modelViewProjection =
        mat4.multiply(mat4.create(), projectionParameters.viewProjectionMat, modelMatrix);

    const clippingPlanes = getFrustrumPlanes(new Float32Array(24), modelViewProjection);

    const detailCutoff = this.displayState.renderScaleTarget.value;
    const {fragmentRelativeVertices} = this.source.format;

    meshShaderManager.beginModel(gl, shader, renderContext, modelMatrix);

    let totalManifestChunks = 0;
    let presentManifestChunks = 0;

    forEachVisibleSegmentToDraw(
        displayState, this, renderContext.emitColor,
        renderContext.emitPickID ? renderContext.pickIDs : undefined,
        (objectId, color, pickIndex) => {
          const key = getObjectKey(objectId);
          const manifestChunk = chunks.get(key);
          ++totalManifestChunks;
          if (manifestChunk === undefined) return;
          ++presentManifestChunks;
          const {manifest} = manifestChunk;
          const {octree, chunkShape, chunkGridSpatialOrigin, vertexOffsets} = manifest;
          if (DEBUG_MULTISCALE_FRAGMENTS) {
            try {
              validateOctree(octree);
            } catch (e) {
              console.log(`invalid octree for object=${objectId}: ${e.message}`)
            }
          }
          if (renderContext.emitColor) {
            meshShaderManager.setColor(gl, shader, color!);
          }
          if (renderContext.emitPickID) {
            meshShaderManager.setPickID(gl, shader, pickIndex!);
          }
          getMultiscaleChunksToDraw(
              manifest, modelViewProjection, clippingPlanes, detailCutoff,
              projectionParameters.width, projectionParameters.height,
              (lod, chunkIndex, renderScale) => {
                const has = hasFragmentChunk(fragmentChunks, key, lod, chunkIndex);
                if (renderContext.emitColor) {
                  renderScaleHistogram.add(
                      manifest.lodScales[lod] * scaleMultiplier, renderScale, has ? 1 : 0,
                      has ? 0 : 1);
                }
                return has;
              },
              (lod, chunkIndex, subChunkBegin, subChunkEnd) => {
                const fragmentKey = getMultiscaleFragmentKey(key, lod, chunkIndex);
                const fragmentChunk = fragmentChunks.get(fragmentKey)!;
                const x = octree[5 * chunkIndex], y = octree[5 * chunkIndex + 1],
                      z = octree[5 * chunkIndex + 2];
                const scale = 1 << lod;
                if (fragmentRelativeVertices) {
                  gl.uniform3f(
                      shader.uniform('uFragmentOrigin'),
                      chunkGridSpatialOrigin[0] + (x * chunkShape[0]) * scale +
                          vertexOffsets[lod * 3 + 0],
                      chunkGridSpatialOrigin[1] + (y * chunkShape[1]) * scale +
                          vertexOffsets[lod * 3 + 1],
                      chunkGridSpatialOrigin[2] + (z * chunkShape[2]) * scale +
                          vertexOffsets[lod * 3 + 2]);
                  gl.uniform3f(
                      shader.uniform('uFragmentShape'), chunkShape[0] * scale,
                      chunkShape[1] * scale, chunkShape[2] * scale);
                }
                if (DEBUG_MULTISCALE_FRAGMENTS) {
                  const message = `lod=${lod}, chunkIndex=${chunkIndex}, subChunkBegin=${
                      subChunkBegin}, subChunkEnd=${subChunkEnd}, uFragmentOrigin=${
                          [chunkGridSpatialOrigin[0] + (x * chunkShape[0]) * scale +
                               vertexOffsets[lod * 3 + 0],
                           chunkGridSpatialOrigin[1] + (y * chunkShape[1]) * scale +
                               vertexOffsets[lod * 3 + 1],
                           chunkGridSpatialOrigin[2] + (z * chunkShape[2]) * scale +
                               vertexOffsets[lod * 3 + 2]]}, uFragmentShape=${
                          [chunkShape[0] * scale, chunkShape[1] * scale, chunkShape[2] * scale]}`;
                  const pickIndex =
                      renderContext.pickIDs.registerUint64(this, objectId, 1, message);
                  if (renderContext.emitPickID) {
                    meshShaderManager.setPickID(gl, shader, pickIndex!);
                  }
                }
                meshShaderManager.drawMultiscaleFragment(
                    gl,
                    shader,
                    fragmentChunk,
                    subChunkBegin,
                    subChunkEnd,
                );
              });
        });
    renderScaleHistogram.add(
        Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, presentManifestChunks,
        totalManifestChunks - presentManifestChunks);
    meshShaderManager.endLayer(gl, shader);
  }

  isReady(
      renderContext: PerspectiveViewReadyRenderContext,
      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {
    let {displayState} = this;
    if (displayState.objectAlpha.value <= 0.0) {
      // Skip drawing.
      return true;
    }
    const modelMatrix = update3dRenderLayerAttachment(
        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,
        attachment);
    if (modelMatrix === undefined) return false;
    const {chunks} = this.source;
    const fragmentChunks = this.source.fragmentSource.chunks;

    const {projectionParameters} = renderContext;
    const modelViewProjection =
        mat4.multiply(mat4.create(), projectionParameters.viewProjectionMat, modelMatrix);

    const clippingPlanes = getFrustrumPlanes(new Float32Array(24), modelViewProjection);

    const detailCutoff = this.displayState.renderScaleTarget.value;

    let hasAllChunks = true;

    forEachVisibleSegment(displayState.segmentationGroupState.value, (objectId) => {
      if (!hasAllChunks) return;
      const key = getObjectKey(objectId);
      const manifestChunk = chunks.get(key);
      if (manifestChunk === undefined) {
        hasAllChunks = false;
        return;
      }
      const {manifest} = manifestChunk;
      getMultiscaleChunksToDraw(
          manifest, modelViewProjection, clippingPlanes, detailCutoff, projectionParameters.width,
          projectionParameters.height, (lod, chunkIndex) => {
            hasAllChunks = hasAllChunks && hasFragmentChunk(fragmentChunks, key, lod, chunkIndex);
            return hasAllChunks;
          }, () => {});
    });
    return hasAllChunks;
  }

  getObjectPosition(id: Uint64): Float32Array|undefined {
    const transform = this.displayState.transform.value;
    if (transform.error !== undefined) return undefined;
    const chunk = this.source.chunks.get(getObjectKey(id));
    if (chunk === undefined) return undefined;
    const {manifest} = chunk;
    const {clipLowerBound, clipUpperBound} = manifest;
    const {rank} = transform;
    // Center position, in model coordinates.
    const modelCenter = new Float32Array(rank);
    for (let i = 0; i < 3; ++i) {
      modelCenter[i] = (clipLowerBound[i] + clipUpperBound[i]) / 2;
    }
    const layerCenter = new Float32Array(rank);
    matrix.transformPoint(
      layerCenter, transform.modelToRenderLayerTransform, rank + 1, modelCenter, rank);
    return layerCenter;
  }
}

export class MultiscaleManifestChunk extends Chunk {
  manifest: MultiscaleMeshManifest;
  source: MultiscaleMeshSource;

  constructor(source: MultiscaleMeshSource, x: any) {
    super(source);
    this.manifest = x['manifest'];
  }
}

export class MultiscaleFragmentChunk extends Chunk {
  meshData: EncodedMeshData&{subChunkOffsets: Uint32Array};
  source: MultiscaleFragmentSource;
  vertexBuffer: Buffer;
  indexBuffer: Buffer;
  normalBuffer: Buffer;

  constructor(source: MultiscaleFragmentSource, x: any) {
    super(source);
    this.meshData = x;
  }

  copyToGPU(gl: GL) {
    super.copyToGPU(gl);
    copyMeshDataToGpu(gl, this);
  }

  freeGPUMemory(gl: GL) {
    super.freeGPUMemory(gl);
    freeGpuMeshData(this);
  }
}

export interface MultiscaleMeshSourceOptions extends MeshSourceOptions {
  format: MultiscaleFragmentFormat;
}

export class MultiscaleMeshSource extends ChunkSource {
  OPTIONS: MultiscaleMeshSourceOptions;
  fragmentSource = this.registerDisposer(new MultiscaleFragmentSource(this.chunkManager, this));
  chunks: Map<string, MultiscaleManifestChunk>;
  format: MultiscaleFragmentFormat;
  constructor(chunkManager: Borrowed<ChunkManager>, options: MultiscaleMeshSourceOptions) {
    super(chunkManager, options);
    this.format = options.format;
  }

  static encodeOptions(options: MultiscaleMeshSourceOptions) {
    return {format: options.format, ...super.encodeOptions(options)};
  }

  initializeCounterpart(rpc: RPC, options: any) {
    this.fragmentSource.initializeCounterpart(this.chunkManager.rpc!, {});
    options['fragmentSource'] = this.fragmentSource.addCounterpartRef();
    options['format'] = this.format;
    super.initializeCounterpart(rpc, options);
  }
  getChunk(x: any) {
    return new MultiscaleManifestChunk(this, x);
  }
}

@registerSharedObjectOwner(MULTISCALE_FRAGMENT_SOURCE_RPC_ID)
export class MultiscaleFragmentSource extends ChunkSource {
  chunks: Map<string, MultiscaleFragmentChunk>;
  get key() {
    return this.meshSource.key;
  }
  constructor(chunkManager: ChunkManager, public meshSource: MultiscaleMeshSource) {
    super(chunkManager);
  }
  getChunk(x: any) {
    return new MultiscaleFragmentChunk(this, x);
  }
}

<MSG> feat(mesh): encode normal vectors using 2xSnorm8 octahedron encoding

This reduces memory usage of normal vectors from 12 bytes to 2 bytes per vertex.

<DFF> @@ -33,22 +33,40 @@ const tempMat3 = mat3.create();
 
 const DEBUG_MULTISCALE_FRAGMENTS = false;
 
+/**
+ * Decodes normal vectors in 2xSnorm8 octahedron encoding into normalized 3x32f vector.
+ *
+ * Zina H. Cigolle, Sam Donow, Daniel Evangelakos, Michael Mara, Morgan McGuire, and Quirin Meyer,
+ * Survey of Efficient Representations for Independent Unit Vectors, Journal of Computer Graphics
+ * Techniques (JCGT), vol. 3, no. 2, 1-30, 2014
+ *
+ * Available online http://jcgt.org/published/0003/02/01/
+ */
+const glsl_decodeNormalOctahedronSnorm8 = `
+highp vec3 decodeNormalOctahedronSnorm8(highp vec2 e) {
+  vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));
+  if (v.z < 0.0) v.xy = (1.0 - abs(v.yx)) * vec2(v.x > 0.0 ? 1.0 : -1.0, v.y > 0.0 ? 1.0 : -1.0);
+  return normalize(v);
+}
+`;
+
 export class MeshShaderManager {
   private tempLightVec = new Float32Array(4);
   constructor() {}
 
   defineShader(builder: ShaderBuilder) {
     builder.addAttribute('highp vec3', 'aVertexPosition');
-    builder.addAttribute('highp vec3', 'aVertexNormal');
+    builder.addAttribute('highp vec2', 'aVertexNormal');
     builder.addVarying('highp vec4', 'vColor');
     builder.addUniform('highp vec4', 'uLightDirection');
     builder.addUniform('highp vec4', 'uColor');
     builder.addUniform('highp mat3', 'uNormalMatrix');
     builder.addUniform('highp mat4', 'uModelViewProjection');
     builder.addUniform('highp uint', 'uPickID');
+    builder.addVertexCode(glsl_decodeNormalOctahedronSnorm8);
     builder.setVertexMain(`
 gl_Position = uModelViewProjection * vec4(aVertexPosition, 1.0);
-vec3 normal = uNormalMatrix * aVertexNormal;
+vec3 normal = uNormalMatrix * decodeNormalOctahedronSnorm8(aVertexNormal);
 float lightingFactor = abs(dot(normal, uLightDirection.xyz)) + uLightDirection.w;
 vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
 `);
@@ -99,7 +117,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
 
     fragmentChunk.normalBuffer.bindToVertexAttrib(
         shader.attribute('aVertexNormal'),
-        /*components=*/ 3);
+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);
     fragmentChunk.indexBuffer.bind();
     gl.drawElements(gl.TRIANGLES, fragmentChunk.numIndices, gl.UNSIGNED_INT, 0);
   }
@@ -113,7 +131,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
 
     fragmentChunk.normalBuffer.bindToVertexAttrib(
         shader.attribute('aVertexNormal'),
-        /*components=*/ 3);
+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);
     fragmentChunk.indexBuffer.bind();
     const indexBegin = fragmentChunk.subChunkOffsets[subChunkBegin];
     const indexEnd = fragmentChunk.subChunkOffsets[subChunkEnd];
@@ -257,7 +275,7 @@ export class ManifestChunk extends Chunk {
 export class FragmentChunk extends Chunk {
   vertexPositions: Float32Array;
   indices: Uint32Array;
-  vertexNormals: Float32Array;
+  vertexNormals: Uint8Array;
   objectKey: string;
   source: FragmentSource;
   vertexBuffer: Buffer;
@@ -503,7 +521,7 @@ export class MultiscaleFragmentChunk extends Chunk {
   subChunkOffsets: Uint32Array;
   vertexPositions: Float32Array;
   indices: Uint32Array;
-  vertexNormals: Float32Array;
+  vertexNormals: Uint8Array;
   source: MultiscaleFragmentSource;
   vertexBuffer: Buffer;
   indexBuffer: Buffer;

{'commit': "diff --git a/src/neuroglancer/datasource/brainmaps/backend.ts b/src/neuroglancer/datasource/brainmaps/backend.ts\nindex 2d838ee..fc12c20 100644\n--- a/src/neuroglancer/datasource/brainmaps/backend.ts\n+++ b/src/neuroglancer/datasource/brainmaps/backend.ts\n@@ -21,7 +21,7 @@ import {ChunkSourceParametersConstructor} from 'neuroglancer/chunk_manager/base'\n import {WithSharedCredentialsProviderCounterpart} from 'neuroglancer/credentials_provider/shared_counterpart';\n import {BatchMeshFragment, BatchMeshFragmentPayload, ChangeStackAwarePayload, Credentials, makeRequest, SkeletonPayload, SubvolumePayload} from 'neuroglancer/datasource/brainmaps/api';\n import {AnnotationSourceParameters, ChangeSpec, MeshSourceParameters, MultiscaleMeshSourceParameters, SkeletonSourceParameters, VolumeChunkEncoding, VolumeSourceParameters} from 'neuroglancer/datasource/brainmaps/base';\n-import {computeVertexNormals, FragmentChunk, ManifestChunk, MeshSource, MultiscaleFragmentChunk, MultiscaleManifestChunk, MultiscaleMeshSource} from 'neuroglancer/mesh/backend';\n+import {computeVertexNormals, encodeNormals32fx3ToOctahedron8x2, FragmentChunk, ManifestChunk, MeshSource, MultiscaleFragmentChunk, MultiscaleManifestChunk, MultiscaleMeshSource} from 'neuroglancer/mesh/backend';\n import {MultiscaleMeshManifest} from 'neuroglancer/mesh/multiscale';\n import {decodeSkeletonVertexPositionsAndIndices, SkeletonChunk, SkeletonSource} from 'neuroglancer/skeleton/backend';\n import {decodeCompressedSegmentationChunk} from 'neuroglancer/sliceview/backend_chunk_decoders/compressed_segmentation';\n@@ -288,143 +288,145 @@ const maxMeshBatchSize = 100;\n \n     let totalVertices = 0;\n     let totalIndices = 0;\n-    let fragments: {\n-      buffer: ArrayBuffer,\n-      verticesOffset: number,\n-      indicesOffset: number,\n-      numVertices: number,\n-      numIndices: number\n+    let fragments: {buffer: ArrayBuffer,\n+                    verticesOffset: number,\n+                    indicesOffset: number,\n+                    numVertices: number,\n+                    numIndices: number\n       chunkIndex: number,\n     }[] = [];\n \n-    idArray.sort((a, b) => a[0].localeCompare(b[0]));\n-    const ids = new Map(idArray);\n-\n-    function copyMeshData() {\n-      const vertexBuffer = new Float32Array(totalVertices * 3);\n-      const indexBuffer = new Uint32Array(totalIndices);\n-      let vertexOffset = 0;\n-      let indexOffset = 0;\n-      fragments.sort((a, b) => a.chunkIndex - b.chunkIndex);\n-      const subChunkOffsets = new Uint32Array(endChunkIndex - startChunkIndex + 1);\n-      for (const fragment of fragments) {\n-        vertexBuffer.set(\n-            new Float32Array(fragment.buffer, fragment.verticesOffset, fragment.numVertices * 3),\n-            vertexOffset * 3);\n-        const {numIndices} = fragment;\n-        const sourceIndices = new Uint32Array(fragment.buffer, fragment.indicesOffset, numIndices);\n-        convertEndian32(sourceIndices, Endianness.LITTLE);\n-        for (let i = 0; i < numIndices; ++i) {\n-          indexBuffer[indexOffset++] = sourceIndices[i] + vertexOffset;\n+      idArray.sort((a, b) => a[0].localeCompare(b[0]));\n+      const ids = new Map(idArray);\n+\n+      function copyMeshData() {\n+        const vertexBuffer = new Float32Array(totalVertices * 3);\n+        const indexBuffer = new Uint32Array(totalIndices);\n+        let vertexOffset = 0;\n+        let indexOffset = 0;\n+        fragments.sort((a, b) => a.chunkIndex - b.chunkIndex);\n+        const subChunkOffsets = new Uint32Array(endChunkIndex - startChunkIndex + 1);\n+        for (const fragment of fragments) {\n+          vertexBuffer.set(\n+              new Float32Array(fragment.buffer, fragment.verticesOffset, fragment.numVertices * 3),\n+              vertexOffset * 3);\n+          const {numIndices} = fragment;\n+          const sourceIndices =\n+              new Uint32Array(fragment.buffer, fragment.indicesOffset, numIndices);\n+          convertEndian32(sourceIndices, Endianness.LITTLE);\n+          for (let i = 0; i < numIndices; ++i) {\n+            indexBuffer[indexOffset++] = sourceIndices[i] + vertexOffset;\n+          }\n+          vertexOffset += fragment.numVertices;\n+          subChunkOffsets[fragment.chunkIndex - startChunkIndex + 1] = indexOffset;\n         }\n-        vertexOffset += fragment.numVertices;\n-        subChunkOffsets[fragment.chunkIndex - startChunkIndex + 1] = indexOffset;\n+        convertEndian32(vertexBuffer, Endianness.LITTLE);\n+        chunk.vertexPositions = vertexBuffer;\n+        chunk.subChunkOffsets = subChunkOffsets;\n+        chunk.indices = indexBuffer;\n+        const normals = computeVertexNormals(chunk.vertexPositions!, chunk.indices!);\n+        const encodedNormals = chunk.vertexNormals = new Uint8Array(normals.length / 3 * 2);\n+        encodeNormals32fx3ToOctahedron8x2(encodedNormals, normals);\n       }\n-      convertEndian32(vertexBuffer, Endianness.LITTLE);\n-      chunk.vertexPositions = vertexBuffer;\n-      chunk.subChunkOffsets = subChunkOffsets;\n-      chunk.indices = indexBuffer;\n-      chunk.vertexNormals = computeVertexNormals(vertexBuffer, indexBuffer);\n-    }\n-    function decodeResponse(response: ArrayBuffer): Promise<void>|void {\n-      let length = response.byteLength;\n-      let index = 0;\n-      const dataView = new DataView(response);\n-      const headerSize =\n-          /*object id*/ 8 + /*fragment key length*/ 8 + /*num vertices*/ 8 + /*num triangles*/ 8;\n-      while (index < length) {\n-        if (index + headerSize > length) {\n-          throw new Error(`Invalid batch mesh fragment response.`);\n-        }\n-        const objectIdLow = dataView.getUint32(index, /*littleEndian=*/ true);\n-        const objectIdHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n-        const objectIdString = new Uint64(objectIdLow, objectIdHigh).toString();\n-        const prefix = objectIdString + '\\0';\n-        index += 8;\n-        const fragmentKeyLength = dataView.getUint32(index, /*littleEndian=*/true);\n-        const fragmentKeyLengthHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n-        index += 8;\n-        if (fragmentKeyLengthHigh !== 0) {\n-          throw new Error(`Invalid batch mesh fragment response.`);\n-        }\n-        if (index + fragmentKeyLength + /* num vertices */ 8 + /*num indices*/ 8 > length) {\n-          throw new Error(`Invalid batch mesh fragment response.`);\n-        }\n-        const fragmentKey =\n-            new TextDecoder().decode(new Uint8Array(response, index, fragmentKeyLength));\n-        const fullKey = prefix + fragmentKey;\n-        const chunkIndex = ids.get(fullKey)!;\n-        if (!ids.delete(fullKey)) {\n-          throw new Error(`Received unexpected fragment key: ${JSON.stringify(fullKey)}.`);\n-        }\n-        index += fragmentKeyLength;\n-        const numVertices = dataView.getUint32(index, /*littleEndian=*/true);\n-        const numVerticesHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n-        index += 8;\n-        const numTriangles = dataView.getUint32(index, /*littleEndian=*/true);\n-        const numTrianglesHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n-        index += 8;\n-        if (numVerticesHigh !== 0 || numTrianglesHigh !== 0) {\n-          throw new Error(`Invalid batch mesh fragment response.`);\n-        }\n-        const endOffset = index + numTriangles * 12 + numVertices * 12;\n-        if (endOffset > length) {\n-          throw new Error(`Invalid batch mesh fragment response.`);\n+      function decodeResponse(response: ArrayBuffer): Promise<void>|void {\n+        let length = response.byteLength;\n+        let index = 0;\n+        const dataView = new DataView(response);\n+        const headerSize =\n+            /*object id*/ 8 + /*fragment key length*/ 8 + /*num vertices*/ 8 + /*num triangles*/ 8;\n+        while (index < length) {\n+          if (index + headerSize > length) {\n+            throw new Error(`Invalid batch mesh fragment response.`);\n+          }\n+          const objectIdLow = dataView.getUint32(index, /*littleEndian=*/ true);\n+          const objectIdHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n+          const objectIdString = new Uint64(objectIdLow, objectIdHigh).toString();\n+          const prefix = objectIdString + '\\0';\n+          index += 8;\n+          const fragmentKeyLength = dataView.getUint32(index, /*littleEndian=*/ true);\n+          const fragmentKeyLengthHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n+          index += 8;\n+          if (fragmentKeyLengthHigh !== 0) {\n+            throw new Error(`Invalid batch mesh fragment response.`);\n+          }\n+          if (index + fragmentKeyLength + /* num vertices */ 8 + /*num indices*/ 8 > length) {\n+            throw new Error(`Invalid batch mesh fragment response.`);\n+          }\n+          const fragmentKey =\n+              new TextDecoder().decode(new Uint8Array(response, index, fragmentKeyLength));\n+          const fullKey = prefix + fragmentKey;\n+          const chunkIndex = ids.get(fullKey)!;\n+          if (!ids.delete(fullKey)) {\n+            throw new Error(`Received unexpected fragment key: ${JSON.stringify(fullKey)}.`);\n+          }\n+          index += fragmentKeyLength;\n+          const numVertices = dataView.getUint32(index, /*littleEndian=*/ true);\n+          const numVerticesHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n+          index += 8;\n+          const numTriangles = dataView.getUint32(index, /*littleEndian=*/ true);\n+          const numTrianglesHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n+          index += 8;\n+          if (numVerticesHigh !== 0 || numTrianglesHigh !== 0) {\n+            throw new Error(`Invalid batch mesh fragment response.`);\n+          }\n+          const endOffset = index + numTriangles * 12 + numVertices * 12;\n+          if (endOffset > length) {\n+            throw new Error(`Invalid batch mesh fragment response.`);\n+          }\n+          totalVertices += numVertices;\n+          totalIndices += numTriangles * 3;\n+          fragments.push({\n+            chunkIndex,\n+            buffer: response,\n+            verticesOffset: index,\n+            numVertices,\n+            indicesOffset: index + 12 * numVertices,\n+            numIndices: numTriangles * 3,\n+          });\n+          index = endOffset;\n         }\n-        totalVertices += numVertices;\n-        totalIndices += numTriangles * 3;\n-        fragments.push({\n-          chunkIndex,\n-          buffer: response,\n-          verticesOffset: index,\n-          numVertices,\n-          indicesOffset: index + 12 * numVertices,\n-          numIndices: numTriangles * 3,\n-        });\n-        index = endOffset;\n-      }\n \n-      if (ids.size !== 0) {\n-        // Partial response received.\n-        return makeBatchRequest();\n+        if (ids.size !== 0) {\n+          // Partial response received.\n+          return makeBatchRequest();\n+        }\n+        copyMeshData();\n       }\n-      copyMeshData();\n-    }\n \n-    const {credentialsProvider} = this;\n+      const {credentialsProvider} = this;\n \n-    const meshName = parameters.info.lods.find(x => x.lod === lod)!.info.name;\n+      const meshName = parameters.info.lods.find(x => x.lod === lod)!.info.name;\n \n-    function makeBatchRequest(): Promise<void> {\n-      const batches: BatchMeshFragment[] = [];\n-      let prevObjectId: string|undefined;\n-      let batchSize = 0;\n-      for (const id of ids.keys()) {\n-        const splitIndex = id.indexOf('\\0');\n-        const objectId = id.substring(0, splitIndex);\n-        const fragmentId = id.substring(splitIndex + 1);\n-        if (objectId !== prevObjectId) {\n-          batches.push({object_id: objectId, fragment_keys: []});\n+      function makeBatchRequest(): Promise<void> {\n+        const batches: BatchMeshFragment[] = [];\n+        let prevObjectId: string|undefined;\n+        let batchSize = 0;\n+        for (const id of ids.keys()) {\n+          const splitIndex = id.indexOf('\\0');\n+          const objectId = id.substring(0, splitIndex);\n+          const fragmentId = id.substring(splitIndex + 1);\n+          if (objectId !== prevObjectId) {\n+            batches.push({object_id: objectId, fragment_keys: []});\n+          }\n+          batches[batches.length - 1].fragment_keys.push(fragmentId);\n+          if (++batchSize === maxMeshBatchSize) break;\n         }\n-        batches[batches.length - 1].fragment_keys.push(fragmentId);\n-        if (++batchSize === maxMeshBatchSize) break;\n+        const payload: BatchMeshFragmentPayload = {\n+          volume_id: parameters.volumeId,\n+          mesh_name: meshName,\n+          batches: batches,\n+        };\n+        return makeRequest(\n+                   parameters['instance'], credentialsProvider, {\n+                     method: 'POST',\n+                     path,\n+                     payload: JSON.stringify(payload),\n+                     responseType: 'arraybuffer',\n+                   },\n+                   cancellationToken)\n+            .then(decodeResponse);\n       }\n-      const payload: BatchMeshFragmentPayload = {\n-        volume_id: parameters.volumeId,\n-        mesh_name: meshName,\n-        batches: batches,\n-      };\n-      return makeRequest(\n-                 parameters['instance'], credentialsProvider, {\n-                   method: 'POST',\n-                   path,\n-                   payload: JSON.stringify(payload),\n-                   responseType: 'arraybuffer',\n-                 },\n-                 cancellationToken)\n-          .then(decodeResponse);\n-    }\n-    return makeBatchRequest();\n+      return makeBatchRequest();\n   }\n }\n \n@@ -515,7 +517,9 @@ function decodeManifestChunkWithSupervoxelIds(chunk: ManifestChunk, response: an\n       convertEndian32(vertexBuffer, Endianness.LITTLE);\n       chunk.vertexPositions = vertexBuffer;\n       chunk.indices = indexBuffer;\n-      chunk.vertexNormals = computeVertexNormals(vertexBuffer, indexBuffer);\n+      const normals = computeVertexNormals(chunk.vertexPositions!, chunk.indices!);\n+      const encodedNormals = chunk.vertexNormals = new Uint8Array(normals.length / 3 * 2);\n+      encodeNormals32fx3ToOctahedron8x2(encodedNormals, normals);\n     }\n     function decodeResponse(response: ArrayBuffer): Promise<void>|void {\n       let length = response.byteLength;\n@@ -533,8 +537,8 @@ function decodeManifestChunkWithSupervoxelIds(chunk: ManifestChunk, response: an\n         const objectIdString = new Uint64(objectIdLow, objectIdHigh).toString();\n         prefix = objectIdString + '\\0';\n         index += 8;\n-        const fragmentKeyLength = dataView.getUint32(index, /*littleEndian=*/true);\n-        const fragmentKeyLengthHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n+        const fragmentKeyLength = dataView.getUint32(index, /*littleEndian=*/ true);\n+        const fragmentKeyLengthHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n         index += 8;\n         if (fragmentKeyLengthHigh !== 0) {\n           throw new Error(`Invalid batch mesh fragment response.`);\n@@ -549,11 +553,11 @@ function decodeManifestChunkWithSupervoxelIds(chunk: ManifestChunk, response: an\n           throw new Error(`Received unexpected fragment key: ${JSON.stringify(fullKey)}.`);\n         }\n         index += fragmentKeyLength;\n-        const numVertices = dataView.getUint32(index, /*littleEndian=*/true);\n-        const numVerticesHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n+        const numVertices = dataView.getUint32(index, /*littleEndian=*/ true);\n+        const numVerticesHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n         index += 8;\n-        const numTriangles = dataView.getUint32(index, /*littleEndian=*/true);\n-        const numTrianglesHigh = dataView.getUint32(index + 4, /*littleEndian=*/true);\n+        const numTriangles = dataView.getUint32(index, /*littleEndian=*/ true);\n+        const numTrianglesHigh = dataView.getUint32(index + 4, /*littleEndian=*/ true);\n         index += 8;\n         if (numVerticesHigh !== 0 || numTrianglesHigh !== 0) {\n           throw new Error(`Invalid batch mesh fragment response.`);\n@@ -627,8 +631,8 @@ function decodeSkeletonChunk(chunk: SkeletonChunk, response: ArrayBuffer) {\n     throw new Error(`The number of edges should not exceed 2^32-1.`);\n   }\n   decodeSkeletonVertexPositionsAndIndices(\n-      chunk, response, Endianness.LITTLE, /*vertexByteOffset=*/16, numVertices,\n-      /*indexByteOffset=*/undefined, /*numEdges=*/numEdges);\n+      chunk, response, Endianness.LITTLE, /*vertexByteOffset=*/ 16, numVertices,\n+      /*indexByteOffset=*/ undefined, /*numEdges=*/ numEdges);\n }\n \n @registerSharedObject() export class BrainmapsSkeletonSource extends\n@@ -854,7 +858,8 @@ function annotationToBrainmaps(annotation: Annotation): any {\n         });\n   }\n \n-  downloadSegmentFilteredGeometry(chunk: AnnotationSubsetGeometryChunk, cancellationToken: CancellationToken) {\n+  downloadSegmentFilteredGeometry(\n+      chunk: AnnotationSubsetGeometryChunk, cancellationToken: CancellationToken) {\n     const {parameters} = this;\n     return Promise\n         .all(spatialAnnotationTypes.map(\ndiff --git a/src/neuroglancer/mesh/backend.ts b/src/neuroglancer/mesh/backend.ts\nindex 7553b54..3437c8d 100644\n--- a/src/neuroglancer/mesh/backend.ts\n+++ b/src/neuroglancer/mesh/backend.ts\n@@ -84,7 +84,7 @@ export class FragmentChunk extends Chunk {\n   fragmentId: FragmentId|null = null;\n   vertexPositions: Float32Array|null = null;\n   indices: Uint32Array|null = null;\n-  vertexNormals: Float32Array|null = null;\n+  vertexNormals: Uint8Array|null = null;\n   constructor() {\n     super();\n   }\n@@ -179,6 +179,49 @@ export function computeVertexNormals(positions: Float32Array, indices: Uint32Arr\n   return vertexNormals;\n }\n \n+/**\n+ * Converts a floating-point number in the range `[-1, 1]` to an integer in the range `[-127, 127]`.\n+ */\n+function snorm8(x: number) {\n+  return Math.min(Math.max(-127, x * 127 + 0.5), 127) >>> 0;\n+}\n+\n+function signNotZero(x: number) {\n+  return x < 0 ? -1 : 1;\n+}\n+\n+/**\n+ * Encodes normal vectors represented as 3x32-bit floating vectors into a 2x8-bit octahedron\n+ * representation.\n+ *\n+ * Zina H. Cigolle, Sam Donow, Daniel Evangelakos, Michael Mara, Morgan McGuire, and Quirin Meyer,\n+ * Survey of Efficient Representations for Independent Unit Vectors, Journal of Computer Graphics\n+ * Techniques (JCGT), vol. 3, no. 2, 1-30, 2014\n+ *\n+ * Available online http://jcgt.org/published/0003/02/01/\n+ *\n+ * @param out[out] Row-major array of shape `[n, 2]` set to octahedron representation.\n+ * @param normals[in] Row-major array of shape `[n, 3]` specifying unit normal vectors.\n+ */\n+export function encodeNormals32fx3ToOctahedron8x2(out: Uint8Array, normals: Float32Array) {\n+  const length = normals.length;\n+  let outIndex = 0;\n+  for (let i = 0; i < length; i += 3) {\n+    const x = normals[i], y = normals[i + 1], z = normals[i + 2];\n+\n+    const invL1Norm = 1 / (Math.abs(x) + Math.abs(y) + Math.abs(z));\n+\n+    if (z < 0) {\n+      out[outIndex] = snorm8((1 - Math.abs(y * invL1Norm)) * signNotZero(x));\n+      out[outIndex + 1] = snorm8((1 - Math.abs(x * invL1Norm)) * signNotZero(y));\n+    } else {\n+      out[outIndex] = snorm8(x * invL1Norm);\n+      out[outIndex+1] = snorm8(y * invL1Norm);\n+    }\n+    outIndex += 2;\n+  }\n+}\n+\n /**\n  * Extracts vertex positions and indices of the specified endianness from `data'.\n  *\n@@ -231,8 +274,10 @@ export function decodeTriangleVertexPositionsAndIndices(\n     numVertices: number, indexByteOffset?: number, numTriangles?: number) {\n   decodeVertexPositionsAndIndices(\n       chunk, /*verticesPerPrimitive=*/ 3, data, endianness, vertexByteOffset, numVertices,\n-      indexByteOffset, numTriangles);\n-  chunk.vertexNormals = computeVertexNormals(chunk.vertexPositions!, chunk.indices!);\n+    indexByteOffset, numTriangles);\n+  const normals = computeVertexNormals(chunk.vertexPositions!, chunk.indices!);\n+  const encodedNormals = chunk.vertexNormals = new Uint8Array(normals.length / 3 * 2);\n+  encodeNormals32fx3ToOctahedron8x2(encodedNormals, normals);\n }\n \n export interface MeshSource {\n@@ -397,7 +442,7 @@ export class MultiscaleFragmentChunk extends Chunk {\n   subChunkOffsets: Uint32Array|null = null;\n   vertexPositions: Float32Array|null = null;\n   indices: Uint32Array|null = null;\n-  vertexNormals: Float32Array|null = null;\n+  vertexNormals: Uint8Array|null = null;\n   lod: number = 0;\n   chunkIndex: number = 0;\n   manifestChunk: MultiscaleManifestChunk|null = null;\ndiff --git a/src/neuroglancer/mesh/frontend.ts b/src/neuroglancer/mesh/frontend.ts\nindex dd4c712..528a34e 100644\n--- a/src/neuroglancer/mesh/frontend.ts\n+++ b/src/neuroglancer/mesh/frontend.ts\n@@ -33,22 +33,40 @@ const tempMat3 = mat3.create();\n \n const DEBUG_MULTISCALE_FRAGMENTS = false;\n \n+/**\n+ * Decodes normal vectors in 2xSnorm8 octahedron encoding into normalized 3x32f vector.\n+ *\n+ * Zina H. Cigolle, Sam Donow, Daniel Evangelakos, Michael Mara, Morgan McGuire, and Quirin Meyer,\n+ * Survey of Efficient Representations for Independent Unit Vectors, Journal of Computer Graphics\n+ * Techniques (JCGT), vol. 3, no. 2, 1-30, 2014\n+ *\n+ * Available online http://jcgt.org/published/0003/02/01/\n+ */\n+const glsl_decodeNormalOctahedronSnorm8 = `\n+highp vec3 decodeNormalOctahedronSnorm8(highp vec2 e) {\n+  vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));\n+  if (v.z < 0.0) v.xy = (1.0 - abs(v.yx)) * vec2(v.x > 0.0 ? 1.0 : -1.0, v.y > 0.0 ? 1.0 : -1.0);\n+  return normalize(v);\n+}\n+`;\n+\n export class MeshShaderManager {\n   private tempLightVec = new Float32Array(4);\n   constructor() {}\n \n   defineShader(builder: ShaderBuilder) {\n     builder.addAttribute('highp vec3', 'aVertexPosition');\n-    builder.addAttribute('highp vec3', 'aVertexNormal');\n+    builder.addAttribute('highp vec2', 'aVertexNormal');\n     builder.addVarying('highp vec4', 'vColor');\n     builder.addUniform('highp vec4', 'uLightDirection');\n     builder.addUniform('highp vec4', 'uColor');\n     builder.addUniform('highp mat3', 'uNormalMatrix');\n     builder.addUniform('highp mat4', 'uModelViewProjection');\n     builder.addUniform('highp uint', 'uPickID');\n+    builder.addVertexCode(glsl_decodeNormalOctahedronSnorm8);\n     builder.setVertexMain(`\n gl_Position = uModelViewProjection * vec4(aVertexPosition, 1.0);\n-vec3 normal = uNormalMatrix * aVertexNormal;\n+vec3 normal = uNormalMatrix * decodeNormalOctahedronSnorm8(aVertexNormal);\n float lightingFactor = abs(dot(normal, uLightDirection.xyz)) + uLightDirection.w;\n vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n `);\n@@ -99,7 +117,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n \n     fragmentChunk.normalBuffer.bindToVertexAttrib(\n         shader.attribute('aVertexNormal'),\n-        /*components=*/ 3);\n+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);\n     fragmentChunk.indexBuffer.bind();\n     gl.drawElements(gl.TRIANGLES, fragmentChunk.numIndices, gl.UNSIGNED_INT, 0);\n   }\n@@ -113,7 +131,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n \n     fragmentChunk.normalBuffer.bindToVertexAttrib(\n         shader.attribute('aVertexNormal'),\n-        /*components=*/ 3);\n+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);\n     fragmentChunk.indexBuffer.bind();\n     const indexBegin = fragmentChunk.subChunkOffsets[subChunkBegin];\n     const indexEnd = fragmentChunk.subChunkOffsets[subChunkEnd];\n@@ -257,7 +275,7 @@ export class ManifestChunk extends Chunk {\n export class FragmentChunk extends Chunk {\n   vertexPositions: Float32Array;\n   indices: Uint32Array;\n-  vertexNormals: Float32Array;\n+  vertexNormals: Uint8Array;\n   objectKey: string;\n   source: FragmentSource;\n   vertexBuffer: Buffer;\n@@ -503,7 +521,7 @@ export class MultiscaleFragmentChunk extends Chunk {\n   subChunkOffsets: Uint32Array;\n   vertexPositions: Float32Array;\n   indices: Uint32Array;\n-  vertexNormals: Float32Array;\n+  vertexNormals: Uint8Array;\n   source: MultiscaleFragmentSource;\n   vertexBuffer: Buffer;\n   indexBuffer: Buffer;\n", 'commit_subject': 'feat(mesh): encode normal vectors using 2xSnorm8 octahedron encoding', 'repo_name': 'google/neuroglancer', 'committer': {'name': 'Jeremy Maitin-Shepard', 'email': 'e349c33eb9031b46b70f51528427a078cdb06294@google.com', 'time_sec': '1554685581', 'tz_offset': '-420', 'date': {'seconds': '1554685581'}}, 'commit_tree': 'ffe389ede417da0cd786875a9c11de421514b177', 'commit_message': 'feat(mesh): encode normal vectors using 2xSnorm8 octahedron encoding\n\nThis reduces memory usage of normal vectors from 12 bytes to 2 bytes per vertex.\n', 'language_name': 'TypeScript', 'license': 'apache-2.0', 'hunks': "@@ -33,22 +33,40 @@ const tempMat3 = mat3.create();\n \n const DEBUG_MULTISCALE_FRAGMENTS = false;\n \n+/**\n+ * Decodes normal vectors in 2xSnorm8 octahedron encoding into normalized 3x32f vector.\n+ *\n+ * Zina H. Cigolle, Sam Donow, Daniel Evangelakos, Michael Mara, Morgan McGuire, and Quirin Meyer,\n+ * Survey of Efficient Representations for Independent Unit Vectors, Journal of Computer Graphics\n+ * Techniques (JCGT), vol. 3, no. 2, 1-30, 2014\n+ *\n+ * Available online http://jcgt.org/published/0003/02/01/\n+ */\n+const glsl_decodeNormalOctahedronSnorm8 = `\n+highp vec3 decodeNormalOctahedronSnorm8(highp vec2 e) {\n+  vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));\n+  if (v.z < 0.0) v.xy = (1.0 - abs(v.yx)) * vec2(v.x > 0.0 ? 1.0 : -1.0, v.y > 0.0 ? 1.0 : -1.0);\n+  return normalize(v);\n+}\n+`;\n+\n export class MeshShaderManager {\n   private tempLightVec = new Float32Array(4);\n   constructor() {}\n \n   defineShader(builder: ShaderBuilder) {\n     builder.addAttribute('highp vec3', 'aVertexPosition');\n-    builder.addAttribute('highp vec3', 'aVertexNormal');\n+    builder.addAttribute('highp vec2', 'aVertexNormal');\n     builder.addVarying('highp vec4', 'vColor');\n     builder.addUniform('highp vec4', 'uLightDirection');\n     builder.addUniform('highp vec4', 'uColor');\n     builder.addUniform('highp mat3', 'uNormalMatrix');\n     builder.addUniform('highp mat4', 'uModelViewProjection');\n     builder.addUniform('highp uint', 'uPickID');\n+    builder.addVertexCode(glsl_decodeNormalOctahedronSnorm8);\n     builder.setVertexMain(`\n gl_Position = uModelViewProjection * vec4(aVertexPosition, 1.0);\n-vec3 normal = uNormalMatrix * aVertexNormal;\n+vec3 normal = uNormalMatrix * decodeNormalOctahedronSnorm8(aVertexNormal);\n float lightingFactor = abs(dot(normal, uLightDirection.xyz)) + uLightDirection.w;\n vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n `);\n@@ -99,7 +117,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n \n     fragmentChunk.normalBuffer.bindToVertexAttrib(\n         shader.attribute('aVertexNormal'),\n-        /*components=*/ 3);\n+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);\n     fragmentChunk.indexBuffer.bind();\n     gl.drawElements(gl.TRIANGLES, fragmentChunk.numIndices, gl.UNSIGNED_INT, 0);\n   }\n@@ -113,7 +131,7 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n \n     fragmentChunk.normalBuffer.bindToVertexAttrib(\n         shader.attribute('aVertexNormal'),\n-        /*components=*/ 3);\n+        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);\n     fragmentChunk.indexBuffer.bind();\n     const indexBegin = fragmentChunk.subChunkOffsets[subChunkBegin];\n     const indexEnd = fragmentChunk.subChunkOffsets[subChunkEnd];\n@@ -257,7 +275,7 @@ export class ManifestChunk extends Chunk {\n export class FragmentChunk extends Chunk {\n   vertexPositions: Float32Array;\n   indices: Uint32Array;\n-  vertexNormals: Float32Array;\n+  vertexNormals: Uint8Array;\n   objectKey: string;\n   source: FragmentSource;\n   vertexBuffer: Buffer;\n@@ -503,7 +521,7 @@ export class MultiscaleFragmentChunk extends Chunk {\n   subChunkOffsets: Uint32Array;\n   vertexPositions: Float32Array;\n   indices: Uint32Array;\n-  vertexNormals: Float32Array;\n+  vertexNormals: Uint8Array;\n   source: MultiscaleFragmentSource;\n   vertexBuffer: Buffer;\n   indexBuffer: Buffer;\n", 'addition_count': 24, 'deletion_count': 6, 'src_file': 'a/src/neuroglancer/mesh/frontend.ts', 'tgt_file': 'b/src/neuroglancer/mesh/frontend.ts', 'file_extension': '.ts', 'before_file': '/**\n * @license\n * Copyright 2016 Google Inc.\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ChunkState} from \'neuroglancer/chunk_manager/base\';\nimport {Chunk, ChunkManager, ChunkSource} from \'neuroglancer/chunk_manager/frontend\';\nimport {VisibleLayerInfo} from \'neuroglancer/layer\';\nimport {EncodedMeshData, FRAGMENT_SOURCE_RPC_ID, MESH_LAYER_RPC_ID, MULTISCALE_FRAGMENT_SOURCE_RPC_ID, MULTISCALE_MESH_LAYER_RPC_ID, MultiscaleFragmentFormat, VertexPositionFormat} from \'neuroglancer/mesh/base\';\nimport {getMultiscaleChunksToDraw, getMultiscaleFragmentKey, MultiscaleMeshManifest, validateOctree} from \'neuroglancer/mesh/multiscale\';\nimport {PerspectivePanel} from \'neuroglancer/perspective_view/panel\';\nimport {PerspectiveViewReadyRenderContext, PerspectiveViewRenderContext, PerspectiveViewRenderLayer} from \'neuroglancer/perspective_view/render_layer\';\nimport {ThreeDimensionalRenderLayerAttachmentState, update3dRenderLayerAttachment} from \'neuroglancer/renderlayer\';\nimport {forEachVisibleSegment, getObjectKey} from \'neuroglancer/segmentation_display_state/base\';\nimport {forEachVisibleSegmentToDraw, registerRedrawWhenSegmentationDisplayState3DChanged, SegmentationDisplayState3D, SegmentationLayerSharedObject} from \'neuroglancer/segmentation_display_state/frontend\';\nimport {makeCachedDerivedWatchableValue, WatchableValueInterface} from \'neuroglancer/trackable_value\';\nimport {Borrowed, RefCounted} from \'neuroglancer/util/disposable\';\nimport {getFrustrumPlanes, mat3, mat3FromMat4, mat4, scaleMat3Output, vec3, vec4} from \'neuroglancer/util/geom\';\nimport * as matrix from \'neuroglancer/util/matrix\';\nimport {Uint64} from \'neuroglancer/util/uint64\';\nimport {Buffer} from \'neuroglancer/webgl/buffer\';\n\nconst DEBUG_MULTISCALE_FRAGMENTS = false;\n\nexport class MeshShaderManager {\n  private tempLightVec = new Float32Array(4);\n  constructor() {}\n\n  defineShader(builder: ShaderBuilder) {\n    builder.addAttribute(\'highp vec3\', \'aVertexPosition\');\n    builder.addAttribute(\'highp vec3\', \'aVertexNormal\');\n    builder.addVarying(\'highp vec4\', \'vColor\');\n    builder.addUniform(\'highp vec4\', \'uLightDirection\');\n    builder.addUniform(\'highp vec4\', \'uColor\');\n    builder.addUniform(\'highp mat3\', \'uNormalMatrix\');\n    builder.addUniform(\'highp mat4\', \'uModelViewProjection\');\n    builder.addUniform(\'highp uint\', \'uPickID\');\n    builder.setVertexMain(`\ngl_Position = uModelViewProjection * vec4(aVertexPosition, 1.0);\nvec3 normal = uNormalMatrix * aVertexNormal;\nfloat lightingFactor = abs(dot(normal, uLightDirection.xyz)) + uLightDirection.w;\nvColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n`);\n  if (v.z < 0.0) v.xy = (1.0 - abs(v.yx)) * vec2(v.x > 0.0 ? 1.0 : -1.0, v.y > 0.0 ? 1.0 : -1.0);\n  return normalize(v);\n}\n`;\n\nfunction decodeSnorm8(x: number) {\n  if (x >= 128) {\n    x = x - 256;\n  }\n  return Math.max(-1, x / 127);\n}\n\n/**\n * Javascript implementation of normal decoding, for debugging.\n */\nexport function decodeNormalOctahedronSnorm8(normals: Uint8Array) {\n  let n = normals.length;\n  n -= 1;\n  let out = new Float32Array(Math.floor(normals.length / 2) * 3);\n  let outIndex = 0;\n  for (let i = 0; i < n; i += 2) {\n    const e0 = decodeSnorm8(normals[i]);\n    const e1 = decodeSnorm8(normals[i + 1]);\n    let v0 = e0;\n    let v1 = e1;\n    let v2 = 1.0 - Math.abs(e0) - Math.abs(e1);\n    if (v2 < 0) {\n      v0 = (1 - Math.abs(v1)) * (v0 > 0 ? 1 : -1);\n      v1 = (1 - Math.abs(v0)) * (v1 > 0 ? 1 : -1);\n    }\n    const len = Math.sqrt(v0 ** 2 + v1 ** 2 + v2 ** 2);\n    out[outIndex] = v0 / len;\n    out[outIndex + 1] = v1 / len;\n    out[outIndex + 2] = v2 / len;\n    outIndex += 3;\n  }\n  return out;\n}\n\ninterface VertexPositionFormatHandler {\n  defineShader: (builder: ShaderBuilder) => void;\n  bind:\n      (gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) => void;\n  endLayer: (gl: GL, shader: ShaderProgram) => void;\n\n    fragmentChunk.normalBuffer.bindToVertexAttrib(\n        shader.attribute(\'aVertexNormal\'),\n        /*components=*/ 3);\n    fragmentChunk.indexBuffer.bind();\n    gl.drawElements(gl.TRIANGLES, fragmentChunk.numIndices, gl.UNSIGNED_INT, 0);\n  }\n    },\n    bind(_gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) {\n      fragmentChunk.vertexBuffer.bindToVertexAttrib(\n          shader.attribute(\'aVertexPosition\'),\n          /*components=*/ 3, glAttributeType, /* normalized=*/ true);\n    },\n    endLayer: (gl: GL, shader: ShaderProgram) => {\n\n    fragmentChunk.normalBuffer.bindToVertexAttrib(\n        shader.attribute(\'aVertexNormal\'),\n        /*components=*/ 3);\n    fragmentChunk.indexBuffer.bind();\n    const indexBegin = fragmentChunk.subChunkOffsets[subChunkBegin];\n    const indexEnd = fragmentChunk.subChunkOffsets[subChunkEnd];\n  [VertexPositionFormat.uint16]: getFloatPositionHandler(WebGL2RenderingContext.UNSIGNED_SHORT),\n  [VertexPositionFormat.uint10]: {\n    defineShader: (builder: ShaderBuilder) => {\n      builder.addAttribute(\'highp uint\', \'aVertexPosition\');\n      builder.addVertexCode(`\nhighp vec3 getVertexPosition() {\n  return vec3(float(aVertexPosition & 1023u),\n              float((aVertexPosition >> 10) & 1023u),\n              float((aVertexPosition >> 20) & 1023u)) / 1023.0;\n}\n`);\n    },\n    bind(_gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk) {\n      fragmentChunk.vertexBuffer.bindToVertexAttribI(\n          shader.attribute(\'aVertexPosition\'),\n          /*components=*/ 1, WebGL2RenderingContext.UNSIGNED_INT);\n    },\n    endLayer: (gl: GL, shader: ShaderProgram) => {\n      gl.disableVertexAttribArray(shader.attribute(\'aVertexPosition\'));\n    }\n  },\n};\n\nexport class MeshShaderManager {\n  private tempLightVec = new Float32Array(4);\n  private vertexPositionHandler = vertexPositionHandlers[this.vertexPositionFormat];\n  constructor(\n      public fragmentRelativeVertices: boolean, public vertexPositionFormat: VertexPositionFormat) {\n  }\n\n  beginLayer(\n      gl: GL, shader: ShaderProgram, renderContext: PerspectiveViewRenderContext,\n      displayState: MeshDisplayState) {\n    let {lightDirection, ambientLighting, directionalLighting} = renderContext;\n    let lightVec = <vec3>this.tempLightVec;\n    vec3.scale(lightVec, lightDirection, directionalLighting);\n    lightVec[3] = ambientLighting;\n    gl.uniform4fv(shader.uniform(\'uLightDirection\'), lightVec);\n    const silhouetteRendering = displayState.silhouetteRendering.value;\n    if (silhouetteRendering > 0) {\n      gl.uniform1f(shader.uniform(\'uSilhouettePower\'), silhouetteRendering);\n    }\n  }\n\n  setColor(gl: GL, shader: ShaderProgram, color: vec4) {\n    gl.uniform4fv(shader.uniform(\'uColor\'), color);\n  }\n\n  setPickID(gl: GL, shader: ShaderProgram, pickID: number) {\n    gl.uniform1ui(shader.uniform(\'uPickID\'), pickID);\n  }\n\n  beginModel(\n      gl: GL, shader: ShaderProgram, renderContext: PerspectiveViewRenderContext, modelMat: mat4) {\n    const {projectionParameters} = renderContext;\n    gl.uniformMatrix4fv(\n        shader.uniform(\'uModelViewProjection\'), false,\n        mat4.multiply(tempMat4, projectionParameters.viewProjectionMat, modelMat));\n    mat3FromMat4(tempMat3, modelMat);\n    scaleMat3Output(\n        tempMat3, tempMat3, projectionParameters.displayDimensionRenderInfo.canonicalVoxelFactors);\n    mat3.invert(tempMat3, tempMat3);\n    mat3.transpose(tempMat3, tempMat3);\n    gl.uniformMatrix3fv(shader.uniform(\'uNormalMatrix\'), false, tempMat3);\n  }\n\n  drawFragmentHelper(\n      gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk|MultiscaleFragmentChunk,\n      indexBegin: number, indexEnd: number) {\n    this.vertexPositionHandler.bind(gl, shader, fragmentChunk);\n    const {meshData} = fragmentChunk;\n    fragmentChunk.normalBuffer.bindToVertexAttrib(\n        shader.attribute(\'aVertexNormal\'),\n        /*components=*/ 2, WebGL2RenderingContext.BYTE, /*normalized=*/ true);\n    fragmentChunk.indexBuffer.bind();\n    const {indices} = meshData;\n    gl.drawElements(\n        meshData.strips ? WebGL2RenderingContext.TRIANGLE_STRIP : WebGL2RenderingContext.TRIANGLES,\n        indexEnd - indexBegin,\n        indices.BYTES_PER_ELEMENT === 2 ? WebGL2RenderingContext.UNSIGNED_SHORT :\n                                          WebGL2RenderingContext.UNSIGNED_INT,\n        indexBegin * indices.BYTES_PER_ELEMENT);\n  }\n\n  drawFragment(gl: GL, shader: ShaderProgram, fragmentChunk: FragmentChunk) {\n    const {meshData} = fragmentChunk;\n    const {indices} = meshData;\n    this.drawFragmentHelper(gl, shader, fragmentChunk, 0, indices.length);\n  }\n\n  drawMultiscaleFragment(\n      gl: GL,\n      shader: ShaderProgram,\n      fragmentChunk: MultiscaleFragmentChunk,\n      subChunkBegin: number,\n      subChunkEnd: number,\n  ) {\n    const indexBegin = fragmentChunk.meshData.subChunkOffsets[subChunkBegin];\n    const indexEnd = fragmentChunk.meshData.subChunkOffsets[subChunkEnd];\n    this.drawFragmentHelper(gl, shader, fragmentChunk, indexBegin, indexEnd);\n  }\n\n  endLayer(gl: GL, shader: ShaderProgram) {\n    this.vertexPositionHandler.endLayer(gl, shader);\n    gl.disableVertexAttribArray(shader.attribute(\'aVertexNormal\'));\n  }\n\n  makeGetter(layer: RefCounted&{gl: GL, displayState: MeshDisplayState}) {\n    const silhouetteRenderingEnabled = layer.registerDisposer(\n        makeCachedDerivedWatchableValue(x => x > 0, [layer.displayState.silhouetteRendering]));\n    return parameterizedEmitterDependentShaderGetter(layer, layer.gl, {\n      memoizeKey:\n          `mesh/MeshShaderManager/${this.fragmentRelativeVertices}/${this.vertexPositionFormat}`,\n      parameters: silhouetteRenderingEnabled,\n      defineShader: (builder, silhouetteRenderingEnabled) => {\n        this.vertexPositionHandler.defineShader(builder);\n        builder.addAttribute(\'highp vec2\', \'aVertexNormal\');\n        builder.addVarying(\'highp vec4\', \'vColor\');\n        builder.addUniform(\'highp vec4\', \'uLightDirection\');\n        builder.addUniform(\'highp vec4\', \'uColor\');\n        builder.addUniform(\'highp mat3\', \'uNormalMatrix\');\n        builder.addUniform(\'highp mat4\', \'uModelViewProjection\');\n        builder.addUniform(\'highp uint\', \'uPickID\');\n        if (silhouetteRenderingEnabled) {\n          builder.addUniform(\'highp float\', \'uSilhouettePower\');\n        }\n        if (this.fragmentRelativeVertices) {\n          builder.addUniform(\'highp vec3\', \'uFragmentOrigin\');\n          builder.addUniform(\'highp vec3\', \'uFragmentShape\');\n        }\n        builder.addVertexCode(glsl_decodeNormalOctahedronSnorm8);\n        let vertexMain = ``;\n        if (this.fragmentRelativeVertices) {\n          vertexMain += `\nhighp vec3 vertexPosition = uFragmentOrigin + uFragmentShape * getVertexPosition();\nhighp vec3 normalMultiplier = 1.0 / uFragmentShape;\n`;\nexport class FragmentChunk extends Chunk {\n  vertexPositions: Float32Array;\n  indices: Uint32Array;\n  vertexNormals: Float32Array;\n  objectKey: string;\n  source: FragmentSource;\n  vertexBuffer: Buffer;\ngl_Position = uModelViewProjection * vec4(vertexPosition, 1.0);\nvec3 origNormal = decodeNormalOctahedronSnorm8(aVertexNormal);\nvec3 normal = normalize(uNormalMatrix * (normalMultiplier * origNormal));\nfloat absCosAngle = abs(dot(normal, uLightDirection.xyz));\nfloat lightingFactor = absCosAngle + uLightDirection.w;\nvColor = vec4(lightingFactor * uColor.rgb, uColor.a);\n`;\n        if (silhouetteRenderingEnabled) {\n          vertexMain += `\nvColor *= pow(1.0 - absCosAngle, uSilhouettePower);\n`;\n        }\n        builder.setVertexMain(vertexMain);\n        builder.setFragmentMain(`emit(vColor, uPickID);`);\n      },\n    });\n  }\n}\n\nexport interface MeshDisplayState extends SegmentationDisplayState3D {\n  silhouetteRendering: WatchableValueInterface<number>;\n}\n\nexport class MeshLayer extends\n    PerspectiveViewRenderLayer<ThreeDimensionalRenderLayerAttachmentState> {\n  protected meshShaderManager =\n      new MeshShaderManager(/*fragmentRelativeVertices=*/ false, VertexPositionFormat.float32);\n  private getShader = this.meshShaderManager.makeGetter(this);\n  backend: SegmentationLayerSharedObject;\n\n  constructor(\n      public chunkManager: ChunkManager, public source: MeshSource,\n      public displayState: MeshDisplayState) {\n    super();\n\n    registerRedrawWhenSegmentationDisplayState3DChanged(displayState, this);\n    this.registerDisposer(displayState.silhouetteRendering.changed.add(this.redrawNeeded.dispatch));\n\n    let sharedObject = this.backend = this.registerDisposer(\n        new SegmentationLayerSharedObject(chunkManager, displayState, this.layerChunkProgressInfo));\n    sharedObject.RPC_TYPE_ID = MESH_LAYER_RPC_ID;\n    sharedObject.initializeCounterpartWithChunkManager({\n      \'source\': source.addCounterpartRef(),\n    });\n    sharedObject.visibility.add(this.visibility);\n    this.registerDisposer(displayState.renderScaleHistogram.visibility.add(this.visibility));\n  }\n\n  get isTransparent() {\n    const {displayState} = this;\n    return displayState.objectAlpha.value < 1.0 || displayState.silhouetteRendering.value > 0;\n  }\n\n  get transparentPickEnabled() {\n    return this.displayState.transparentPickEnabled.value;\n  }\n\n  get gl() {\n    return this.chunkManager.chunkQueueManager.gl;\n  }\n\n  draw(\n      renderContext: PerspectiveViewRenderContext,\n      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {\n    if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) {\n      // No need for a separate pick ID pass.\n      return;\n    }\n    const {gl, displayState, meshShaderManager} = this;\n    if (displayState.objectAlpha.value <= 0.0) {\n      // Skip drawing.\n      return;\n    }\n    const modelMatrix = update3dRenderLayerAttachment(\n        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,\n        attachment);\n    if (modelMatrix === undefined) {\n      return;\n    }\n    const {shader} = this.getShader(renderContext.emitter);\n    if (shader === null) return;\n    shader.bind();\n    meshShaderManager.beginLayer(gl, shader, renderContext, this.displayState);\n    meshShaderManager.beginModel(gl, shader, renderContext, modelMatrix);\n\n    const manifestChunks = this.source.chunks;\n\n    let totalChunks = 0, presentChunks = 0;\n    const {renderScaleHistogram} = this.displayState;\n    const fragmentChunks = this.source.fragmentSource.chunks;\n\n    forEachVisibleSegmentToDraw(\n        displayState, this, renderContext.emitColor,\n        renderContext.emitPickID ? renderContext.pickIDs : undefined,\n        (objectId, color, pickIndex) => {\n          const key = getObjectKey(objectId);\n          const manifestChunk = manifestChunks.get(key);\n          ++totalChunks;\n          if (manifestChunk === undefined) return;\n          ++presentChunks;\n          if (renderContext.emitColor) {\n            meshShaderManager.setColor(gl, shader, color!);\n          }\n          if (renderContext.emitPickID) {\n            meshShaderManager.setPickID(gl, shader, pickIndex!);\n          }\n          totalChunks += manifestChunk.fragmentIds.length;\n\n          for (const fragmentId of manifestChunk.fragmentIds) {\n            const {key: fragmentKey} = this.source.getFragmentKey(key, fragmentId);\n            const fragment = fragmentChunks.get(fragmentKey);\n            if (fragment !== undefined && fragment.state === ChunkState.GPU_MEMORY) {\n              meshShaderManager.drawFragment(gl, shader, fragment);\n              ++presentChunks;\n            }\n          }\n        });\n\n    if (renderContext.emitColor) {\n      renderScaleHistogram.begin(\n          this.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber);\n      renderScaleHistogram.add(\n          Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, presentChunks,\n          totalChunks - presentChunks);\n    }\n    meshShaderManager.endLayer(gl, shader);\n  }\n\n  isReady() {\n    const {displayState, source} = this;\n    let ready = true;\n    const fragmentChunks = source.fragmentSource.chunks;\n    forEachVisibleSegment(displayState.segmentationGroupState.value, objectId => {\n      const key = getObjectKey(objectId);\n      const manifestChunk = source.chunks.get(key);\n      if (manifestChunk === undefined) {\n        ready = false;\n        return;\n      }\n      for (const fragmentId of manifestChunk.fragmentIds) {\n        const {key: fragmentKey} = this.source.getFragmentKey(key, fragmentId);\n        const fragmentChunk = fragmentChunks.get(fragmentKey);\n        if (fragmentChunk === undefined || fragmentChunk.state !== ChunkState.GPU_MEMORY) {\n          ready = false;\n          return;\n        }\n      }\n    });\n    return ready;\n  }\n}\n\nexport class ManifestChunk extends Chunk {\n  fragmentIds: string[];\n\n  constructor(source: MeshSource, x: any) {\n    super(source);\n    this.fragmentIds = x.fragmentIds;\n  }\n}\n\nexport class FragmentChunk extends Chunk {\n  source: FragmentSource;\n  vertexBuffer: Buffer;\n  indexBuffer: Buffer;\n  normalBuffer: Buffer;\n  meshData: EncodedMeshData;\n\n  constructor(source: FragmentSource, x: any) {\n    super(source);\n    this.meshData = x;\n  }\n\n  copyToGPU(gl: GL) {\n    super.copyToGPU(gl);\n    copyMeshDataToGpu(gl, this);\n  }\n\n  freeGPUMemory(gl: GL) {\n    super.freeGPUMemory(gl);\n    freeGpuMeshData(this);\n  }\n}\n\nexport interface MeshSourceOptions {}\n\nexport class MeshSource extends ChunkSource {\n  fragmentSource = this.registerDisposer(new FragmentSource(this.chunkManager, this));\n  chunks: Map<string, ManifestChunk>;\n\n  constructor(chunkManager: Borrowed<ChunkManager>, options: MeshSourceOptions) {\n    super(chunkManager, options);\n  }\n\n  initializeCounterpart(rpc: RPC, options: any) {\n    this.fragmentSource.initializeCounterpart(this.chunkManager.rpc!, {});\n    options[\'fragmentSource\'] = this.fragmentSource.addCounterpartRef();\n    super.initializeCounterpart(rpc, options);\n  }\n  getChunk(x: any) {\n    return new ManifestChunk(this, x);\n  }\n  getFragmentKey(objectKey: string, fragmentId: string) {\n    return {key:`${objectKey}/${fragmentId}`, fragmentId: fragmentId}\n  }\n}\n\n@registerSharedObjectOwner(FRAGMENT_SOURCE_RPC_ID)\nexport class FragmentSource extends ChunkSource {\n  chunks: Map<string, FragmentChunk>;\n  get key() {\n    return this.meshSource.key;\n  }\n  constructor(chunkManager: ChunkManager, public meshSource: MeshSource) {\n    super(chunkManager);\n  }\n  getChunk(x: any) {\n    return new FragmentChunk(this, x);\n  }\n}\n\n\nfunction hasFragmentChunk(\n    fragmentChunks: Map<string, MultiscaleFragmentChunk>, objectKey: string, lod: number,\n    chunkIndex: number) {\n  const fragmentChunk = fragmentChunks.get(getMultiscaleFragmentKey(objectKey, lod, chunkIndex));\n  return fragmentChunk !== undefined && fragmentChunk.state === ChunkState.GPU_MEMORY;\n}\n\nexport class MultiscaleMeshLayer extends\n    PerspectiveViewRenderLayer<ThreeDimensionalRenderLayerAttachmentState> {\n  protected meshShaderManager = new MeshShaderManager(\n      /*fragmentRelativeVertices=*/ this.source.format.fragmentRelativeVertices,\n      this.source.format.vertexPositionFormat);\n  private getShader = this.meshShaderManager.makeGetter(this);\n  backend: SegmentationLayerSharedObject;\n\n  constructor(\n      public chunkManager: ChunkManager, public source: MultiscaleMeshSource,\n  subChunkOffsets: Uint32Array;\n  vertexPositions: Float32Array;\n  indices: Uint32Array;\n  vertexNormals: Float32Array;\n  source: MultiscaleFragmentSource;\n  vertexBuffer: Buffer;\n  indexBuffer: Buffer;\n        new SegmentationLayerSharedObject(chunkManager, displayState, this.layerChunkProgressInfo));\n    sharedObject.RPC_TYPE_ID = MULTISCALE_MESH_LAYER_RPC_ID;\n    sharedObject.initializeCounterpartWithChunkManager({\n      \'source\': source.addCounterpartRef(),\n    });\n    sharedObject.visibility.add(this.visibility);\n    this.registerDisposer(displayState.renderScaleHistogram.visibility.add(this.visibility));\n  }\n\n  get isTransparent() {\n    const {displayState} = this;\n    return displayState.objectAlpha.value < 1.0 || displayState.silhouetteRendering.value > 0;\n  }\n\n  get transparentPickEnabled() {\n    return this.displayState.transparentPickEnabled.value;\n  }\n\n  get gl() {\n    return this.chunkManager.chunkQueueManager.gl;\n  }\n\n  draw(\n      renderContext: PerspectiveViewRenderContext,\n      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {\n    if (!renderContext.emitColor && renderContext.alreadyEmittedPickID) {\n      // No need for a separate pick ID pass.\n      return;\n    }\n    const {gl, displayState, meshShaderManager} = this;\n    if (displayState.objectAlpha.value <= 0.0) {\n      // Skip drawing.\n      return;\n    }\n    const modelMatrix = update3dRenderLayerAttachment(\n        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,\n        attachment);\n    if (modelMatrix === undefined) return;\n    const {shader} = this.getShader(renderContext.emitter);\n    if (shader === null) return;\n    shader.bind();\n    meshShaderManager.beginLayer(gl, shader, renderContext, this.displayState);\n\n    const {renderScaleHistogram} = this.displayState;\n    if (renderContext.emitColor) {\n      renderScaleHistogram.begin(\n          this.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber);\n    }\n\n    mat3FromMat4(tempMat3, modelMatrix);\n    scaleMat3Output(\n        tempMat3, tempMat3,\n        renderContext.projectionParameters.displayDimensionRenderInfo.voxelPhysicalScales);\n    const scaleMultiplier = Math.pow(Math.abs(mat3.determinant(tempMat3)), 1 / 3);\n\n    const {chunks} = this.source;\n    const fragmentChunks = this.source.fragmentSource.chunks;\n\n    const {projectionParameters} = renderContext;\n\n    const modelViewProjection =\n        mat4.multiply(mat4.create(), projectionParameters.viewProjectionMat, modelMatrix);\n\n    const clippingPlanes = getFrustrumPlanes(new Float32Array(24), modelViewProjection);\n\n    const detailCutoff = this.displayState.renderScaleTarget.value;\n    const {fragmentRelativeVertices} = this.source.format;\n\n    meshShaderManager.beginModel(gl, shader, renderContext, modelMatrix);\n\n    let totalManifestChunks = 0;\n    let presentManifestChunks = 0;\n\n    forEachVisibleSegmentToDraw(\n        displayState, this, renderContext.emitColor,\n        renderContext.emitPickID ? renderContext.pickIDs : undefined,\n        (objectId, color, pickIndex) => {\n          const key = getObjectKey(objectId);\n          const manifestChunk = chunks.get(key);\n          ++totalManifestChunks;\n          if (manifestChunk === undefined) return;\n          ++presentManifestChunks;\n          const {manifest} = manifestChunk;\n          const {octree, chunkShape, chunkGridSpatialOrigin, vertexOffsets} = manifest;\n          if (DEBUG_MULTISCALE_FRAGMENTS) {\n            try {\n              validateOctree(octree);\n            } catch (e) {\n              console.log(`invalid octree for object=${objectId}: ${e.message}`)\n            }\n          }\n          if (renderContext.emitColor) {\n            meshShaderManager.setColor(gl, shader, color!);\n          }\n          if (renderContext.emitPickID) {\n            meshShaderManager.setPickID(gl, shader, pickIndex!);\n          }\n          getMultiscaleChunksToDraw(\n              manifest, modelViewProjection, clippingPlanes, detailCutoff,\n              projectionParameters.width, projectionParameters.height,\n              (lod, chunkIndex, renderScale) => {\n                const has = hasFragmentChunk(fragmentChunks, key, lod, chunkIndex);\n                if (renderContext.emitColor) {\n                  renderScaleHistogram.add(\n                      manifest.lodScales[lod] * scaleMultiplier, renderScale, has ? 1 : 0,\n                      has ? 0 : 1);\n                }\n                return has;\n              },\n              (lod, chunkIndex, subChunkBegin, subChunkEnd) => {\n                const fragmentKey = getMultiscaleFragmentKey(key, lod, chunkIndex);\n                const fragmentChunk = fragmentChunks.get(fragmentKey)!;\n                const x = octree[5 * chunkIndex], y = octree[5 * chunkIndex + 1],\n                      z = octree[5 * chunkIndex + 2];\n                const scale = 1 << lod;\n                if (fragmentRelativeVertices) {\n                  gl.uniform3f(\n                      shader.uniform(\'uFragmentOrigin\'),\n                      chunkGridSpatialOrigin[0] + (x * chunkShape[0]) * scale +\n                          vertexOffsets[lod * 3 + 0],\n                      chunkGridSpatialOrigin[1] + (y * chunkShape[1]) * scale +\n                          vertexOffsets[lod * 3 + 1],\n                      chunkGridSpatialOrigin[2] + (z * chunkShape[2]) * scale +\n                          vertexOffsets[lod * 3 + 2]);\n                  gl.uniform3f(\n                      shader.uniform(\'uFragmentShape\'), chunkShape[0] * scale,\n                      chunkShape[1] * scale, chunkShape[2] * scale);\n                }\n                if (DEBUG_MULTISCALE_FRAGMENTS) {\n                  const message = `lod=${lod}, chunkIndex=${chunkIndex}, subChunkBegin=${\n                      subChunkBegin}, subChunkEnd=${subChunkEnd}, uFragmentOrigin=${\n                          [chunkGridSpatialOrigin[0] + (x * chunkShape[0]) * scale +\n                               vertexOffsets[lod * 3 + 0],\n                           chunkGridSpatialOrigin[1] + (y * chunkShape[1]) * scale +\n                               vertexOffsets[lod * 3 + 1],\n                           chunkGridSpatialOrigin[2] + (z * chunkShape[2]) * scale +\n                               vertexOffsets[lod * 3 + 2]]}, uFragmentShape=${\n                          [chunkShape[0] * scale, chunkShape[1] * scale, chunkShape[2] * scale]}`;\n                  const pickIndex =\n                      renderContext.pickIDs.registerUint64(this, objectId, 1, message);\n                  if (renderContext.emitPickID) {\n                    meshShaderManager.setPickID(gl, shader, pickIndex!);\n                  }\n                }\n                meshShaderManager.drawMultiscaleFragment(\n                    gl,\n                    shader,\n                    fragmentChunk,\n                    subChunkBegin,\n                    subChunkEnd,\n                );\n              });\n        });\n    renderScaleHistogram.add(\n        Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, presentManifestChunks,\n        totalManifestChunks - presentManifestChunks);\n    meshShaderManager.endLayer(gl, shader);\n  }\n\n  isReady(\n      renderContext: PerspectiveViewReadyRenderContext,\n      attachment: VisibleLayerInfo<PerspectivePanel, ThreeDimensionalRenderLayerAttachmentState>) {\n    let {displayState} = this;\n    if (displayState.objectAlpha.value <= 0.0) {\n      // Skip drawing.\n      return true;\n    }\n    const modelMatrix = update3dRenderLayerAttachment(\n        displayState.transform.value, renderContext.projectionParameters.displayDimensionRenderInfo,\n        attachment);\n    if (modelMatrix === undefined) return false;\n    const {chunks} = this.source;\n    const fragmentChunks = this.source.fragmentSource.chunks;\n\n    const {projectionParameters} = renderContext;\n    const modelViewProjection =\n        mat4.multiply(mat4.create(), projectionParameters.viewProjectionMat, modelMatrix);\n\n    const clippingPlanes = getFrustrumPlanes(new Float32Array(24), modelViewProjection);\n\n    const detailCutoff = this.displayState.renderScaleTarget.value;\n\n    let hasAllChunks = true;\n\n    forEachVisibleSegment(displayState.segmentationGroupState.value, (objectId) => {\n      if (!hasAllChunks) return;\n      const key = getObjectKey(objectId);\n      const manifestChunk = chunks.get(key);\n      if (manifestChunk === undefined) {\n        hasAllChunks = false;\n        return;\n      }\n      const {manifest} = manifestChunk;\n      getMultiscaleChunksToDraw(\n          manifest, modelViewProjection, clippingPlanes, detailCutoff, projectionParameters.width,\n          projectionParameters.height, (lod, chunkIndex) => {\n            hasAllChunks = hasAllChunks && hasFragmentChunk(fragmentChunks, key, lod, chunkIndex);\n            return hasAllChunks;\n          }, () => {});\n    });\n    return hasAllChunks;\n  }\n\n  getObjectPosition(id: Uint64): Float32Array|undefined {\n    const transform = this.displayState.transform.value;\n    if (transform.error !== undefined) return undefined;\n    const chunk = this.source.chunks.get(getObjectKey(id));\n    if (chunk === undefined) return undefined;\n    const {manifest} = chunk;\n    const {clipLowerBound, clipUpperBound} = manifest;\n    const {rank} = transform;\n    // Center position, in model coordinates.\n    const modelCenter = new Float32Array(rank);\n    for (let i = 0; i < 3; ++i) {\n      modelCenter[i] = (clipLowerBound[i] + clipUpperBound[i]) / 2;\n    }\n    const layerCenter = new Float32Array(rank);\n    matrix.transformPoint(\n      layerCenter, transform.modelToRenderLayerTransform, rank + 1, modelCenter, rank);\n    return layerCenter;\n  }\n}\n\nexport class MultiscaleManifestChunk extends Chunk {\n  manifest: MultiscaleMeshManifest;\n  source: MultiscaleMeshSource;\n\n  constructor(source: MultiscaleMeshSource, x: any) {\n    super(source);\n    this.manifest = x[\'manifest\'];\n  }\n}\n\nexport class MultiscaleFragmentChunk extends Chunk {\n  meshData: EncodedMeshData&{subChunkOffsets: Uint32Array};\n  source: MultiscaleFragmentSource;\n  vertexBuffer: Buffer;\n  indexBuffer: Buffer;\n  normalBuffer: Buffer;\n\n  constructor(source: MultiscaleFragmentSource, x: any) {\n    super(source);\n    this.meshData = x;\n  }\n\n  copyToGPU(gl: GL) {\n    super.copyToGPU(gl);\n    copyMeshDataToGpu(gl, this);\n  }\n\n  freeGPUMemory(gl: GL) {\n    super.freeGPUMemory(gl);\n    freeGpuMeshData(this);\n  }\n}\n\nexport interface MultiscaleMeshSourceOptions extends MeshSourceOptions {\n  format: MultiscaleFragmentFormat;\n}\n\nexport class MultiscaleMeshSource extends ChunkSource {\n  OPTIONS: MultiscaleMeshSourceOptions;\n  fragmentSource = this.registerDisposer(new MultiscaleFragmentSource(this.chunkManager, this));\n  chunks: Map<string, MultiscaleManifestChunk>;\n  format: MultiscaleFragmentFormat;\n  constructor(chunkManager: Borrowed<ChunkManager>, options: MultiscaleMeshSourceOptions) {\n    super(chunkManager, options);\n    this.format = options.format;\n  }\n\n  static encodeOptions(options: MultiscaleMeshSourceOptions) {\n    return {format: options.format, ...super.encodeOptions(options)};\n  }\n\n  initializeCounterpart(rpc: RPC, options: any) {\n    this.fragmentSource.initializeCounterpart(this.chunkManager.rpc!, {});\n    options[\'fragmentSource\'] = this.fragmentSource.addCounterpartRef();\n    options[\'format\'] = this.format;\n    super.initializeCounterpart(rpc, options);\n  }\n  getChunk(x: any) {\n    return new MultiscaleManifestChunk(this, x);\n  }\n}\n\n@registerSharedObjectOwner(MULTISCALE_FRAGMENT_SOURCE_RPC_ID)\nexport class MultiscaleFragmentSource extends ChunkSource {\n  chunks: Map<string, MultiscaleFragmentChunk>;\n  get key() {\n    return this.meshSource.key;\n  }\n  constructor(chunkManager: ChunkManager, public meshSource: MultiscaleMeshSource) {\n    super(chunkManager);\n  }\n  getChunk(x: any) {\n    return new MultiscaleFragmentChunk(this, x);\n  }\n}\n', 'message': 'feat(mesh): encode normal vectors using 2xSnorm8 octahedron encoding\n\nThis reduces memory usage of normal vectors from 12 bytes to 2 bytes per vertex.\n', 'author_name': 'Jeremy Maitin-Shepard', 'lang': 'ts'}