Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/troika-3d-text/src/facade/Text3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const TEXT_MESH_PROPS = [
'sdfGlyphSize',
'unicodeFontsURL',
'gpuAccelerateSDF',
'usePremultipliedAlpha',
'debugSDF'
]

Expand Down
4 changes: 2 additions & 2 deletions packages/troika-3d/src/facade/World3DFacade.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WorldBaseFacade, utils } from 'troika-core'
import { WebGLRenderer, Raycaster, Color, Vector2, Vector3, LinearEncoding, NoToneMapping } from 'three'
import { WebGLRenderer, Raycaster, Color, Vector2, Vector3, LinearSRGBColorSpace, NoToneMapping } from 'three'
import Scene3DFacade from './Scene3DFacade.js'
import {PerspectiveCamera3DFacade} from './Camera3DFacade.js'
import {BoundingSphereOctree} from '../BoundingSphereOctree.js'
Expand Down Expand Up @@ -53,7 +53,7 @@ class World3DFacade extends WorldBaseFacade {
this._bgColor = backgroundColor
}

renderer.outputEncoding = this.outputEncoding || LinearEncoding
renderer.outputEncoding = this.outputEncoding || LinearSRGBColorSpace
renderer.toneMapping = this.toneMapping || NoToneMapping

// Update render canvas size
Expand Down
Binary file added packages/troika-examples/Geist-Regular.ttf
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/troika-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"react": "^16.14.0",
"react-dat-gui": "^4.0.0",
"react-dom": "^16.14.0",
"three": "^0.149.0",
"three": "^0.180.0",
"three-instanced-uniforms-mesh": "^0.52.4",
"three-line-2d": "^1.1.6",
"troika-2d": "^0.52.0",
Expand Down
218 changes: 139 additions & 79 deletions packages/troika-examples/text-batched/BatchedTextExample.jsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,148 @@
import React from "react";
import { BoxGeometry, Mesh, MeshBasicMaterial, MeshStandardMaterial } from "three";
import { Canvas3D } from "troika-3d";
import { Text3DFacade, BatchedText3DFacade } from "troika-3d-text";
import { FONTS } from '../text/TextExample'
import { Color } from "three/src/math/Color";
import { Object3DFacade } from "../../troika-3d/src/index";

