소스 검색

basic text rendering: render-object, builder, shader

Alexander Rose 6 년 전
부모
커밋
ce331b3a15

+ 136 - 0
src/mol-geo/geometry/text/text-builder.ts

@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { ValueCell } from 'mol-util/value-cell'
+import { ChunkedArray } from 'mol-data/util';
+import { Text } from './text';
+import { getFontAtlas } from './font-atlas';
+
+const quadIndices = new Uint16Array([
+    0, 1, 2,
+    1, 3, 2
+])
+
+export interface TextBuilder {
+    add(str: string, x: number, y: number, z: number, group: number): void
+    getText(): Text
+}
+
+export namespace TextBuilder {
+    export function create(props: Partial<PD.Values<Text.Params>> = {}, initialCount = 2048, chunkSize = 1024, text?: Text): TextBuilder {
+        const centers = ChunkedArray.create(Float32Array, 3, chunkSize, text ? text.centerBuffer.ref.value : initialCount);
+        const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, text ? text.mappingBuffer.ref.value : initialCount);
+        const indices = ChunkedArray.create(Uint32Array, 3, chunkSize, text ? text.indexBuffer.ref.value : initialCount);
+        const groups = ChunkedArray.create(Float32Array, 1, chunkSize, text ? text.groupBuffer.ref.value : initialCount);
+        const tcoords = ChunkedArray.create(Float32Array, 2, chunkSize, text ? text.tcoordBuffer.ref.value : initialCount);
+
+        const p = { ...PD.getDefaultValues(Text.Params), ...props }
+        const { attachment, background, backgroundMargin } = p
+
+        const fontAtlas = getFontAtlas(p)
+        const { lineHeight } = fontAtlas
+
+        const margin = (lineHeight * backgroundMargin * 0.1) - 10
+        const outline = fontAtlas.buffer
+        console.log('margin', margin)
+
+        return {
+            add: (str: string, x: number, y: number, z: number, group: number) => {
+                let xadvance = 0
+                const nChar = str.length
+
+                // calculate width
+                for (let iChar = 0; iChar < nChar; ++iChar) {
+                    const c = fontAtlas.get(str[iChar])
+                    xadvance += c.w - 2 * outline
+                }
+
+                // attachment
+                let yShift: number, xShift: number
+                if (attachment.startsWith('top')) {
+                    yShift = lineHeight / 1.25
+                } else if (attachment.startsWith('middle')) {
+                    yShift = lineHeight / 2.5
+                } else {
+                    yShift = 0  // "bottom"
+                }
+                if (attachment.endsWith('right')) {
+                    xShift = xadvance
+                } else if (attachment.endsWith('center')) {
+                    xShift = xadvance / 2
+                } else {
+                    xShift = 0  // "left"
+                }
+                xShift += outline
+                yShift += outline
+
+                // background
+                if (background) {
+                    ChunkedArray.add2(mappings, -lineHeight / 6 - xShift - margin, lineHeight - yShift + margin)
+                    ChunkedArray.add2(mappings, -lineHeight / 6 - xShift - margin, 0 - yShift - margin)
+                    ChunkedArray.add2(mappings, xadvance + lineHeight / 6 - xShift + 2 * outline + margin, lineHeight - yShift + margin)
+                    ChunkedArray.add2(mappings, xadvance + lineHeight / 6 - xShift + 2 * outline + margin, 0 - yShift - margin)
+
+                    const offset = centers.elementCount
+                    for (let i = 0; i < 4; ++i) {
+                        ChunkedArray.add2(tcoords, 0, 10)
+                        ChunkedArray.add3(centers, x, y, z);
+                        ChunkedArray.add(groups, group);
+                    }
+                    ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2])
+                    ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5])
+                }
+
+                xadvance = 0
+
+                for (let iChar = 0; iChar < nChar; ++iChar) {
+                    const c = fontAtlas.get(str[iChar])
+
+                    ChunkedArray.add2(mappings, xadvance - xShift, c.h - yShift) // top left
+                    ChunkedArray.add2(mappings, xadvance - xShift, 0 - yShift) // bottom left
+                    ChunkedArray.add2(mappings, xadvance + c.w - xShift, c.h - yShift) // top right
+                    ChunkedArray.add2(mappings, xadvance + c.w - xShift, 0 - yShift) // bottom right
+
+                    const texWidth = fontAtlas.texture.width
+                    const texHeight = fontAtlas.texture.height
+
+                    ChunkedArray.add2(tcoords, c.x / texWidth, c.y / texHeight) // top left
+                    ChunkedArray.add2(tcoords, c.x / texWidth, (c.y + c.h) / texHeight) // bottom left
+                    ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, c.y / texHeight) // top right
+                    ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, (c.y + c.h) / texHeight) // bottom right
+
+                    xadvance += c.w - 2 * outline
+
+                    const offset = centers.elementCount
+                    for (let i = 0; i < 4; ++i) {
+                        ChunkedArray.add3(centers, x, y, z);
+                        ChunkedArray.add(groups, group);
+                    }
+                    ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2])
+                    ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5])
+                }
+            },
+            getText: () => {
+                const cb = ChunkedArray.compact(centers, true) as Float32Array
+                const mb = ChunkedArray.compact(mappings, true) as Float32Array
+                const ib = ChunkedArray.compact(indices, true) as Uint32Array
+                const gb = ChunkedArray.compact(groups, true) as Float32Array
+                const tb = ChunkedArray.compact(tcoords, true) as Float32Array
+                return {
+                    kind: 'text',
+                    charCount: centers.elementCount / 4,
+                    fontAtlas,
+                    centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
+                    mappingBuffer: text ? ValueCell.update(text.centerBuffer, mb) : ValueCell.create(mb),
+                    indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
+                    groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
+                    tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb),
+                }
+            }
+        }
+    }
+}

