Codrops 원본
Rotating Twisted 3D Typography with Three.js and Shaders
텍스트 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
css/base.css
*,
*::after,
*::before {
box-sizing: border-box;
}
:root {
font-size: 12px;
--color-text: #c9c9c9;
--color-bg: #121212;
--color-link: #a2939c;
--color-link-hover: #fff;
--padding-page: 1.5rem;
touch-action: pan-x pan-y;
text-transform: uppercase;
}
body {
margin: 0;
color: var(--color-text);
background-color: var(--color-bg);
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Page Loader */
.js .loading::before,
.js .loading::after {
content: '';
position: fixed;
z-index: 1000;
}
.js .loading::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--color-bg);
}
.js .loading::after {
top: 50%;
left: 50%;
width: 60px;
height: 60px;
margin: -30px 0 0 -30px;
border-radius: 50%;
opacity: 0.4;
background: var(--color-link);
animation: loaderAnim 0.7s linear infinite alternate forwards;
}
@keyframes loaderAnim {
to {
opacity: 1;
transform: scale3d(0.5,0.5,1);
}
}
a {
text-decoration: none;
color: var(--color-link);
outline: none;
cursor: pointer;
}
a:hover {
color: var(--color-link-hover);
outline: none;
}
/* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */
a:focus {
/* Provide a fallback style for browsers
that don't support :focus-visible */
outline: none;
background: lightgrey;
}
a:focus:not(:focus-visible) {
/* Remove the focus indicator on mouse-focus for browsers
that do support :focus-visible */
background: transparent;
}
a:focus-visible {
/* Draw a very noticeable focus style for
keyboard-focus on browsers that do support
:focus-visible */
outline: 2px solid red;
background: transparent;
}
.unbutton {
background: none;
border: 0;
padding: 0;
margin: 0;
font: inherit;
cursor: pointer;
}
.unbutton:focus {
outline: none;
}
body #cdawrap {
justify-self: start;
}
.frame {
position: fixed;
color: var(--color-title);
padding: var(--padding-page);
display: grid;
top: 0;
left: 0;
width: 100%;
height: 100vh;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto auto;
grid-template-areas: 'title prev ...' 'sponsor ... demo';
justify-content: start;
pointer-events: none;
grid-gap: 2rem;
z-index: 1000;
align-content: space-between;
}
.frame a {
pointer-events: auto;
}
.frame a:not(.frame__title-back) {
white-space: nowrap;
overflow: hidden;
position: relative;
}
.frame a:not(.frame__title-back)::before {
content: '';
height: 1px;
width: 100%;
background: currentColor;
position: absolute;
top: 90%;
transition: transform 0.3s;
transform-origin: 0% 50%;
}
.frame a:not(.frame__title-back):hover::before {
transform: scaleX(0);
transform-origin: 100% 50%;
}
.frame__title {
grid-area: title;
display: flex;
align-items: start;
}
.frame__title-main {
font-size: 1rem;
margin: 0;
font-weight: normal;
max-width: 32ch;
}
.frame__title-back {
position: relative;
display: flex;
align-items: flex-end;
}
.frame__title-back span {
display: none;
}
.frame__title-back svg {
fill: currentColor;
}
.frame__prev {
grid-area: prev;
align-self: start;
}
.frame__demos {
grid-area: demo;
display: flex;
gap: 1.5rem;
justify-self: end;
}
canvas {
display: block;
}
.content {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.ui-controls {
position: absolute;
bottom: 0;
right: 0;
user-select: none;
text-align: end;
min-width: 200px;
padding: var(--padding-page);
}
.ui-controls__button {
cursor: pointer;
display: inline-block;
text-align: center;
font-size: 1.15rem;
border: 1px solid #000;
padding: 0.65rem 1rem;
line-height: 1;
cursor: pointer;
margin: 0 0 1rem 0.5rem;
}
.ui-controls__button.disabled {
pointer-events: none;
cursor: auto;
opacity: .3;
}
.lil-gui {
--background-color: #000000;
--text-color: #b1b1b1;
--title-background-color: #171717;
--title-text-color: #d5d5d5;
--widget-color: #343237;
--hover-color: #3c3c3c;
--focus-color: #3d3d3d;
--number-color: #cf0778;
--string-color: #8956f9;
}
@media screen and (min-width: 53em) {
}index.html
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rotating Twisted Text | Demo 1 | Codrops</title>
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="author" content="Yuriy Artyukh for Codrops" />
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="css/base.css" />
<script>document.documentElement.className="js";var supportsCssVars=function(){var e,t=document.createElement("style");return t.innerHTML="root: { --tmp-var: bold; }",document.head.appendChild(t),e=!!(window.CSS&&window.CSS.supports&&window.CSS.supports("font-weight","var(--tmp-var)")),t.parentNode.removeChild(t),e};supportsCssVars()||alert("Please view this demo in a modern browser that supports CSS Variables.");</script>
<script src="//tympanus.net/codrops/adpacks/analytics.js"></script>
</head>
<body>
<main>
<div class="frame">
<div class="frame__title">
<h1 class="frame__title-main">Rotating Twisted Text</h1>
<a aria-label="Back to the article" class="frame__title-back" href="https://tympanus.net/codrops/?p=69794">
<span class="oh__inner">Back to the article</span>
<svg width="18px" height="18px" viewBox="0 0 24 24"><path vector-effect="non-scaling-stroke" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path>
</svg>
</a>
</div>
<nav class="frame__demos">
<span class="frame__demo frame__demo--current">Circular text</span>
<a href="index2.html" class="frame__demo">Spiral text</a>
</nav>
<a class="frame__prev" href="https://tympanus.net/Tutorials/LoopScrolling/">Previous demo</a>
</div>
<div class="content" id="container"></div>
<script src="js/app.js" type="module"></script>
</main>
<script src="https://tympanus.net/codrops/adpacks/cda_sponsor.js"></script>
</body>
</html>
함께 쓰는 파일 9개 보기
index2.html
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rotating Twisted Text | Demo 2 | Codrops</title>
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="author" content="Yuriy Artyukh for Codrops" />
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="css/base.css" />
<script>document.documentElement.className="js";var supportsCssVars=function(){var e,t=document.createElement("style");return t.innerHTML="root: { --tmp-var: bold; }",document.head.appendChild(t),e=!!(window.CSS&&window.CSS.supports&&window.CSS.supports("font-weight","var(--tmp-var)")),t.parentNode.removeChild(t),e};supportsCssVars()||alert("Please view this demo in a modern browser that supports CSS Variables.");</script>
<script src="//tympanus.net/codrops/adpacks/analytics.js"></script>
</head>
<body>
<main>
<div class="frame">
<div class="frame__title">
<h1 class="frame__title-main">Rotating Twisted Text</h1>
<a aria-label="Back to the article" class="frame__title-back" href="https://tympanus.net/codrops/?p=69794">
<span class="oh__inner">Back to the article</span>
<svg width="18px" height="18px" viewBox="0 0 24 24"><path vector-effect="non-scaling-stroke" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path>
</svg>
</a>
</div>
<nav class="frame__demos">
<a class="frame__demo" href="index.html">Circular text</a>
<span class="frame__demo frame__demo--current">Spiral text</span>
</nav>
<a class="frame__prev" href="https://tympanus.net/Tutorials/LoopScrolling/">Previous demo</a>
</div>
<div class="content" id="container"></div>
<script src="js/spiral.js" type="module"></script>
</main>
<script src="https://tympanus.net/codrops/adpacks/cda_sponsor.js"></script>
</body>
</html>
js/app.js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import GUI from "lil-gui";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import matcap from "../assets/chemical_carpaint_blue.png";
import matcap1 from "../assets/tomato.png";
import matcap2 from "../assets/clay_alien.png";
import matcap3 from "../assets/metal_copper_flamed.png";
import font from "../assets/font.data";
import font0 from "../assets/font0.data";
import font1 from "../assets/font1.data";
import font2 from "../assets/font2.data";
const FONTS = [font, font0, font1, font2];
let matcaps = [matcap, matcap1, matcap2, matcap3];
matcaps = matcaps.map((m) => new THREE.TextureLoader().load(m));
export default class Sketch {
constructor(options) {
this.scene = new THREE.Scene();
this.scene1 = new THREE.Scene();
this.group = new THREE.Group();
this.group1 = new THREE.Group();
this.scene.add(this.group);
this.scene1.add(this.group1);
this.container = options.dom;
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(this.width, this.height);
this.renderer.physicallyCorrectLights = true;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.container.appendChild(this.renderer.domElement);
this.loader = new FontLoader();
this.camera = new THREE.PerspectiveCamera(
70,
this.width / this.height,
0.1,
100
);
this.camera.position.set(0, 0, 6);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = 4;
this.controls.maxDistance = 12;
this.time = 0;
this.isPlaying = true;
this.settings();
this.addObjects();
this.resize();
this.render();
this.setupResize();
}
settings() {
let that = this;
this.settings = {
text: "INCOMPREHENSIBILITY",
fontSize: 1,
rotateSpeed: 1,
twistSpeed: 7.9,
fontDepth: 0.3,
radius: 2.8,
twists: 2,
visual: 0,
font: 0,
};
this.gui = new GUI();
this.gui.add(this.settings, "text").onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "fontSize", 0, 1, 0.01).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "fontDepth", 0, 1, 0.01).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "radius", 1, 4, 0.01).onChange(() => {
this.uniforms.uRadius.value = this.settings.radius;
});
this.gui.add(this.settings, "rotateSpeed", 0, 1, 0.01).onChange(() => {
this.uniforms.uRotateSpeed.value = this.settings.rotateSpeed;
});
this.gui.add(this.settings, "visual", 0, 3, 1).onChange(() => {
this.material.matcap = matcaps[this.settings.visual];
});
this.gui.add(this.settings, "font", 0, 3, 1).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "twists", 0, 3, 0.01).onChange(() => {
this.uniforms.uTwists.value = this.settings.twists;
});
this.gui.add(this.settings, "twistSpeed", 0, 10, 0.01).onChange(() => {
this.uniforms.uTwistSpeed.value = this.settings.twistSpeed;
});
}
setupResize() {
window.addEventListener("resize", this.resize.bind(this));
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize(this.width, this.height);
this.camera.aspect = this.width / this.height;
this.camera.updateProjectionMatrix();
}
updateGeometry() {
this.loader.load(FONTS[this.settings.font], (font) => {
this.geo = new TextGeometry(this.settings.text, {
font: font,
size: this.settings.fontSize,
height: this.settings.fontDepth,
curveSegments: 100,
bevelThickness: 0.03,
bevelSize: 0.03,
bevelSegments: 10,
bevelEnabled: false,
});
this.geo.center();
this.geo.computeBoundingBox();
this.uniforms.uMin.value = this.geo.boundingBox.min;
this.uniforms.uMax.value = this.geo.boundingBox.max;
this.uniforms.uMax.value.x += this.settings.fontSize / 6;
this.textmesh.geometry = this.geo;
});
}
getMaterial() {
let matcaptexture = new THREE.TextureLoader().load(matcap);
matcaptexture.needsUpdate = true;
let header = `
varying vec3 vPosition;
varying float vDebug;
uniform float uOffset;
uniform float uTwistSpeed;
uniform float uRotateSpeed;
uniform float uTwists;
uniform float uRadius;
uniform vec3 uMin;
uniform vec3 uMax;
uniform float time;
float radius = 1.5;
float twists = 2.;
float PI = 3.141592653589793238;
mat4 rotationMatrix(vec3 axis, float angle) {
axis = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,
0.0, 0.0, 0.0, 1.0);
}
vec3 rotate(vec3 v, vec3 axis, float angle) {
mat4 m = rotationMatrix(axis, angle);
return (m * vec4(v, 1.0)).xyz;
}
float mapRange(float value, float min1, float max1, float min2, float max2) {
// return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
return clamp( min2 + (value - min1) * (max2 - min2) / (max1 - min1), min2, max2 );
}
`;
let normals = `
// objectNormal
vPosition = position;
float xx = mapRange(vPosition.x, uMin.x, uMax.x, -1., 1.0);
// twistnormal
objectNormal = rotate(objectNormal, vec3(1.,0.,0.), 0.5*PI*uTwists*xx + 0.01*time*uTwistSpeed);
// circled normal
objectNormal = rotate(objectNormal, vec3(0.,0.,1.), (xx + 0.01*time*uRotateSpeed)*PI);
`;
let geometry = `
vec3 pos = transformed;
// twist
float xxx = mapRange(position.x, uMin.x, uMax.x, -1., 1.);
//circle
float theta = (xxx + 0.01*time*uRotateSpeed)*PI;
pos = rotate(pos,vec3(1.,0.,0.), 0.5*PI*uTwists*xxx + 0.01*time*uTwistSpeed);
vec3 dir = vec3(sin(theta), cos(theta),pos.z);
vec3 circled = vec3(dir.xy*uRadius,pos.z) + vec3(pos.y*dir.x,pos.y*dir.y,0.);
transformed = circled;
`;
this.uniforms = {
uOffset: { value: 0 },
time: {
value: 0,
},
uRadius: {
value: this.settings.radius,
},
uTwists: {
value: this.settings.twists,
},
uTwistSpeed: {
value: this.settings.twistSpeed,
},
uRotateSpeed: {
value: this.settings.rotateSpeed,
},
uMin: {
value: { x: -1, y: 0, z: 0 },
},
uMax: {
value: { x: 1, y: 0, z: 0 },
},
};
let material = new THREE.MeshMatcapMaterial({
matcap: matcaptexture,
});
material.onBeforeCompile = (shader) => {
shader.uniforms = { ...shader.uniforms, ...this.uniforms };
shader.vertexShader = header + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
"#include <beginnormal_vertex>",
"#include <beginnormal_vertex>" + normals
);
shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
"#include <begin_vertex>" + geometry
);
material.userData.shader = shader;
};
return material;
}
addObjects() {
this.material = this.getMaterial();
this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
this.loader.load(font1, (font) => {
this.geo = new TextGeometry(this.settings.text, {
font: font,
size: this.settings.fontSize,
height: this.settings.fontDepth,
curveSegments: 100,
bevelThickness: 0.03,
bevelSize: 0.03,
bevelSegments: 10,
bevelEnabled: false,
});
this.geo.center();
this.geo.computeBoundingBox();
this.uniforms.uMin.value = this.geo.boundingBox.min;
this.uniforms.uMax.value = this.geo.boundingBox.max;
this.uniforms.uMax.value.x += this.settings.fontSize / 6;
this.textmesh = new THREE.Mesh(this.geo, this.material);
this.group.add(this.textmesh);
});
}
render() {
if (!this.isPlaying) return;
this.time += 0.05;
this.uniforms.time.value = this.time;
this.controls.update();
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.render.bind(this));
}
}
new Sketch({
dom: document.getElementById("container"),
});
js/extend.js
import * as THREE from "three";
// Author: Fyrestar https://mevedia.com (https://github.com/Fyrestar/THREE.extendMaterial)
const {
ShaderChunk,
UniformsLib,
Vector2,
Color,
EventDispatcher,
TangentSpaceNormalMap,
MultiplyOperation,
Material,
ShaderMaterial,
RawShaderMaterial
} = THREE;
// Fix missing pairs
UniformsLib.clearcoatnormalmap = {
clearcoatNormalScale: { value: new Vector2( 1, 1 ) }
};
// Patch materials
const Materials = [
'MeshDistanceMaterial',
'MeshMatcapMaterial',
'ShadowMaterial',
'SpriteMaterial',
'RawShaderMaterial',
'ShaderMaterial',
'PointsMaterial',
'MeshPhysicalMaterial',
'MeshStandardMaterial',
'MeshPhongMaterial',
'MeshToonMaterial',
'MeshNormalMaterial',
'MeshLambertMaterial',
'MeshDepthMaterial',
'MeshBasicMaterial',
'LineDashedMaterial',
'LineBasicMaterial',
'Material',
'MeshFaceMaterial',
'MultiMaterial',
'PointCloudMaterial',
'ParticleBasicMaterial',
'ParticleSystemMaterial'
];
for ( let name of Materials ) {
if ( THREE[ name ] !== undefined ) {
const prototype = THREE[ name ].prototype;
// Type on prototype needed to identify when minified
prototype.type = name;
prototype.customDepthMaterial = null;
prototype.customDistanceMaterial = null;
prototype.templates = [];
}
}
// New material methods
const _clone = ShaderMaterial.prototype.clone;
function clone() {
const clone = _clone.call( this );
clone.templates = this.templates;
return clone;
}
function link( source ) {
if ( source && source.uniforms ) {
for ( let name in source.uniforms ) {
if ( source.uniforms[ name ].linked )
this.uniforms[ name ] = source.uniforms[ name ];
}
}
return this;
}
function copy( source ) {
Material.prototype.copy.call( this, source );
for ( let name of mapList ) {
if ( source[ name ] )
this[ name ] = source[ name ];
}
for ( let name of localsList ) {
if ( source[ name ] )
this[ name ] = source[ name ];
}
this.fragmentShader = source.fragmentShader;
this.vertexShader = source.vertexShader;
this.uniforms = cloneUniforms( source.uniforms );
this.defines = Object.assign( {}, source.defines );
this.wireframe = source.wireframe;
this.wireframeLinewidth = source.wireframeLinewidth;
this.lights = source.lights;
this.clipping = source.clipping;
this.skinning = source.skinning;
this.morphTargets = source.morphTargets;
this.morphNormals = source.morphNormals;
this.extensions = source.extensions;
if ( source.customDepthMaterial )
this.customDepthMaterial = source.customDepthMaterial.clone().link( this );
if ( source.customDistanceMaterial )
this.customDistanceMaterial = source.customDistanceMaterial.clone().link( this );
return this;
}
function uniform( name ) {
if ( this.uniforms[ name ] === undefined )
this.uniforms[ name ] = { value: null };
return this.uniforms[ name ];
}
function extend( source, object ) {
object = object || {};
// Extend from class or shader material
let uniforms = {}, vertexShader = '', fragmentShader = '';
// Inherit from previous material templates chain
const base = object.template || object.extends;
const MaterialClass = object.class || ( source.isMaterial && source.constructor ? source.constructor : null ) || CustomMaterial;
// New shader material
const material = new MaterialClass;
const properties = object.material = object.material || {};
const defines = Object.assign( {}, properties.defines );
// Template chain
material.templates = [ object ];
if ( source.templates instanceof Array )
material.templates = source.templates.concat( material.templates );
let name;
if ( source instanceof Function ) {
// Source is a constructor
name = source.prototype.type;
const mapping = mappings[ name ];
if ( mapping === undefined ) {
console.error( 'THREE.extendMaterial: no mapping for material class "%s" found', name );
return material;
}
properties.lights = properties.lights === undefined ? true : properties.lights;
uniforms = mapUniforms( mapping.name, uniforms, object ); // Use only declared/necessary uniforms or all
vertexShader = ShaderChunk[ mapping.id + '_vert' ];
fragmentShader = ShaderChunk[ mapping.id + '_frag' ];
} else if ( source.isShaderMaterial ) {
// Source is a ShaderMaterial
name = source.type;
uniforms = cloneUniforms( source.uniforms, uniforms ); // Use uniforms of previous material
vertexShader = source.vertexShader;
fragmentShader = source.fragmentShader;
material.copy( source, false );
if ( source.defines )
Object.assign( defines, source.defines );
} else {
// Source is a material instance
name = source.type;
const mapping = mappings[ name ];
if ( mapping === undefined ) {
console.error( 'THREE.extendMaterial: no mapping for material class "%s" found', name );
return material;
}
properties.lights = properties.lights === undefined ? true : properties.lights;
uniforms = mapUniforms( mapping.name, uniforms, object );
vertexShader = ShaderChunk[ mapping.id + '_vert' ];
fragmentShader = ShaderChunk[ mapping.id + '_frag' ];
// Built-in properties to uniforms ( if explicit not disabled, those being null will be skipped )
const defaults = THREE.ShaderLib[ mapping.name ].uniforms;
for ( let name in defaults )
if ( uniforms[ name ] === undefined && source[ name ] !== undefined && ( source[ name ] !== null || object.explicit === false ) ) {
uniforms[ name ] = uniforms[ name ] || { value: null };
uniforms[ name ].value = source[ name ];
}
}
// ShaderMaterial in disguise ( currently not required )
//if ( material.isCustomMaterial )
// material[ 'is' + name ] = true;
// Override constants
if ( object.defines )
Object.assign( defines, object.defines );
// A shared header ( varyings, uniforms, functions etc )
let header = ( object.header || '' ) + '\n';
// Insert or replace lines (@ to replace)
if ( object.vertex !== undefined )
vertexShader = applyPatches( vertexShader, object.vertex );
if ( object.fragment !== undefined )
fragmentShader = applyPatches( fragmentShader, object.fragment );
properties.defines = defines;
properties.uniforms = uniforms;
properties.vertexShader = header + ( object.vertexHeader || '' ) + '\n' + vertexShader;
properties.fragmentShader = header + ( object.fragmentHeader || '' ) + '\n' + fragmentShader;
if ( object.vertexEnd )
properties.vertexShader = properties.vertexShader.replace( /\}(?=[^.]*$)/g, object.vertexEnd + '\n}' );
if ( object.fragmentEnd )
properties.fragmentShader = properties.fragmentShader.replace( /\}(?=[^.]*$)/g, object.fragmentEnd + '\n}' );
// Uniforms override
if ( object.override ) {
for ( let name in object.override ) {
const src = object.override[ name ];
const dst = uniforms[ name ] = uniforms[ name ] || { value: null };
for ( let k in src )
dst[ k ] = src[ k ];
// Expose mixed uniforms to template if not exposed yet ( material before might have been built-in )
if ( dst.mixed ) {
if ( !object.uniforms )
object.uniforms = {};
object.uniforms[ name ] = dst;
}
}
}
// Apply base templates, uniforms that are required in a template must be flagged as mixed to get inherited
if ( base && base.templates && base.templates.length ) {
object.class = object.class || base.class;
for ( let template of base.templates )
patchShader( properties, template, mixUniforms, defines );
// Linked uniforms: Assign linked uniforms of base template material
for ( let name in base.uniforms ) {
const src = base.uniforms[ name ];
if ( src.linked )
uniforms[ name ] = src;
}
}
// Applies uniforms defined for this new material
applyUniforms( material, object, properties, defines );
// Finally apply material properties
material.setValues( properties );
if ( material.isCustomMaterial ) {
for ( let name in uniforms ) {
if ( mapFlags[ name ] && uniforms[ name ].value )
material[ name ] = uniforms[ name ].value;
}
}
// Fix: since we use #ifdef false would be false positive
for ( let name in defines )
if ( defines[ name ] === false )
delete defines[ name ];
// Fix: default for depth material packing
if ( ( source.isMeshDepthMaterial || source === THREE.MeshDepthMaterial ) && defines.DEPTH_PACKING === undefined )
defines.DEPTH_PACKING = THREE.RGBADepthPacking;
return material;
}
Object.assign( RawShaderMaterial.prototype, { extend, uniform, clone, link, copy } );
Object.assign( ShaderMaterial.prototype, { extend, uniform, clone, link, copy } );
// Polyfill to allow custom depth/distance materials as material variation
if ( ShaderMaterial.prototype.customDepthMaterial !== undefined ) {
const MeshPolyfill = {
_customDepthMaterial: {
enumerable: true,
value: undefined,
writable: true
},
_customDistanceMaterial: {
enumerable: true,
value: undefined,
writable: true
},
customDepthMaterial: {
get: function () {
return this._customDepthMaterial || ( this.material && this.material.customDepthMaterial ? this.material.customDepthMaterial : undefined );
},
set: function ( value ) {
this._customDepthMaterial = value;
}
},
customDistanceMaterial: {
get: function () {
return this._customDistanceMaterial || ( this.material && this.material.customDistanceMaterial ? this.material.customDistanceMaterial : undefined );
},
set: function ( value ) {
this._customDistanceMaterial = value;
}
}
};
Object.defineProperties( THREE.Mesh.prototype, MeshPolyfill );
Object.defineProperties( THREE.SkinnedMesh.prototype, MeshPolyfill );
}
// A built-in materials compatible ShaderMaterial
extend.CustomMaterial = function CustomMaterial( object ) {
ShaderMaterial.call( this, object );
this.type = 'CustomMaterial';
};
Object.assign(
extend.CustomMaterial.prototype,
Material.prototype,
ShaderMaterial.prototype,
EventDispatcher.prototype,
{
isShaderMaterial: true,
isCustomMaterial: true,
isMeshPhongMaterial: false,
isMeshDistanceMaterial: false,
isMeshMatcapMaterial: false,
isShadowMaterial: false,
isSpriteMaterial: false,
isRawShaderMaterial: false,
isPointsMaterial: false,
isMeshPhysicalMaterial: false,
isMeshStandardMaterial: false,
isMeshPhongMaterial: false,
isMeshToonMaterial: false,
isMeshNormalMaterial: false,
isMeshLambertMaterial: false,
isMeshDepthMaterial: false,
isMeshBasicMaterial: false,
isLineDashedMaterial: false,
isLineBasicMaterial: false,
isMaterial: false,
isMeshFaceMaterial: false,
isMultiMaterial: false,
isPointCloudMaterial: false,
isParticleBasicMaterial: false,
isParticleSystemMaterial: false,
constructor: extend.CustomMaterial,
map: null,
aoMap: null,
envMap: null,
bumpMap: null,
normalMap: null,
lightMap: null,
emissiveMap: null,
specularMap: null,
roughnessMap: null,
metalnessMap: null,
alphaMap: null,
displacementMap: null,
clearcoatMap: null,
clearcoatRoughnessMap: null,
clearcoatNormalMap: null,
normalMapType: TangentSpaceNormalMap,
combine: MultiplyOperation,
clone: function ( source ) {
const clone = _clone.call( this );
if ( this.map ) clone.map = this.map;
if ( this.aoMap ) clone.aoMap = this.aoMap;
if ( this.envMap ) clone.envMap = this.envMap;
if ( this.bumpMap ) clone.bumpMap = this.bumpMap;
if ( this.normalMap ) clone.normalMap = this.normalMap;
if ( this.lightMap ) clone.lightMap = this.lightMap;
if ( this.emissiveMap ) clone.emissiveMap = this.emissiveMap;
if ( this.specularMap ) clone.specularMap = this.specularMap;
if ( this.roughnessMap ) clone.roughnessMap = this.roughnessMap;
if ( this.metalnessMap ) clone.metalnessMap = this.metalnessMap;
if ( this.alphaMap ) clone.alphaMap = this.alphaMap;
if ( this.displacementMap ) clone.displacementMap = this.displacementMap;
if ( this.clearcoatMap ) clone.clearcoatMap = this.clearcoatMap;
if ( this.clearcoatRoughnessMap ) clone.clearcoatRoughnessMap = this.clearcoatRoughnessMap;
if ( this.clearcoatNormalMap ) clone.clearcoatNormalMap = this.clearcoatNormalMap;
clone.templates = this.templates;
return clone;
}
}
);
Object.defineProperties( extend.CustomMaterial.prototype, {
reflectivity: {
get: function () {
return this.uniforms.reflectivity ? this.uniforms.reflectivity.value : 0;
},
set: function ( value ) {
this.uniforms.reflectivity.value = value;
}
},
specular: {
get: function () {
if ( this.uniforms.specular === undefined )
this.uniforms.specular = { value: new THREE.Color( 'white' ) };
return this.uniforms.specular.value;
},
set: function ( value ) {
if ( this.uniforms.specular === undefined )
this.uniforms.specular = { value };
this.uniforms.specular.value = value;
}
},
shininess: {
get: function () {
return this.uniforms.shininess ? this.uniforms.shininess.value : 0;
},
set: function ( value ) {
if ( this.uniforms.shininess === undefined )
this.uniforms.shininess = { value };
this.uniforms.shininess.value = value;
}
}
} );
// Wrap ES6
if ( !Object.isExtensible( THREE ) || parseInt( THREE.REVISION ) > 126 ) {
class CustomMaterial extends ShaderMaterial {
constructor( object ) {
super( object );
this.type = 'CustomMaterial';
}
get specular () {
if ( this.uniforms.specular === undefined )
this.uniforms.specular = { value: new THREE.Color( 'white' ) };
return this.uniforms.specular.value;
}
set specular ( value ) {
if ( this.uniforms.specular === undefined )
this.uniforms.specular = { value };
this.uniforms.specular.value = value;
}
get shininess () {
return this.uniforms.shininess ? this.uniforms.shininess.value : 0;
}
set shininess ( value ) {
if ( this.uniforms.shininess === undefined )
this.uniforms.shininess = { value };
this.uniforms.shininess.value = value;
}
get reflectivity () {
return this.uniforms.reflectivity ? this.uniforms.reflectivity.value : 0;
}
set reflectivity ( value ) {
this.uniforms.reflectivity.value = value;
}
}
Object.assign( CustomMaterial, extend.CustomMaterial )
Object.assign( CustomMaterial.prototype, extend.CustomMaterial.prototype, { copy, clone } );
extend.CustomMaterial = CustomMaterial;
}
// Alias
const extendMaterial = extend;
const CustomMaterial = extendMaterial.CustomMaterial;
let sharedLightsUniforms;
// Class name to internal lib names
const localsList = [
'bumpScale',
'roughness',
'metalness',
'shininess',
'envMapIntensity',
'opacity',
'dashSize',
'totalSize'
];
const localsMapping = {
bumpScale: 'bumpScale',
roughness: 'roughness',
metalness: 'metalness',
shininess: 'shininess',
envMapIntensity: 'envMapIntensity',
opacity: 'opacity',
dashSize: 'dashSize',
totalSize: 'totalSize'
};
const mapList = [
'map',
'aoMap',
'envMap',
'bumpMap',
'normalMap',
'lightMap',
'emissiveMap',
'specularMap',
'roughnessMap',
'metalnessMap',
'alphaMap',
'displacementMap'
];
const mappings = {
MeshLambertMaterial: {
id: 'meshlambert',
name: 'lambert'
},
MeshBasicMaterial: {
id: 'meshbasic',
name: 'basic'
},
MeshStandardMaterial: {
id: 'meshphysical',
name: 'physical'
},
MeshPhongMaterial: {
id: 'meshphong',
name: 'phong'
},
MeshMatcapMaterial: {
id: 'meshmatcap',
name: 'matcap'
},
PointsMaterial: {
id: 'points',
name: 'points'
},
LineDashedMaterial: {
id: 'dashed',
name: 'linedashed'
},
MeshDepthMaterial: {
id: 'depth',
name: 'depth'
},
MeshNormalMaterial: {
id: 'normal',
name: 'normal'
},
MeshDistanceMaterial: {
id: 'distanceRGBA',
name: 'distanceRGBA'
},
SpriteMaterial: {
id: 'sprite',
name: 'sprite'
}
};
// Aliases for shorter code hints
const aliases = {
lightsBegin: '?#include <lights_fragment_maps>',
lightsEnd: '?#include <aomap_fragment>',
colorBegin: '?#include <logdepthbuf_fragment>',
colorEnd: '?#include <tonemapping_fragment>',
transformBegin: '?#include <morphtarget_vertex>',
transformEnd: '?#include <project_vertex>'
};
// Converts properties to constant definition
const uniformFlags = {
alphaTest: {
as: 'ALPHATEST',
not: 0
}
};
// Set of required uniforms ( which aren't null or zero )
const uniforms = {
opacity: { value: 1.0 },
specular: { value: new Color( 0x111111 ) }
};
const requiredUniforms = {
points: UniformsLib.points,
sprite: UniformsLib.sprite,
dashed: {
scale: { value: 1 },
dashSize: { value: 1 },
totalSize: { value: 2 }
},
normal: {
opacity: uniforms.opacity
},
toon: {
specular: uniforms.specular,
shininess: { value: 30 }
},
standard: {
shininess: { value: 30 },
roughness: { value: 1.0 },
metalness: { value: 0.0 },
envMapIntensity: { value: 1 } // temporary
},
physical: {
shininess: { value: 30 },
roughness: { value: 1.0 },
metalness: { value: 0.0 },
envMapIntensity: { value: 1 } // temporary
},
phong: {
specular: uniforms.specular,
shininess: { value: 30 }
},
cube: {
opacity: uniforms.opacity
},
distanceRGBA: {
nearDistance: { value: 1 },
farDistance: { value: 1000 }
},
shadow: {
opacity: uniforms.opacity
}
};
// Constant definitions for which maps are used
const mapFlags = {
map: 'USE_MAP',
aoMap: 'USE_AOMAP',
envMap: 'USE_ENVMAP',
bumpMap: 'USE_BUMPMAP',
normalMap: 'USE_NORMALMAP',
lightMap: 'USE_LIGHTMAP',
emissiveMap: 'USE_EMISSIVEMAP',
specularMap: 'USE_SPECULARMAP',
roughnessMap: 'USE_ROUGHNESSMAP',
metalnessMap: 'USE_METALNESSMAP',
alphaMap: 'USE_ALPHAMAP',
displacementMap: 'USE_DISPLACEMENTMAP'
};
function useUniformPairs( instance, uniforms ) {
// Only pairs with initial values other than null or zero needed
if ( !instance ) return;
if ( instance.envMap )
cloneUniforms( UniformsLib.envmap, uniforms );
if ( instance.aoMap )
cloneUniforms( UniformsLib.aomap, uniforms );
if ( instance.lightMap )
cloneUniforms( UniformsLib.lightmap, uniforms );
if ( instance.bumpMap )
cloneUniforms( UniformsLib.bumpmap, uniforms );
if ( instance.normalMap )
cloneUniforms( UniformsLib.normalmap, uniforms );
if ( instance.displacementMap )
cloneUniforms( UniformsLib.displacementmap, uniforms );
if ( instance.clearcoatNormalMap )
cloneUniforms( UniformsLib.clearcoatnormalmap, uniforms );
}
function useUniforms( name, object, uniforms ) {
useUniformPairs( object.uniforms, uniforms );
if ( object.common !== false )
cloneUniforms( UniformsLib.common, uniforms );
if ( requiredUniforms[ name ] !== undefined )
cloneUniforms( requiredUniforms[ name ], uniforms );
const fog = object.fog || ( object.material ? object.material.fog || object.material.useFog : null );
const lights = object.lights || ( object.material ? object.material.lights : null );
if ( fog )
cloneUniforms( UniformsLib.fog, uniforms );
if ( lights ) {
if ( object.use ) {
const use = object.use;
const shared = use.indexOf( 'sharedLights' ) > -1;
const shadows = use.indexOf( 'shadows' ) > -1;
if ( use.indexOf( 'PointLight' ) > -1 ) {
cloneUniforms( UniformsLib.lights.pointLights, uniforms );
if ( shadows ) {
cloneUniforms( UniformsLib.pointLightShadows, uniforms );
cloneUniforms( UniformsLib.pointShadowMap, uniforms );
cloneUniforms( UniformsLib.pointShadowMatrix, uniforms );
}
}
if ( use.indexOf( 'SpotLight' ) > -1 ) {
cloneUniforms( UniformsLib.lights.spotLights, uniforms );
if ( shadows ) {
cloneUniforms( UniformsLib.spotLightShadows, uniforms );
cloneUniforms( UniformsLib.spotShadowMap, uniforms );
cloneUniforms( UniformsLib.spotShadowMatrix, uniforms );
}
}
if ( use.indexOf( 'DirectionalLight' ) > -1 ) {
cloneUniforms( UniformsLib.lights.directionalLights, uniforms );
if ( shadows ) {
cloneUniforms( UniformsLib.directionalLightShadows, uniforms );
cloneUniforms( UniformsLib.directionalShadowMap, uniforms );
cloneUniforms( UniformsLib.directionalShadowMatrix, uniforms );
}
}
if ( use.indexOf( 'LightProbe' ) > -1 )
cloneUniforms( UniformsLib.lights.lightProbe, uniforms );
if ( use.indexOf( 'AmbientLight' ) > -1 )
cloneUniforms( UniformsLib.lights.ambientLightColor, uniforms );
if ( use.indexOf( 'ReactAreaLight' ) > -1 )
cloneUniforms( UniformsLib.lights.rectAreaLights, uniforms );
if ( use.indexOf( 'HemisphereLight' ) > -1 )
cloneUniforms( UniformsLib.lights.hemisphereLights, uniforms );
} else {
cloneUniforms( sharedLightsUniforms, uniforms );
}
}
return uniforms;
}
function cloneUniform( src, dst ) {
dst = dst || {};
for ( let key in src ) {
const property = src[ key ];
if ( property && ( property.isColor ||
property.isMatrix3 || property.isMatrix4 ||
property.isVector2 || property.isVector3 || property.isVector4 ||
property.isTexture ) ) {
dst[ key ] = property.clone();
} else if ( Array.isArray( property ) ) {
dst[ key ] = property.slice();
} else {
dst[ key ] = property;
}
}
return dst;
}
function makeUniform( value ) {
return value && value.value !== undefined ? value : { value: value };
}
function cloneUniforms( src, dst, notNull = false, share = false, mix = false, link = false ) {
dst = dst || {};
for ( let u in src ) {
const uniform = src[ u ];
if ( !uniform ) continue;
if ( notNull ) {
// Uniforms with null are skipped
if ( uniform.value === null || uniform.value === 0 )
continue;
// Their parameters then too
}
if ( uniform.shared || ( uniform.linked && link ) ) {
dst[ u ] = uniform;
} else {
dst[ u ] = cloneUniform( uniform );
if ( share === true )
dst[ u ].shared = true;
if ( mix === true )
dst[ u ].mixed = true;
}
}
return dst;
}
function applyPatches( chunk, map ) {
for ( let name in map ) {
const value = map[ name ];
if ( aliases[ name ] !== undefined )
name = aliases[ name ];
if ( value instanceof Object ) {
if ( ShaderChunk[ name ] === undefined ) {
console.error( 'ShaderMaterial.extend: ShaderChunk "%s" not found', name );
} else {
chunk = chunk.replace( '#include <' + name + '>', applyPatches( ShaderChunk[ name ], value ) );
}
} else {
if ( name[ 0 ] === '@' ) {
// Replace
const line = name.substr( 1 );
chunk = chunk.replace( line, value );
} else if ( name[ 0 ] === '?' ) {
// Insert before
const line = name.substr( 1 );
chunk = chunk.replace( line, value + '\n' + line );
} else {
// Insert after
if ( !chunk ) {
console.error( "THREE.patchShader: chunk not found '%s'", name );
} else {
chunk = chunk.replace( name, name + '\n' + value );
}
}
}
}
return chunk;
}
function applyConstants( name, uniform, defines, object, instance ) {
// Uniforms that exist but are derived from the material instance internally
if ( uniform && uniform.value && localsMapping[ name ] !== undefined )
instance[ name ] = uniform.value;
// Maps require USE_X constants
if ( mapFlags[ name ] !== undefined && uniform && uniform.value !== undefined ) {
// Expose uniform to be detected
instance[ name ] = uniform.value;
}
// Converts properties like alphaTest to their constant
const flag = uniformFlags[ name ];
if ( flag !== undefined && ( flag.not === undefined || flag.not !== value ) )
defines[ flag.as ] = uniform.value;
}
// applyUniforms: Adds or overrides src uniforms to dst
function applyUniforms( instance, src, dst, defines ) {
if ( !src.uniforms || !dst )
return;
for ( let name in src.uniforms ) {
if ( !dst.uniforms[ name ] )
dst.uniforms[ name ] = {};
// Accepts uniform objects and plain values
let uniform = makeUniform( src.uniforms[ name ] );
if ( defines && applyConstants( name, uniform, defines, src, instance ) === false ) continue;
dst.uniforms[ name ] = uniform;
}
}
// mixUniforms: Only adds new uniforms which are declared as mixed
function mixUniforms( src, dst, defines ) {
// Only mixed uniforms are passed to dst, only if they not exist
if ( !src.uniforms )
return;
for ( let name in src.uniforms ) {
let uniform = src.uniforms[ name ];
if ( !uniform ) continue;
uniform = makeUniform( uniform );
if ( uniform.mixed && dst.uniforms[ name ] === undefined ) {
dst.uniforms[ name ] = uniform.shared ? uniform : cloneUniform( uniform );
if ( defines ) applyConstants( name, uniform, defines, src );
}
}
}
function mapUniforms( name, uniforms, object ) {
if ( object.explicit === false ) {
// Use all possible uniforms
return cloneUniforms( THREE.ShaderLib[ name ].uniforms, uniforms );
} else {
// Only use declared and necessary
return useUniforms( name, object, uniforms ); // cloneUniforms( THREE.ShaderLib[ name ].uniforms, uniforms, true );
}
}
function mapShader( name, type ) {
const mapping = mappings[ name ];
return ShaderChunk[ mapping.id + '_' + ( type === 'vertex' ? 'vert' : 'frag' ) ];
}
function patchShader( shader, object, uniformsMixer = applyUniforms, defines = null ) {
// A shared header ( varyings, uniforms, functions etc )
let header = ( object.header || '' ) + '\n';
let vertexShader = ( object.vertexHeader || '' ) + '\n' + shader.vertexShader;
let fragmentShader = ( object.fragmentHeader || '' ) + '\n' + shader.fragmentShader;
if ( object.vertexEnd )
vertexShader = vertexShader.replace( /\}(?=[^.]*$)/g, object.vertexEnd + '\n}' );
if ( object.fragmentEnd )
fragmentShader = fragmentShader.replace( /\}(?=[^.]*$)/g, object.fragmentEnd + '\n}' );
// Insert or replace lines (@ to replace)
if ( object.vertex !== undefined )
vertexShader = applyPatches( vertexShader, object.vertex );
if ( object.fragment !== undefined )
fragmentShader = applyPatches( fragmentShader, object.fragment );
shader.vertexShader = header + vertexShader;
shader.fragmentShader = header + fragmentShader;
if ( uniformsMixer instanceof Function )
uniformsMixer( object, shader, defines );
return shader;
}
sharedLightsUniforms = cloneUniforms( UniformsLib.lights, {}, false, true );
if ( Object.isExtensible( THREE ) ) {
THREE.cloneUniforms = cloneUniforms;
THREE.cloneUniform = cloneUniform;
THREE.patchShader = patchShader;
THREE.mapShader = mapShader;
THREE.extendMaterial = extendMaterial;
THREE.CustomMaterial = CustomMaterial;
}
export {
CustomMaterial,
patchShader,
extendMaterial,
cloneUniforms,
cloneUniform,
mapShader
};js/spiral.js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import GUI from "lil-gui";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import matcap from "../assets/chemical_carpaint_blue.png";
import matcap1 from "../assets/tomato.png";
import matcap2 from "../assets/clay_alien.png";
import matcap3 from "../assets/metal_copper_flamed.png";
import font from "../assets/font.data";
import font0 from "../assets/font0.data";
import font1 from "../assets/font1.data";
import font2 from "../assets/font2.data";
const FONTS = [font, font0, font1, font2];
let matcaps = [matcap, matcap1, matcap2, matcap3];
matcaps = matcaps.map((m) => new THREE.TextureLoader().load(m));
export default class Sketch {
constructor(options) {
this.scene = new THREE.Scene();
this.scene1 = new THREE.Scene();
this.group = new THREE.Group();
this.group1 = new THREE.Group();
this.scene.add(this.group);
this.scene1.add(this.group1);
this.container = options.dom;
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(this.width, this.height);
this.renderer.physicallyCorrectLights = true;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.container.appendChild(this.renderer.domElement);
this.loader = new FontLoader();
this.camera = new THREE.PerspectiveCamera(
70,
this.width / this.height,
0.1,
100
);
this.camera.position.set(0, 0, 6);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = 4;
this.controls.maxDistance = 12;
this.time = 0;
this.isPlaying = true;
this.settings();
this.addObjects();
this.resize();
this.render();
this.setupResize();
}
settings() {
let that = this;
this.settings = {
text: "INCOMPREHENSIBILITY",
fontSize: 1,
rotateSpeed: 1,
twistSpeed: 7.9,
fontDepth: 0.3,
radius: 2.8,
twists: 2,
visual: 0,
font: 0,
};
this.gui = new GUI();
this.gui.add(this.settings, "text").onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "fontSize", 0, 1, 0.01).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "fontDepth", 0, 1, 0.01).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "radius", 1, 4, 0.01).onChange(() => {
this.uniforms.uRadius.value = this.settings.radius;
});
this.gui.add(this.settings, "rotateSpeed", 0, 1, 0.01).onChange(() => {
this.uniforms.uRotateSpeed.value = this.settings.rotateSpeed;
});
this.gui.add(this.settings, "visual", 0, 3, 1).onChange(() => {
this.material.matcap = matcaps[this.settings.visual];
});
this.gui.add(this.settings, "font", 0, 3, 1).onChange(() => {
this.updateGeometry();
});
this.gui.add(this.settings, "twists", 0, 3, 0.01).onChange(() => {
this.uniforms.uTwists.value = this.settings.twists;
});
this.gui.add(this.settings, "twistSpeed", 0, 10, 0.01).onChange(() => {
this.uniforms.uTwistSpeed.value = this.settings.twistSpeed;
});
}
setupResize() {
window.addEventListener("resize", this.resize.bind(this));
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize(this.width, this.height);
this.camera.aspect = this.width / this.height;
this.camera.updateProjectionMatrix();
}
updateGeometry() {
this.loader.load(FONTS[this.settings.font], (font) => {
this.geo = new TextGeometry(this.settings.text, {
font: font,
size: this.settings.fontSize,
height: this.settings.fontDepth,
curveSegments: 100,
bevelThickness: 0.03,
bevelSize: 0.03,
bevelSegments: 10,
bevelEnabled: false,
});
this.geo.center();
this.geo.computeBoundingBox();
this.uniforms.uMin.value = this.geo.boundingBox.min;
this.uniforms.uMax.value = this.geo.boundingBox.max;
this.uniforms.uMax.value.x += this.settings.fontSize / 6;
this.textmesh.geometry = this.geo;
});
}
getMaterial() {
let matcaptexture = new THREE.TextureLoader().load(matcap);
matcaptexture.needsUpdate = true;
let header = `
varying vec3 vPosition;
varying float vDebug;
uniform float uOffset;
uniform float uTwistSpeed;
uniform float uRotateSpeed;
uniform float uTwists;
uniform float uRadius;
uniform vec3 uMin;
uniform vec3 uMax;
uniform float time;
float radius = 1.5;
float twists = 2.;
float PI = 3.141592653589793238;
mat4 rotationMatrix(vec3 axis, float angle) {
axis = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,
0.0, 0.0, 0.0, 1.0);
}
vec3 rotate(vec3 v, vec3 axis, float angle) {
mat4 m = rotationMatrix(axis, angle);
return (m * vec4(v, 1.0)).xyz;
}
float mapRange(float value, float min1, float max1, float min2, float max2) {
// return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
return clamp( min2 + (value - min1) * (max2 - min2) / (max1 - min1), min2, max2 );
}
`;
let normals = `
// objectNormal
vPosition = position;
float xx = mapRange(vPosition.x, uMin.x, uMax.x, -1., 1.0);
// twistnormal
objectNormal = rotate(objectNormal, vec3(1.,0.,0.), 0.5*PI*uTwists*xx + 0.01*time*uTwistSpeed);
// circled normal
objectNormal = rotate(objectNormal, vec3(0.,1.,0.), (xx + 0.01*time*uRotateSpeed)*PI*2.);
`;
let geometry = `
vec3 pos = transformed;
float theta = (xx + 0.01*time*uRotateSpeed)*PI*2.;
pos = rotate(pos,vec3(1.,0.,0.), 0.5*PI*uTwists*xx + 0.01*time*uTwistSpeed);
vec3 dir = vec3(sin(theta),pos.y,cos(theta));
vec3 undir = vec3(cos(theta),0.,sin(theta));
vec3 circled = vec3(dir.x*uRadius,pos.y, dir.z*uRadius) + vec3(pos.z*dir.x,xx*3.,pos.z*dir.z) - undir*pos.y*0.15;
transformed = circled;
`;
this.uniforms = {
uOffset: { value: 0 },
time: {
value: 0,
},
uRadius: {
value: this.settings.radius,
},
uTwists: {
value: this.settings.twists,
},
uTwistSpeed: {
value: this.settings.twistSpeed,
},
uRotateSpeed: {
value: this.settings.rotateSpeed,
},
uMin: {
value: { x: -1, y: 0, z: 0 },
},
uMax: {
value: { x: 1, y: 0, z: 0 },
},
};
let material = new THREE.MeshMatcapMaterial({
matcap: matcaptexture,
});
material.onBeforeCompile = (shader) => {
shader.uniforms = { ...shader.uniforms, ...this.uniforms };
shader.vertexShader = header + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
"#include <beginnormal_vertex>",
"#include <beginnormal_vertex>" + normals
);
shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
"#include <begin_vertex>" + geometry
);
material.userData.shader = shader;
};
return material;
}
addObjects() {
this.material = this.getMaterial();
this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
this.loader.load(font1, (font) => {
this.geo = new TextGeometry(this.settings.text, {
font: font,
size: this.settings.fontSize,
height: this.settings.fontDepth,
curveSegments: 100,
bevelThickness: 0.03,
bevelSize: 0.03,
bevelSegments: 10,
bevelEnabled: false,
});
this.geo.center();
this.geo.computeBoundingBox();
this.uniforms.uMin.value = this.geo.boundingBox.min;
this.uniforms.uMax.value = this.geo.boundingBox.max;
this.uniforms.uMax.value.x += this.settings.fontSize / 6;
this.textmesh = new THREE.Mesh(this.geo, this.material);
this.group.add(this.textmesh);
});
}
render() {
if (!this.isPlaying) return;
this.time += 0.05;
this.uniforms.time.value = this.time;
this.controls.update();
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.render.bind(this));
}
}
new Sketch({
dom: document.getElementById("container"),
});
Original author attribution실행 안내·자료
Rotating Twisted 3D Typography with Three.js and Shaders
Original author: Yuri Artiukh
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
Media credits and license evidence실행 안내·자료
README.md
## Credits
- Images from [Unsplash](https://unsplash.com/)
## License
[MIT](LICENSE)
Made with :blue_heart: by [Codrops](http://www.codrops.com)
Bundled dependency licenses실행 안내·자료
three@0.148.0 — LICENSE
The MIT License
Copyright © 2010-2022 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
lil-gui@0.17.0 — LICENSE.md
MIT License
Copyright (c) 2019 George Michael Brower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
글꼴 로드 뒤 geometry와 변형 경계 교체
입력·상태·출력·수명과 실제 결과를 명시합니다.
- 이 예제에서는
- Sketch.updateGeometry는 선택한 font를 읽은 후 TextGeometry를 새로 만들고 중심·boundingBox를 계산합니다. uMin·uMax를 바꾸고 textmesh.geometry를 대체하지만 이 경로에서 이전 geometry를 dispose하지 않습니다.
