Three.js ShaderMaterial

    技术2022-07-11  100

    自定义材质的使用,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]] } } function removeRags(strLst) { let arr = strLst.split(""); let map = {}, keys = []; for (let i = 0; i < arr.length; i++) { let str = arr[i]; if (map[str] == undefined) { map[str] = [i]; } else { map[str].push(i); if (keys.indexOf(str) < 0) { keys.push(str); } } } for (let i = keys.length - 1; i > -1; i--) { let key = keys[i]; let start = map[key][0] + 1, deleteCount = map[key][1] - map[key][0]; if (arr[start - 1] == key) { arr.splice(start, deleteCount); } } return arr.join(""); }

    Cesium 中的字体轮廓,《Labels.html》

    function setFont() { Sandcastle.declare(setFont); viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222), label: { text: "模型1", font: "16px Helvetica", fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 4, style: Cesium.LabelStyle.FILL_AND_OUTLINE, }, }); }

    outlineWidth 是如何传入shader的 LabelCollection.update() => BillboardCollection.update() BillboardCollection function createVAF()

    BillboardCollection var attributeLocationsBatched = {} BillboardCollection function writeSDF()

    VertexArrayFacade._verifyAttributes() VertexArray gl.vertexAttribPointer //指定了渲染时索引值为 index 的顶点属性数组的数据格式和位置。

    BillboardCollectionVS.glsl

    attribute vec2 sdf; // sdf outline color (rgb) and width (w) #ifdef SDF vec4 outlineColor; float outlineWidth; temp = sdf.x; temp = temp * SHIFT_RIGHT8; outlineColor.b = (temp - floor(temp)) * SHIFT_LEFT8; temp = floor(temp) * SHIFT_RIGHT8; outlineColor.g = (temp - floor(temp)) * SHIFT_LEFT8; outlineColor.r = floor(temp); temp = sdf.y; temp = temp * SHIFT_RIGHT8; float temp3 = (temp - floor(temp)) * SHIFT_LEFT8; temp = floor(temp) * SHIFT_RIGHT8; outlineWidth = (temp - floor(temp)) * SHIFT_LEFT8; outlineColor.a = floor(temp); outlineColor /= 255.0; v_outlineWidth = outlineWidth / 255.0; v_outlineColor = outlineColor; v_outlineColor.a *= translucency; #endif <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>chapter1.1</title> <script src="../external/three.js"></script> <script src="../controls/OrbitControls.js"></script> <style> body { overflow: hidden; padding: 0; margin: 0; } </style> </head> <body> <div id="container"></div> <script id="vertexShader" type="x-shader/x-vertex"> varying vec2 pos; void main() { pos = (vec2(position) + 1.0) * 0.5; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } </script> <script id="fragmentShader" type="x-shader/x-fragment"> uniform vec2 u_resolution; uniform float u_time; varying vec2 pos; // Plot a line on Y using a value between 0.0-1.0 float plot(vec2 st) { return smoothstep(0.02, 0.000, abs(st.y - st.x)); } void main() { float y = pos.x; vec3 color = vec3(y); // Plot a line float pct = plot(pos); color = (1.0 - pct) * color + pct * vec3(0.0, 1.0, 0.0); gl_FragColor = vec4(color, 1.0); } </script> <script> //https://thebookofshaders.com/05/?lan=ch var container; var camera, scene, renderer; var uniforms; init(); animate(); function init() { container = document.getElementById( 'container' ); camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(1, 1, 1); scene = new THREE.Scene(); scene.add(new THREE.AxesHelper(20)); var geometry = new THREE.PlaneBufferGeometry( 2, 2 ); uniforms = { u_time: { type: "f", value: 1.0 }, u_resolution: { type: "v2", value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }; var material = new THREE.ShaderMaterial( { uniforms: uniforms, vertexShader: document.getElementById( 'vertexShader' ).textContent, fragmentShader: document.getElementById( 'fragmentShader' ).textContent } ); var material2 = new THREE.MeshBasicMaterial({color: '#00bbbb', wireframe: true, side: THREE.DoubleSide}) var mesh = new THREE.Mesh( geometry, material ); scene.add( mesh ); renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio( window.devicePixelRatio ); container.appendChild( renderer.domElement ); let orbit = new THREE.OrbitControls( camera, renderer.domElement ); onWindowResize(); window.addEventListener( 'resize', onWindowResize, false ); } function onWindowResize( event ) { renderer.setSize( window.innerWidth, window.innerHeight ); // uniforms.u_resolution.value.x = renderer.domElement.width; // uniforms.u_resolution.value.y = renderer.domElement.height; } function animate() { requestAnimationFrame( animate ); render(); } function render() { uniforms.u_time.value += 0.05; renderer.render( scene, camera ); } </script> </body> </html>

    https://www.w3h5.com/post/450.html

    Processed: 0.013, SQL: 9