Codrops 원본

Rotating Twisted 3D Typography with Three.js and Shaders

텍스트 · MIT

텍스트 더 보기
ORIGINAL PREVIEW
Rotating Twisted 3D Typography with Three.js and Shaders 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

11개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

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.
assets/font1.data — OFL-1.1실행 안내·자료

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

assets/font2.data — OFL-1.1실행 안내·자료

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

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.