export default function BatchedTextExample ({ stats, width, height }) {
const [texts, setTexts] = React.useState(randomizeText());
// Define the source text array once, so both 3D and SVG can use it
const words = [
"One", "Two", "Three", "Four", "Five",
"Six", "Seven", "Eight", "Nine", "Ten",
"Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty"
];

function randomizeText() {
const all = [
"One", "Two", "Three", "Four", "Five",
"Six", "Seven", "Eight", "Nine", "Ten",
"Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty"
]
const subset = all.slice(0, Math.max(16, Math.floor(Math.random() * all.length)))
return subset.map((text, i) => ({
facade: Text3DFacade,
text,
font: Object.values(FONTS)[i % Object.values(FONTS).length],
fontSize: randRange(0.05, 0.25),
x: randRange(-1, 1),
y: randRange(-1, 1),
z: randRange(-1, 1),
rotateZ: randRange(-0.01, 0.01),
anchorX: "50%",
anchorY: "50%",
color: randColor(),
// fillOpacity: Math.random() < 0.5 ? 0.1 : 1,
// strokeWidth: Math.random() < 0.5 ? '3%' : 0,
// strokeColor: randColor(),
outlineWidth: Math.random() < 0.3 ? '10%' : 0,
// outlineBlur: Math.random() < 0.3 ? '20%' : 0,
// outlineColor: randColor(),
// outlineOpacity: Math.random(),
// clipRect: Math.random() < 0.5 ? [0, 0, 999, 999] : null,
// curveRadius: Math.random() < 0.5 ? 0.1 : 0,
animation: {
from: { rotateY: 0 },
to: { rotateY: Math.PI * 2 },
duration: randRange(800, 3000),
iterations: Infinity
}
}))
}
// This function generates the properties for the 3D text
function generate3DTextLayout(premultiply) {
const startY = 1.5;
const yStep = 0.15;
const startFontSize = 0.3;
const fontSizeStep = 0.012;

return <div>
<Canvas3D
antialias
stats={stats}
width={width}
height={height}
onBackgroundClick={() => {
setTexts(randomizeText())
}}
camera={{
fov: 75,
aspect: width / height,
x: 0,
y: 0,
z: 2.5
}}
objects={[
{
facade: BatchedText3DFacade,
children: texts,
animation: {
from: { rotateY: 0 },
to: { rotateY: Math.PI * 2 },
duration: 10000,
iterations: Infinity
}
}
]}
/>
</div>;
return words.map((text, i) => ({
facade: Text3DFacade,
text,
font: '/Geist-Regular.ttf',
fontSize: Math.max(0.02, startFontSize - i * fontSizeStep),
anchorX: "50%",
anchorY: "50%",
x: premultiply ? 1 : -1,
y: startY - i * yStep,
z: 0,
// color: 0xc4c4c4,
// color: 0xd0d0d0,
color: 0x808080,
fillOpacity: 0.5,
// fillOpacity: 0.9375,
outlineWidth: '10%',
outlineColor: 0x121212,
outlineOpacity: 1,
usePremultipliedAlpha: !!premultiply,
}));
}

function randRange (min, max) {
return min + Math.random() * (max - min);
}
function randColor() {
return new Color().setHSL(Math.random(), 1, 0.5)
class BackgroundTest extends Object3DFacade {
initThreeObject() {
return new Mesh(
new BoxGeometry(10,1,1),
new MeshBasicMaterial({
color: 0xff0000,
})
)
}
}
// function randFromArray(array) {
// return array[Math.floor(Math.random() * array.length)];
// }


export default function BatchedTextExample({ stats, width, height }) {
const [texts3D,setTexts] = React.useState(generate3DTextLayout());
const [premultipliedTexts3D,setPremultipliedTexts] = React.useState(generate3DTextLayout(true));

console.log('texts3D', texts3D)

// --- SVG Text Logic ---
// Define layout parameters suitable for SVG pixels
const svgStartY = 60; // Starting Y position in pixels
const svgYStep = 20; // Gap between lines in pixels
const svgStartFontSize = 32; // Starting font size in pixels
const svgFontSizeStep = 1; // How much to decrease font size (px)

// Generate the properties for the SVG text elements
const svgTexts = words.map((text, i) => ({
text,
y: svgStartY + i * svgYStep,
fontSize: Math.max(8, svgStartFontSize - i * svgFontSizeStep),
}));

return (
// Main container using Flexbox for a side-by-side layout
<div style={{ display: 'flex', width, height, backgroundColor: '#ffffff' }}>

{/* Add a style tag to load the custom font for the SVG */}
<style>{`
@font-face {
font-family: 'Geist';
src: url('/Geist-Regular.ttf') format('truetype');
}
`}</style>

{/* 1. 3D Canvas Container (takes up the left 50%) */}
<div style={{ flex: 1, minWidth: 0 }}>
<Canvas3D
antialias
stats={stats}
width={width / 2} // Canvas now takes half the total width
height={height}
continuousRender={true}
onBackgroundClick={() => {
setTexts(generate3DTextLayout());
setPremultipliedTexts(generate3DTextLayout(true));
}}
camera={{ fov: 75, aspect: (width / 2) / height, x: 0, y: 0, z: 2.5 }}
objects={[
{
facade: BatchedText3DFacade,
children: texts3D,
},
{
facade: BatchedText3DFacade,
children: premultipliedTexts3D,
},
{
key: 'jnjn',
facade: BackgroundTest,
receiveShadow: true,
scale: 1,
z:-1,
y:-1.4,
}
]}
/>
</div>

{/* 2. SVG Container (takes up the right 50%) */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' }}>
<svg width="80%" height="90%" viewBox="0 0 300 500">
<rect x="0" y="67.5%" width="100%" height="25%" fill="#ff0000" stroke="#ff0000" strokeWidth="2" />
{svgTexts.map((item, i) => (
<text
key={i}
x="50%" // Center text horizontally
y={item.y}
fontFamily="Geist, sans-serif" // Apply the custom font
fontSize={item.fontSize}
textAnchor="middle" // SVG property to honor x="50%" as center
fill="#808080"
fillOpacity="0.5"
paint-order="stroke"
stroke="#121212"
strokeOpacity="1"
strokeWidth={item.fontSize * 0.2} // 10% of font size
>
{item.text}
</text>
))}
</svg>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions packages/troika-three-text/src/BatchedText.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Data texture packing strategy:
30: uTroikaStrokeOpacity

# Outline:
27: usePremultipliedAlpha (0/1)
28-29: uTroikaPositionOffset
30: uTroikaEdgeOffset
31: uTroikaBlurRadius
Expand Down Expand Up @@ -210,6 +211,7 @@ export class BatchedText extends Text {
uTroikaStrokeOpacity,
uTroikaFillOpacity,
uTroikaCurveRadius,
uUsePremultipliedAlpha
} = material.uniforms;

// Total bounds for uv
Expand All @@ -234,6 +236,7 @@ export class BatchedText extends Text {

// Curve radius
setTexData(startIndex + 26, uTroikaCurveRadius.value)
setTexData(startIndex + 27, uUsePremultipliedAlpha.value);

if (isOutline) {
// Outline properties
Expand Down Expand Up @@ -415,6 +418,7 @@ function createBatchedTextMaterial (baseMaterial) {
'uTroikaStrokeOpacity',
'uTroikaFillOpacity',
'uTroikaCurveRadius',
'uUsePremultipliedAlpha',
'diffuse'
]
varyingUniforms.forEach(uniformName => {
Expand Down Expand Up @@ -444,6 +448,7 @@ function createBatchedTextMaterial (baseMaterial) {
diffuse = troikaFloatToColor(data.x);
uTroikaFillOpacity = data.y;
uTroikaCurveRadius = data.z;
uUsePremultipliedAlpha = data.w;

data = troikaBatchTexel(7.0);
if (uTroikaIsOutline) {
Expand Down
21 changes: 20 additions & 1 deletion packages/troika-three-text/src/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
PlaneGeometry,
Vector3,
Vector2,
CustomBlending,
OneFactor,
OneMinusSrcAlphaFactor,
NormalBlending,
} from 'three'
import { GlyphsGeometry } from './GlyphsGeometry.js'
import { createTextDerivedMaterial } from './TextDerivedMaterial.js'
Expand Down Expand Up @@ -398,6 +402,12 @@ class Text extends Mesh {
*/
this.gpuAccelerateSDF = true

/**
* @member {boolean} usePremultipliedAlpha
* TODO
**/
this.usePremultipliedAlpha = true

this.debugSDF = false
}

Expand Down Expand Up @@ -638,7 +648,15 @@ class Text extends Mesh {
}
fillOpacity = this.fillOpacity
}

if(this.usePremultipliedAlpha){
material.premultipliedAlpha = true
material.blending = CustomBlending;
material.blendSrc = OneFactor; // Use the source color as-is
material.blendDst = OneMinusSrcAlphaFactor;
}else{
material.blending = NormalBlending
}

uniforms.uTroikaEdgeOffset.value = distanceOffset
uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY)
uniforms.uTroikaBlurRadius.value = blurRadius
Expand All @@ -663,6 +681,7 @@ class Text extends Mesh {
this.geometry.applyClipRect(uniforms.uTroikaClipRect.value)
}
uniforms.uTroikaSDFDebug.value = !!this.debugSDF
uniforms.uUsePremultipliedAlpha.value = this.usePremultipliedAlpha ? 1 : 0
material.polygonOffset = !!this.depthOffset
material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0

Expand Down
10 changes: 7 additions & 3 deletions packages/troika-three-text/src/TextDerivedMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ uniform vec3 uTroikaStrokeColor;
uniform float uTroikaStrokeWidth;
uniform float uTroikaStrokeOpacity;
uniform bool uTroikaSDFDebug;
uniform float uUsePremultipliedAlpha;
varying vec2 vTroikaGlyphUV;
varying vec4 vTroikaTextureUVBounds;
varying float vTroikaTextureChannel;
Expand Down Expand Up @@ -195,9 +196,11 @@ gl_FragColor = mix(fillRGBA, strokeRGBA, smoothstep(
));
gl_FragColor.a *= edgeAlpha;
#endif

if(uUsePremultipliedAlpha == 1.0){
gl_FragColor.rgb *= gl_FragColor.a;
}
if (edgeAlpha == 0.0) {
discard;
discard;
}
`

Expand Down Expand Up @@ -228,7 +231,8 @@ export function createTextDerivedMaterial(baseMaterial) {
uTroikaStrokeOpacity: {value: 1},
uTroikaOrient: {value: new Matrix3()},
uTroikaUseGlyphColors: {value: true},
uTroikaSDFDebug: {value: false}
uTroikaSDFDebug: {value: false},
uUsePremultipliedAlpha: {value: 0}
},
vertexDefs: VERTEX_DEFS,
vertexTransform: VERTEX_TRANSFORM,
Expand Down