+ 181 - 0
src/mol-geo/geometry/text/text.ts

@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { ValueCell } from 'mol-util';
+import { Geometry } from '../geometry';
+import { LocationIterator } from 'mol-geo/util/location-iterator';
+import { TransformData, createIdentityTransform } from '../transform-data';
+import { Theme } from 'mol-theme/theme';
+import { createColors } from '../color-data';
+import { createSizes, getMaxSize } from '../size-data';
+import { createMarkers } from '../marker-data';
+import { ColorNames } from 'mol-util/color/tables';
+import { NullLocation } from 'mol-model/location';
+import { UniformColorTheme } from 'mol-theme/color/uniform';
+import { UniformSizeTheme } from 'mol-theme/size/uniform';
+import { Sphere3D } from 'mol-math/geometry';
+import { calculateBoundingSphere } from 'mol-gl/renderable/util';
+import { TextValues } from 'mol-gl/renderable/text';
+import { Color } from 'mol-util/color';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { FontAtlas, getFontAtlas, FontAtlasParams } from './font-atlas';
+import { RenderableState } from 'mol-gl/renderable';
+import { clamp } from 'mol-math/interpolate';
+
+type TextAttachment = 'bottom-left' | 'bottom-center' | 'bottom-right' | 'middle-left' | 'middle-center' | 'middle-right' | 'top-left' | 'top-center' | 'top-right'
+
+/** Text */
+export interface Text {
+    readonly kind: 'text',
+
+    /** Number of characters in the text */
+    readonly charCount: number,
+    /** Font Atlas */
+    readonly fontAtlas: FontAtlas,
+
+    /** Center buffer as array of xyz values wrapped in a value cell */
+    readonly centerBuffer: ValueCell<Float32Array>,
+    /** Mapping buffer as array of xy values wrapped in a value cell */
+    readonly mappingBuffer: ValueCell<Float32Array>,
+    /** Index buffer as array of center index triplets wrapped in a value cell */
+    readonly indexBuffer: ValueCell<Uint32Array>,
+    /** Group buffer as array of group ids for each vertex wrapped in a value cell */
+    readonly groupBuffer: ValueCell<Float32Array>,
+    /** Texture coordinates buffer as array of uv values wrapped in a value cell */
+    readonly tcoordBuffer: ValueCell<Float32Array>,
+}
+
+export namespace Text {
+    export function createEmpty(text?: Text): Text {
+        const cb = text ? text.centerBuffer.ref.value : new Float32Array(0)
+        const mb = text ? text.mappingBuffer.ref.value : new Float32Array(0)
+        const ib = text ? text.indexBuffer.ref.value : new Uint32Array(0)
+        const gb = text ? text.groupBuffer.ref.value : new Float32Array(0)
+        const tb = text ? text.tcoordBuffer.ref.value : new Float32Array(0)
+        return {
+            kind: 'text',
+            charCount: 0,
+            fontAtlas: getFontAtlas({}),
+            centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
+            mappingBuffer: text ? ValueCell.update(text.mappingBuffer, mb) : ValueCell.create(mb),
+            indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
+            groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
+            tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb)
+        }
+    }
+
+    export const Params = {
+        ...Geometry.Params,
+        ...FontAtlasParams,
+        sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+
+        borderWidth: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
+        borderColor: PD.Color(ColorNames.grey),
+        offsetX: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        offsetY: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        offsetZ: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        background: PD.Boolean(false),
+        backgroundMargin: PD.Numeric(0.2, { min: 0, max: 10, step: 0.1 }),
+        backgroundColor: PD.Color(ColorNames.grey),
+        backgroundOpacity: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
+
+        attachment: PD.Select('normal', [['bottom-left', 'bottom-left'], ['bottom-center', 'bottom-center'], ['bottom-right', 'bottom-right'], ['middle-left', 'middle-left'], ['top-left', 'top-left'], ['top-center', 'top-center'], ['top-right', 'top-right']] as [TextAttachment, string][]),
+    }
+    export type Params = typeof Params
+
+    export function createValues(text: Text, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextValues {
+        const { instanceCount, groupCount } = locationIt
+        if (instanceCount !== transform.instanceCount.ref.value) {
+            throw new Error('instanceCount values in TransformData and LocationIterator differ')
+        }
+
+        const color = createColors(locationIt, theme.color)
+        const size = createSizes(locationIt, theme.size)
+        const marker = createMarkers(instanceCount * groupCount)
+
+        const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount }
+
+        const padding = getMaxSize(size)
+        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
+            text.centerBuffer.ref.value, text.charCount * 4,
+            transform.aTransform.ref.value, instanceCount, padding
+        )
+
+        console.log(props.sizeFactor, text.fontAtlas.lineHeight, props.fontSize)
+
+        return {
+            aPosition: text.centerBuffer,
+            aMapping: text.mappingBuffer,
+            aGroup: text.groupBuffer,
+            elements: text.indexBuffer,
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            ...color,
+            ...size,
+            ...marker,
+            ...transform,
+
+            aTexCoord: text.tcoordBuffer,
+            tFont: ValueCell.create(text.fontAtlas.texture),
+            padding: ValueCell.create(padding),
+
+            ...Geometry.createValues(props, counts),
+            uSizeFactor: ValueCell.create(props.sizeFactor / text.fontAtlas.lineHeight),
+
+            uBorderWidth: ValueCell.create(clamp(props.borderWidth / 2, 0, 0.5)),
+            uBorderColor: ValueCell.create(Color.toArrayNormalized(props.borderColor, Vec3.zero(), 0)),
+            uOffsetX: ValueCell.create(props.offsetX),
+            uOffsetY: ValueCell.create(props.offsetY),
+            uOffsetZ: ValueCell.create(props.offsetZ),
+            uBackgroundColor: ValueCell.create(Color.toArrayNormalized(props.backgroundColor, Vec3.zero(), 0)),
+            uBackgroundOpacity: ValueCell.create(props.backgroundOpacity),
+        }
+    }
+
+    export function createValuesSimple(text: Text, props: Partial<PD.Values<Params>>, colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData): TextValues {
+
+        if (!transform) transform = createIdentityTransform()
+        const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, () => NullLocation, false, () => false)
+        const theme: Theme = {
+            color: UniformColorTheme({}, { value: colorValue}),
+            size: UniformSizeTheme({}, { value: sizeValue})
+        }
+        const p = { ...PD.getDefaultValues(Params), ...props }
+
+        return createValues(text, transform, locationIterator, theme, p)
+    }
+
+    export function updateValues(values: TextValues, props: PD.Values<Params>) {
+        Geometry.updateValues(values, props)
+        ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor)
+    }
+
+    export function updateBoundingSphere(values: TextValues, text: Text) {
+        const padding = getMaxSize(values)
+        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
+            values.aPosition.ref.value, text.charCount * 4,
+            values.aTransform.ref.value, values.instanceCount.ref.value, padding
+        )
+        if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
+            ValueCell.update(values.boundingSphere, boundingSphere)
+        }
+        if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
+            ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere)
+        }
+    }
+
+    export function createRenderableState(props: PD.Values<Params>): RenderableState {
+        const state = Geometry.createRenderableState(props)
+        updateRenderableState(state, props)
+        return state
+    }
+
+    export function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
+        Geometry.updateRenderableState(state, props)
+        state.opaque = false
+    }
+}

