自定义材质的使用,ShaderMaterial 和 RawShaderMaterial的区别是,前者可以使用一些通用的uniform, attribute 等等,比如
positon,uv, modelViewMatrix, modelMatrix 不需要去定义,可以直接在ShaderMaterial中使用,three.js 会自动定义好这些通用的 变量,并且时候去更新这些 内建变量,也不用去操心,由 three.js 来接管。
后者 RawShaderMaterial 没有这些内建变量,shader代码里用到的所有uniform,attribute 都需要自己去管理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test canvas texture</title>
<style>
body {
overflow: hidden;
}
</style>
<script src="./external/three.js"></script>
<script src="./external/dat.gui.min.js"></script>
<script src="./controls/OrbitControls.js"></script>
<script src="j01/bitmap-sdf.js"></script>
</head>
<body>
<script type="x-shader/x-vertex" id="vertexShader">
uniform vec2 center;
uniform vec3 outline_color;
uniform float outline_width;
varying vec2 vUv;
varying vec4 v_outlineColor;
varying float v_outlineWidth;
bool isPerspectiveMatrix( mat4 m ) {
return m[ 2 ][ 3 ] == - 1.0;
}
void main() {
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
vec2 scale;
scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
#ifndef USE_SIZEATTENUATION
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
if ( isPerspective ) scale *= - mvPosition.z;
#endif
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;
mvPosition.xy += alignedPosition;
gl_Position = projectionMatrix * mvPosition;
// vec4 outlineColor = vec4(1.0, 0, 0, 0.9);
v_outlineWidth = outline_width;
v_outlineColor = vec4(outline_color, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
#extension GL_OES_standard_derivatives : enable
//1.0 - SDFSettings.CUTOFF;
#define SDF_EDGE 0.75
uniform float opacity;
varying vec2 vUv;
uniform sampler2D map;
uniform vec3 fill_color;
varying vec4 v_outlineColor;
varying float v_outlineWidth;
// Get the distance from the edge of a glyph at a given position sampling an SDF texture.
float getDistance(vec2 position) {
return texture2D(map, position).r;
}
// Samples the sdf texture at the given position and produces a color based on the fill color and the outline.
vec4 getSDFColor(vec2 position, float outlineWidth, vec4 outlineColor, float smoothing) {
float distance = getDistance(position);
vec4 v_color = vec4(fill_color, 1.0);
if (outlineWidth > 0.0) {
// Don't get the outline edge exceed the SDF_EDGE
float outlineEdge = clamp(SDF_EDGE - outlineWidth, 0.0, SDF_EDGE);
float outlineFactor = smoothstep(SDF_EDGE - smoothing, SDF_EDGE + smoothing, distance);
vec4 sdfColor = mix(outlineColor, v_color, outlineFactor);
float alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, distance);
return vec4(sdfColor.rgb, sdfColor.a * alpha);
} else {
float alpha = smoothstep(SDF_EDGE - smoothing, SDF_EDGE + smoothing, distance);
return vec4(v_color.rgb, v_color.a * alpha);
}
}
void main() {
vec3 outgoingLight = vec3( 0.0 );
vec4 diffuseColor = vec4( vec3(1,1,1), opacity );
vec4 texelColor = texture2D( map, vUv );
diffuseColor *= texelColor;
outgoingLight = diffuseColor.rgb;
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
gl_FragColor.rgb = toneMapping( gl_FragColor.rgb );
//gl_FragColor = linearToOutputTexel( gl_FragColor );
// Just do a single sample
float smoothing = 1.0/32.0;
// float smoothing = fwidth(texelColor.r);
vec4 color = getSDFColor(vUv, v_outlineWidth, v_outlineColor, smoothing);
gl_FragColor = color;
}
</script>
<script>
function convertToSDF(canvas) {
let ctx = canvas.getContext("2d");
let cw = canvas.width, ch = canvas.height;
let sdfValues = calcSDF(canvas, {
cutoff: 0.25,
radius: 16.0,
});
let imgData = ctx.getImageData(0, 0, cw, ch);
for (let i = 0; i < cw; i++) {
for (let j = 0; j < ch; j++) {
let baseIndex = j * cw + i;
let alpha = sdfValues[baseIndex] * 255;
let imageIndex = baseIndex * 4;
imgData.data[imageIndex + 0] = alpha;
imgData.data[imageIndex + 1] = alpha;
imgData.data[imageIndex + 2] = alpha;
imgData.data[imageIndex + 3] = alpha;
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
function generateCanvas(text) {
let canvas = document.createElement( 'canvas' );
let context = canvas.getContext( '2d' );
let size = 48;
context.font = size + 'px Microsoft YaHei';
let strLst = text.split('\n');
let maxWdith = -Infinity;
for (let str of strLst) {
let measured = context.measureText(str);
if (maxWdith < measured.width) {
maxWdith = measured.width;
}
}
canvas.width = maxWdith+20; //根据文字内容获取宽度
let lineHeight = size * 1.5; // fontsize * 1.5
canvas.height = lineHeight * strLst.length;
// let obj = computeFontSize(text, '40px', 'Microsoft YaHei');
// console.log("generateCanvas, ", obj);
strLst.forEach((str, index) => {
context.beginPath();
context.font = size + 'px Microsoft YaHei';
context.fillStyle = "#ccff8b";
context.fillText(str,10,size * (1 + index));
context.fill();
});
// return canvas;
return convertToSDF(canvas);
}
//根据canvas图形制作sprite
function makeCanvasSprite(canvas){
let texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
let uniforms = { 'opacity': {value: 1}, "center": {value: new THREE.Vector2(0.5, 0.5)},
"map": {value:texture}, "outline_color": {value: new THREE.Color('#52ff8a')}
, "outline_width": {value: 0.25}, "fill_color": {value: new THREE.Color('#ff3f27')} };
let spriteMaterial = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
side: THREE.DoubleSide
} );
spriteMaterial.transparent = true;
let sprite = new THREE.Sprite(spriteMaterial);
sprite.center = new THREE.Vector2(0.5, 0.5);
let poi = {w: canvas.width, h: canvas.height};
sprite.scale.set(poi.w/window.innerHeight, poi.h/window.innerHeight, 1.0);
return sprite;
}
let twoPi = Math.PI * 2;
let gui = new dat.GUI();
let scene = new THREE.Scene();
scene.background = new THREE.Color( 0x444444 );
scene.add(new THREE.AxesHelper(200));
let boxGeo = new THREE.BoxBufferGeometry(5, 5, 5);
let boxMesh = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({color:"#6e8989", wireframe: true}));
boxMesh.position.set(7, 10, 0);
scene.add(boxMesh);
let canvas = generateCanvas("你好啊");
let sprite = makeCanvasSprite(canvas);
scene.add(sprite);
let camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
camera.position.z = 30;
let renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
let orbit = new THREE.OrbitControls( camera, renderer.domElement );
// orbit.enableZoom = false;
let render = function () {
requestAnimationFrame( render );
renderer.render( scene, camera );
};
window.addEventListener( 'resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
render();
</script>
</body>
</html>
/*
* https://github.com/dy/bitmap-sdf
* Calculate SDF for image/bitmap/bw data
* This project is a fork of MapBox's TinySDF that works directly on an input Canvas instead of generating the glyphs themselves.
*/
var INF = 1e20;
function clamp(value, min, max) {
return min < max
? (value < min ? min : value > max ? max : value)
: (value < max ? max : value > min ? min : value)
}
function calcSDF(src, options) {
if (!options) options = {}
var cutoff = options.cutoff == null ? 0.25 : options.cutoff
var radius = options.radius == null ? 8 : options.radius
var channel = options.channel || 0
var w, h, size, data, intData, stride, ctx, canvas, imgData, i, l
// handle image container
if (ArrayBuffer.isView(src) || Array.isArray(src)) {
if (!options.width || !options.height) throw Error('For raw data width and height should be provided by options')
w = options.width, h = options.height
data = src
if (!options.stride) stride = Math.floor(src.length / w / h)
else stride = options.stride
}
else {
if (window.HTMLCanvasElement && src instanceof window.HTMLCanvasElement) {
canvas = src
ctx = canvas.getContext('2d')
w = canvas.width, h = canvas.height
imgData = ctx.getImageData(0, 0, w, h)
data = imgData.data
stride = 4
}
else if (window.CanvasRenderingContext2D && src instanceof window.CanvasRenderingContext2D) {
canvas = src.canvas
ctx = src
w = canvas.width, h = canvas.height
imgData = ctx.getImageData(0, 0, w, h)
data = imgData.data
stride = 4
}
else if (window.ImageData && src instanceof window.ImageData) {
imgData = src
w = src.width, h = src.height
data = imgData.data
stride = 4
}
}
size = Math.max(w, h)
//convert int data to floats
if ((window.Uint8ClampedArray && data instanceof window.Uint8ClampedArray) || (window.Uint8Array && data instanceof window.Uint8Array)) {
intData = data
data = Array(w * h)
for (i = 0, l = intData.length; i < l; i++) {
data[i] = intData[i * stride + channel] / 255
}
}
else {
if (stride !== 1) throw Error('Raw data can have only 1 value per pixel')
}
// temporary arrays for the distance transform
var gridOuter = Array(w * h)
var gridInner = Array(w * h)
var f = Array(size)
var d = Array(size)
var z = Array(size + 1)
var v = Array(size)
for (i = 0, l = w * h; i < l; i++) {
var a = data[i]
gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2)
gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2)
}
edt(gridOuter, w, h, f, d, v, z)
edt(gridInner, w, h, f, d, v, z)
var dist = window.Float32Array ? new Float32Array(w * h) : new Array(w * h)
for (i = 0, l = w * h; i < l; i++) {
dist[i] = clamp(1 - ((gridOuter[i] - gridInner[i]) / radius + cutoff), 0, 1)
}
return dist
}
// 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/
function edt(data, width, height, f, d, v, z) {
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
f[y] = data[y * width + x]
}
edt1d(f, d, v, z, height)
for (y = 0; y < height; y++) {
data[y * width + x] = d[y]
}
}
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
f[x] = data[y * width + x]
}
edt1d(f, d, v, z, width)
for (x = 0; x < width; x++) {
data[y * width + x] = Math.sqrt(d[x])
}
}
}
// 1D squared distance transform
function edt1d(f, d, v, z, n) {
v[0] = 0;
z[0] = -INF
z[1] = +INF
for (var q = 1, k = 0; q < n; q++) {
var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
while (s <= z[k]) {
k--
s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
}
k++
v[k] = q
z[k] = s
z[k + 1] = +INF
}
for (q = 0, k = 0; q < n; q++) {
while (z[k + 1] < q) k++
d[q] = (q - v[k]) * (q - v[k]) + f[v[k]]
}
}