+ 7 - 1
src/mol-gl/render-object.ts

@@ -14,6 +14,7 @@ import { MeshValues, MeshRenderable } from './renderable/mesh';
 import { PointsValues, PointsRenderable } from './renderable/points';
 import { LinesValues, LinesRenderable } from './renderable/lines';
 import { SpheresValues, SpheresRenderable } from './renderable/spheres';
+import { TextValues, TextRenderable } from './renderable/text';
 
 const getNextId = idFactory(0, 0x7FFFFFFF)
 
@@ -21,6 +22,7 @@ export interface BaseRenderObject { id: number, type: string, values: Renderable
 export interface MeshRenderObject extends BaseRenderObject { type: 'mesh', values: MeshValues }
 export interface PointsRenderObject extends BaseRenderObject { type: 'points', values: PointsValues }
 export interface SpheresRenderObject extends BaseRenderObject { type: 'spheres', values: SpheresValues }
+export interface TextRenderObject extends BaseRenderObject { type: 'text', values: TextValues }
 export interface LinesRenderObject extends BaseRenderObject { type: 'lines', values: LinesValues }
 export interface DirectVolumeRenderObject extends BaseRenderObject { type: 'direct-volume', values: DirectVolumeValues }
 
@@ -28,7 +30,7 @@ export interface GaussianDensityRenderObject extends BaseRenderObject { type: 'g
 
 //
 
-export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | LinesRenderObject | DirectVolumeRenderObject
+export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject
 
 export type ComputeRenderObject = GaussianDensityRenderObject
 
@@ -45,6 +47,9 @@ export function createPointsRenderObject(values: PointsValues, state: Renderable
 export function createSpheresRenderObject(values: SpheresValues, state: RenderableState): SpheresRenderObject {
     return { id: getNextId(), type: 'spheres', values, state }
 }
+export function createTextRenderObject(values: TextValues, state: RenderableState): TextRenderObject {
+    return { id: getNextId(), type: 'text', values, state }
+}
 export function createLinesRenderObject(values: LinesValues, state: RenderableState): LinesRenderObject {
     return { id: getNextId(), type: 'lines', values, state }
 }
@@ -61,6 +66,7 @@ export function createRenderable(ctx: WebGLContext, o: RenderObject): Renderable
         case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state)
         case 'points': return PointsRenderable(ctx, o.id, o.values, o.state)
         case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state)
+        case 'text': return TextRenderable(ctx, o.id, o.values, o.state)
         case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state)
         case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state)
 

+ 1 - 0
src/mol-gl/renderable/schema.ts

@@ -148,6 +148,7 @@ export const GlobalUniformSchema = {
     uViewportHeight: UniformSpec('f'),
     uViewport: UniformSpec('v4'),
 
+    uCameraPosition: UniformSpec('v3'),
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),

+ 45 - 0
src/mol-gl/renderable/text.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Renderable, RenderableState, createRenderable } from '../renderable'
+import { WebGLContext } from '../webgl/context';
+import { createRenderItem } from '../webgl/render-item';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, Values, InternalSchema, SizeSchema, InternalValues, TextureSpec, ElementsSpec, ValueSpec } from './schema';
+import { TextShaderCode } from '../shader-code';
+import { ValueCell } from 'mol-util';
+
+export const TextSchema = {
+    ...BaseSchema,
+    ...SizeSchema,
+    aPosition: AttributeSpec('float32', 3, 0),
+    aMapping: AttributeSpec('float32', 2, 0),
+    elements: ElementsSpec('uint32'),
+
+    aTexCoord: AttributeSpec('float32', 2, 0),
+    tFont: TextureSpec('image-uint8', 'alpha', 'ubyte', 'linear'),
+    padding: ValueSpec('number'),
+
+    uBorderWidth: UniformSpec('f'),
+    uBorderColor: UniformSpec('v3'),
+    uOffsetX: UniformSpec('f'),
+    uOffsetY: UniformSpec('f'),
+    uOffsetZ: UniformSpec('f'),
+    uBackgroundColor: UniformSpec('v3'),
+    uBackgroundOpacity: UniformSpec('f'),
+}
+export type TextSchema = typeof TextSchema
+export type TextValues = Values<TextSchema>
+
+export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState): Renderable<TextValues> {
+    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextSchema }
+    const internalValues: InternalValues = {
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
+    }
+    const shaderCode = TextShaderCode
+    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    return createRenderable(renderItem, values, state);
+}

+ 2 - 0
src/mol-gl/renderer.ts

@@ -99,6 +99,7 @@ namespace Renderer {
             uLightColor: ValueCell.create(lightColor),
             uLightAmbient: ValueCell.create(lightAmbient),
 
+            uCameraPosition: ValueCell.create(Vec3.clone(camera.state.position)),
             uFogNear: ValueCell.create(camera.state.fogNear),
             uFogFar: ValueCell.create(camera.state.fogFar),
             uFogColor: ValueCell.create(fogColor),
@@ -160,6 +161,7 @@ namespace Renderer {
             ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection))
             ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection))
 
+            ValueCell.update(globalUniforms.uCameraPosition, camera.state.position)
             ValueCell.update(globalUniforms.uFogFar, camera.state.fogFar)
             ValueCell.update(globalUniforms.uFogNear, camera.state.fogNear)
 

+ 6 - 0
src/mol-gl/shader-code.ts

@@ -42,6 +42,12 @@ export const SpheresShaderCode = ShaderCode(
     { standardDerivatives: false, fragDepth: true }
 )
 
+export const TextShaderCode = ShaderCode(
+    require('mol-gl/shader/text.vert'),
+    require('mol-gl/shader/text.frag'),
+    { standardDerivatives: true, fragDepth: false }
+)
+
 export const LinesShaderCode = ShaderCode(
     require('mol-gl/shader/lines.vert'),
     require('mol-gl/shader/lines.frag'),

+ 1 - 0
src/mol-gl/shader/chunks/common-vert-params.glsl

@@ -1,4 +1,5 @@
 uniform mat4 uProjection, uModel, uView;
+uniform vec3 uCameraPosition;
 
 uniform int uObjectId;
 uniform int uInstanceCount;

+ 68 - 0
src/mol-gl/shader/text.frag

@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+precision highp int;
+
+#pragma glslify: import('./chunks/common-frag-params.glsl')
+#pragma glslify: import('./chunks/color-frag-params.glsl')
+
+uniform sampler2D tFont;
+
+uniform vec3 uBorderColor;
+uniform float uBorderWidth;
+uniform vec3 uBackgroundColor;
+uniform float uBackgroundOpacity;
+
+varying vec2 vTexCoord;
+
+const float smoothness = 32.0;
+const float gamma = 2.2;
+
+void main2(){
+    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
+}
+
+void main(){
+    #pragma glslify: import('./chunks/assign-material-color.glsl')
+
+    if (vTexCoord.x > 1.0) {
+        gl_FragColor = vec4(uBackgroundColor, uBackgroundOpacity);
+    } else {
+        // TODO nicer border
+
+        // retrieve signed distance
+        float sdf = texture2D(tFont, vTexCoord).a + uBorderWidth;
+
+        // perform adaptive anti-aliasing of the edges
+        float w = clamp(
+            smoothness * (abs(dFdx(vTexCoord.x)) + abs(dFdy(vTexCoord.y))),
+            0.0,
+            0.5
+        );
+        float a = smoothstep(0.5 - w, 0.5 + w, sdf);
+
+        // gamma correction for linear attenuation
+        a = pow(a, 1.0 / gamma);
+
+        if (a < 0.5) discard;
+        material.a *= a;
+
+        if (uBorderWidth > 0.0 && sdf < (0.5 + uBorderWidth)) {
+            material.xyz = uBorderColor;
+        }
+
+        gl_FragColor = material;
+    }
+
+    #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
+        if (uAlpha < uPickingAlphaThreshold)
+            discard; // ignore so the element below can be picked
+    #else
+        #pragma glslify: import('./chunks/apply-marker-color.glsl')
+        #pragma glslify: import('./chunks/apply-fog.glsl')
+    #endif
+}

+ 75 - 0
src/mol-gl/shader/text.vert

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+precision highp int;
+
+#pragma glslify: import('./chunks/common-vert-params.glsl')
+#pragma glslify: import('./chunks/color-vert-params.glsl')
+#pragma glslify: import('./chunks/size-vert-params.glsl')
+
+uniform mat4 uModelView;
+
+attribute vec3 aPosition;
+attribute vec2 aMapping;
+attribute vec2 aTexCoord;
+attribute mat4 aTransform;
+attribute float aInstance;
+attribute float aGroup;
+
+uniform float uOffsetX;
+uniform float uOffsetY;
+uniform float uOffsetZ;
+
+// uniform bool ortho;
+uniform float uPixelRatio;
+uniform float uViewportHeight;
+
+varying vec2 vTexCoord;
+
+#pragma glslify: matrixScale = require(./utils/matrix-scale.glsl)
+
+void main(void){
+    #pragma glslify: import('./chunks/assign-color-varying.glsl')
+    #pragma glslify: import('./chunks/assign-marker-varying.glsl')
+    #pragma glslify: import('./chunks/assign-size.glsl')
+
+    vTexCoord = aTexCoord;
+
+    float scale = matrixScale(uModelView);
+
+    float offsetX = uOffsetX * scale;
+    float offsetY = uOffsetY * scale;
+    float offsetZ = uOffsetZ * scale;
+    if (vTexCoord.x == 10.0) {
+        offsetZ -= 0.001;
+    }
+
+    vec4 mvPosition = uModelView * aTransform * vec4(aPosition, 1.0);
+
+    // #ifdef FIXED_SIZE
+    //     if (ortho) {
+    //         scale /= pixelRatio * ((uViewportHeight / 2.0) / -uCameraPosition.z) * 0.1;
+    //     } else {
+    //         scale /= pixelRatio * ((uViewportHeight / 2.0) / -mvPosition.z) * 0.1;
+    //     }
+    // #endif
+
+    vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
+    mvCorner.xy += aMapping * size * scale;
+    mvCorner.x += offsetX;
+    mvCorner.y += offsetY;
+    // if(ortho){
+    //     mvCorner.xyz += normalize(-uCameraPosition) * offsetZ;
+    // } else {
+    //     mvCorner.xyz += normalize(-mvCorner.xyz) * offsetZ;
+    // }
+    mvCorner.xyz += normalize(-mvCorner.xyz) * offsetZ;
+
+    gl_Position = uProjection * mvCorner;
+
+    vViewPosition = -mvCorner.xyz;
+}

+ 71 - 0
src/tests/browser/render-text.ts

@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import './index.html'
+import { Canvas3D } from 'mol-canvas3d/canvas3d';
+import { Geometry } from 'mol-geo/geometry/geometry';
+import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
+import { Text } from 'mol-geo/geometry/text/text';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Color } from 'mol-util/color';
+import { createTextRenderObject, createSpheresRenderObject } from 'mol-gl/render-object';
+import { Representation } from 'mol-repr/representation';
+import { SpheresBuilder } from 'mol-geo/geometry/spheres/spheres-builder';
+import { Spheres } from 'mol-geo/geometry/spheres/spheres';
+
+const parent = document.getElementById('app')!
+parent.style.width = '100%'
+parent.style.height = '100%'
+
+const canvas = document.createElement('canvas')
+canvas.style.width = '100%'
+canvas.style.height = '100%'
+parent.appendChild(canvas)
+
+const canvas3d = Canvas3D.create(canvas, parent)
+canvas3d.animate()
+
+function textRepr() {
+    const props: PD.Values<Text.Params> = {
+        ...PD.getDefaultValues(Text.Params),
+        attachment: 'middle-center',
+        fontSize: 96,
+        fontWeight: 'bold',
+    }
+
+    const textBuilder = TextBuilder.create(props, 1, 1)
+    textBuilder.add('Hello world', 0, 0, 0, 0)
+    textBuilder.add('Добрый день', 0, 1, 0, 0)
+    textBuilder.add('美好的一天', 0, 2, 0, 0)
+    textBuilder.add('¿Cómo estás?', 0, -1, 0, 0)
+    textBuilder.add('αβγ Å', 0, -2, 0, 0)
+    const text = textBuilder.getText()
+
+    const values = Text.createValuesSimple(text, props, Color(0xFFDD00), 1)
+    const state = Text.createRenderableState(props)
+    const renderObject = createTextRenderObject(values, state)
+    console.log('text', renderObject)
+    const repr = Representation.fromRenderObject('text', renderObject)
+    return repr
+}
+
+function spheresRepr() {
+    const spheresBuilder = SpheresBuilder.create(2, 1)
+    spheresBuilder.add(5, 0, 0, 0)
+    spheresBuilder.add(-4, 1, 0, 0)
+    const spheres = spheresBuilder.getSpheres()
+
+    const values = Spheres.createValuesSimple(spheres, {}, Color(0xFF0000), 1)
+    const state = Geometry.createRenderableState()
+    const renderObject = createSpheresRenderObject(values, state)
+    console.log('spheres', renderObject)
+    const repr = Representation.fromRenderObject('spheres', renderObject)
+    return repr
+}
+
+canvas3d.add(textRepr())
+canvas3d.add(spheresRepr())
+canvas3d.resetCamera()