Видео+код: #2/14 THREEJS градиентный полупрозрачный шейдер+новые данные для формирования карты планеты
Статья создана:Видео: 16 THREEJS координаты земли
урок 16 по Three JS / урок 14 по планете
Файлы из урока 14 по 3D планете
GitHUBJS Код из видео (!на threejs-webpack-starter!)
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {BufferGeometryUtils} from 'three/examples/jsm/utils/BufferGeometryUtils'
import {TWEEN} from 'three/examples/jsm/libs/tween.module.min'
import anime from 'animejs/lib/anime'
//import * as dat from 'dat.gui'
//const gui = new dat.GUI()
const do_cument=document;
const ca_nvas=do_cument.querySelector('.webgl');
if(!ca_nvas)throw 'no canvas.webgl';
const sizes = {width: parseInt(window.getComputedStyle(ca_nvas).width), height: parseInt(window.getComputedStyle(ca_nvas).height)}
let o;
const scene = new THREE.Scene();
//scene.background = new THREE.Color('blue');
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.01, 50);
camera.position.set(0, 10, 10);
const renderer = new THREE.WebGLRenderer({canvas:ca_nvas,antialias: true,alpha: true});
renderer.setSize(sizes.width , sizes.height);
renderer.setClearColor(0x000000, 0);
const controls = new OrbitControls(camera, renderer.domElement);
//controls.enablePan = true;
const group=new THREE.Group();// empty groupe for add all rotation object
const params = {
colors: {
gradInner: '#c7c7c7', // HEX Color | Inner gradient of "boom"
gradOuter: '#464646', // HEX Color | Outer gradient of "boom"
lineColor: '#cd390b', // HEX Color | Default color lines
},
mapPoints:{
base: '#ffffff',// HEX Color | Base map color | FOR COLOR USE #FFFFFF -> AND ANY HEX COLOR VALUE
sizeOfPoints:0.5,// !FLOAT ONLY! | MIN: 0.1 , MAX: 0.4
opacityOfOceanPoints:0.1,// !FLOAT ONLY! | ex. 0.1 | MIN: 0.1 - black, MAX: 0.9
countOfPoints:25000,// INT ONLY | ex. 1000 - 40000 | The more — the more points on the planet, but the more difficult the calculations
showBackMap:true, // BOOLEAN | Removes the view from the planet map that is in the background: ;
showSphereToHideBackSide:false, // BOOLEAN | IF TRUE, showBackMap = false || Shows an additional sphere, as if under the map of the planet. This sphere hides the background of the map.
hiddenShpereColor:'#0000ff',// HEX Color | If you want to disable showing the background of the planet map, then an additional object is created in the form of a sphere, which also hides some elements on the back of the planet, which is, as it were, in the background from you
hiddenSphereOpacity:.1,
},
reset: ()=>controls.reset()
}
// An array for forming lines, "boom", as well as sticks (highlighting the point where the line arrives)
//The point of arrival of the line and the point from where it flies is made up of two array elements: 0, 1; 2, 3 and so on.
//In the first object of one of the points (the first one is where the line flies from), you can add some user data in order to unify the default settings
//!!! Only two values in it are mandatory: latitude and longitude
const data=[
// This forms three objects: a line, a "boom", a stick
{
lat:32.622876, // REQUIRED | Float ex. 42.0 | Earth coordinate latitude
lon:107.523152, // REQUIRED | Float ex. 42.0 | Earth coordinate longitude
lineSpeed:2, // Integer | Default 2 | min ≈1, max ≈20 | It's speed - how fast does the animation of the line go from point A to point B
lineWidth:1,// Float | min ≈.1, max ≈10 | Worked only on Linux system | ex. for randomization it: THREE.Math.randFloat(.5, 2).toFixed(2) | Arrives line width — https://stackoverflow.com/questions/11638883/thickness-of-lines-using-three-linebasicmaterial
lineColor:'#ff0000',// HEX Color | Default params.colors.lineColor | Line color in HEX, ex. 0xffffff - it's white
lineRepeats:100, // Infinity or Integer || 1, 2, 1000, Infinity | Number of line flight repetitions
boomNeed:true,// Boolean | 'Boom' is added by default | If you do not need "boom", then set the value to false. By default, "boom" passes
boomSpeed: 5000,// Integer | Default (some random): THREE.Math.randInt(2500, 5000) | min ≈500 , max ≈5000 || THREE.Math.randomInt(2500, 5000)
boomRadius: 2, // Integer | Default (some random): [5 * THREE.Math.randFloat(.2, .7)] | min ≈.5 , max ≈3 || 5 * THREE.Math.randFloat(.2, .7)
boomRepeat:100,// Infinity or Integer | Default: Infinity | 1, 2, 1000, Infinity | Number of repeats "boom"
showStick:true, // Boolean | Default: false | A line from the point where the "boom" arrives
stickColorTo:'#ff0000',// HEX Color | Default #ffffff | Arrives line color in HEX, ex. 0xffffff - it's white | To create gradient
stickColorFrom:'#000000',// HEX Color | Default #ffffff | Arrives line color in HEX, ex. 0xffffff - it's white | To create gradient
stickHeight:2, // Integer or Float | Default 1.1 | min ≈1, max ≈5 | ex. for randomization it: THREE.Math.randFloat(.5, 2).toFixed(2) | Arrives line height
stickWidth:.2, // Float | Default 0.1 | min ≈.01, max ≈.2 | ex. for randomization it: THREE.Math.randFloat(.5, 2).toFixed(2) | Arrives line height
},//FROM 1 China
{lat:-26.164493,lon:134.742407},//TO 1 Australia
// \\ This forms three objects: a line, a "boom", a stick
{
lat:7.466688, lon:19.987692,
lineSpeed:5,
lineColor:'#ff0000',
boomNeed:true,
boomSpeed: 3500,
boomRadius: 3,
boomRepeat:100,
lineRepeats:100,
showStick:true,
stickColorTo:'#00ff00',
stickColorFrom:'#ffffff',
stickHeight:1.5,
stickWidth:.05,
},//FROM 2 // Central Africa
{lat:-15.860255, lon:-58.059177},//TO 2 // Central South America
{
lat:48.358527, lon:-99.761561,
lineSpeed:5,
lineColor:'#333333',
boomNeed:false,
boomSpeed: 3500,
boomRadius: 3,
boomRepeat:100,
lineRepeats:100,
showStick:true,
stickColorTo:'#0000ff',
stickColorFrom:'#ff0000',
stickHeight:1,
stickWidth:.1,
},//FROM 3 // South Amer
{lat:76.910298, lon:-40.348415},//TO 3 // Greenland
]
const maxImpactAmount = data.length/2; // Constant for determining the number "boom"
function isFloat(n){return Number(n) === n && n % 1 !== 0;} // Flote of numbers
//Checking the correspondence of the quantity of data in the object with data. If the data is odd, then the rest of the script will not work. Since the data is probably violated.
if(!Number.isInteger(data.length/2%2)){
throw new Error('Check data array. The number of array elements is odd!')
}
const impacts = []; // Array for "boom"
const trails = []; // Array for animated lines
let tmp=0, // For the cycle that sorting out the values of the object with the data
tmp1=0, // ~^~
isMapLoaded=false // Determines when the planet card is formed
const tweenGroup = new TWEEN.Group()
const easing='easeInOutSine'// https://codepen.io/kcjpop/pen/GvdQdX
for(let i=0;i<data.length/2;i++){ // The cycle that sorting out the values of the object with the data
if( // We check the availability of strictly necessary data in the array with data
!data[tmp1].lat
||!data[tmp1].lon
||!isFloat(data[tmp1].lat)
||!isFloat(data[tmp1].lon)
||!data[tmp1+1].lat
||!data[tmp1+1].lon
||!isFloat(data[tmp1+1].lat)
||!isFloat(data[tmp1+1].lon)
){ // If the check has not passed, then we stop the script
console.log(data[tmp1],data[tmp1+1]);
throw new Error('Check data lat OR lon!')
}
const whereItArrives=cTv(data[tmp1+1]); // Constant taking the value where the line flies
if(!data[tmp1].stickHeight)data[tmp1].stickHeight=1.1; // Setting the default value, in the absence of data from an object with data
if(data[tmp1].showStick){
const material = new THREE.ShaderMaterial({//https://discourse.threejs.org/t/draw-a-line-with-a-simple-single-colour-fading-gradient/1775/32
side:THREE.DoubleSide,
uniforms: {
color: {value: new THREE.Color(data[tmp1].stickColorTo || 0xffffff)},
color2: {value: new THREE.Color(data[tmp1].stickColorFrom ||0xffffff)},
origin: {value: new THREE.Vector3()},
limitDistance: {value: parseInt(data[tmp1].stickHeight*5)},
},
linewidth:1,
vertexShader: `
varying vec2 vUv; // We create a variable, then to convey it to a fragmentShader
varying vec3 vPos; // We create a variable, then to convey it to a fragmentShader
void main(){
vUv=uv;
vPos = position;
vec3 pos=position.xyz * sin(1.);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv; // We accept data from vertexShader
uniform vec3 color; // We accept data from uniforms
uniform vec3 color2; // We accept data from uniforms
uniform vec3 origin; // We accept data from uniforms
uniform float limitDistance; // We accept data from uniforms
varying vec3 vPos; // We accept data from vertexShader
void main() {
vec2 center = vec2((vUv.y - 1.)*1.,(vUv.y - 1.)*1.); // We set a conditional center for one cylinder
float distance = length(center); // Determine its size
float opacity = smoothstep(.3,1.,distance); // We make a soft fill
gl_FragColor = vec4(mix(color2,color, vUv.y), opacity);
}
`, transparent: true,
});
// Creating and positioning the cylinder - sticks
const geometry = new THREE.CylinderBufferGeometry(0,data[tmp1].stickWidth || .1,data[tmp1].stickHeight || 1.1);
const mesh = new THREE.Mesh( geometry, material );
const stickHeight=data[tmp1].stickHeight*(1/data[tmp1].stickHeight+.085) || 1.05
mesh.position.set(whereItArrives.x*stickHeight,whereItArrives.y*stickHeight,whereItArrives.z*stickHeight);
mesh.lookAt(new THREE.Vector3()); // We ask him to look at his normal at the center of the planet
mesh.rotateX(Math.PI * -.5); // Since it has normal in the middle, before that he was “lying” on the planet, and this code makes him stand perpendicular to it
mesh.scale.set(0,0,0);
group.add(mesh)
let interval=setInterval(()=>{ // Some hack to give the opportunity to load the planet’s map itself, and then show the sticks themselves
if(isMapLoaded){
clearInterval(interval);
interval=undefined
anime({targets:mesh.scale,x:1,y:1,z:1,easing,duration:2000,}) // We animize the scale
anime({targets:mesh.position, // We animize the position
x:[whereItArrives.x*.9,whereItArrives.x*stickHeight],
y:[whereItArrives.y*.9,whereItArrives.y*stickHeight],
z:[whereItArrives.z*.9,whereItArrives.z*stickHeight],
delay:1000,easing,duration:2000,})
}
},100);
}
const o=Object.create({ // We create an object for the subsequent creation of "boom" and lines
prevPosition: cTv(data[tmp1]),
impactPosition: whereItArrives,
impactMaxRadius: parseFloat(data[tmp1].boomRadius) || 5 * THREE.Math.randFloat(.2, .7),
impactRatio: 0,
trailRatio: {value: 0},
trailLength: {value: 0},
})
impacts.push(o);
if(data[tmp1].boomNeed===undefined || data[tmp1].boomNeed!==false){ // If the “boom” is not indicated in the object with data for a particular object, then by default it will be shown
new TWEEN.Tween({ value:0},tweenGroup)
.to({ value: 1 }, parseInt(data[tmp1].boomSpeed) || THREE.Math.randInt(2500, 5000))
.onUpdate(val => {o.impactRatio = val.value}).start().repeat(data[tmp1].boomRepeat || Infinity)
}
// Lines
makeTrail(i,data[tmp1].lineColor || 0xffffff,data[tmp1].lineWidth || .1); // Creating the line itself. This function also fills the trails array
const path = trails[i];
const speed = data[tmp1].lineSpeed || 2;
const t=new TWEEN.Tween({value: 0}) // We anmile "boom" and lines
.to({value: 1}, path.geometry.attributes.lineDistance.array[99] / speed * 1000)
.onUpdate( val => {o.trailRatio.value = val.value})
//t.chain(w)
t.start().repeat(data[tmp1].lineRepeats || Infinity)
if(tmp===1){
tmp1+=2; tmp=0
}else{
tmp++; (tmp1===0)?tmp1=2:tmp1++
}
}
const uniforms = { // For Shader with "boom"
impacts: {value: impacts},
maxSize: {value: .04},
minSize: {value: .03},
waveHeight: {value: .125},
scaling: {value: 2},
gradInner: {value: new THREE.Color(params.colors.gradInner)},
gradOuter: {value: new THREE.Color(params.colors.gradOuter)}
};
(()=>{ // Creation of the planet map
const dummyObj = new THREE.Object3D()
const p = new THREE.Vector3()
const sph = new THREE.Spherical()
const geoms = new Array()
const tex = new THREE.TextureLoader().load('/map.jpg',()=>{
// https://web.archive.org/web/20120107030109/http://cgafaq.info/wiki/Evenly_distributed_points_on_sphere#Spirals
const counter = params.mapPoints.countOfPoints
const rad = 5
let r = 0
const dlong = Math.PI * (3 - Math.sqrt(5))
const dz = 2 / counter
let long = 0
let z = 1 - dz / 2
for(let i = 0; i < counter; i++){
r = Math.sqrt(1 - z * z);
p.set( Math.cos(long) * r, z, -Math.sin(long) * r).multiplyScalar(rad);
z = z - dz;
long = long + dlong;
sph.setFromVector3(p);
dummyObj.lookAt(p);
dummyObj.updateMatrix();
const g = new THREE.PlaneBufferGeometry(2, 2);
g.applyMatrix4(dummyObj.matrix);
g.translate(p.x, p.y, p.z);
const centers = [
p.x, p.y, p.z,
p.x, p.y, p.z,
p.x, p.y, p.z,
p.x, p.y, p.z
];
const uv = new THREE.Vector2(
(sph.theta + Math.PI) / (Math.PI * 2),
1. - sph.phi / Math.PI
);
const uvs = [
uv.x, uv.y,
uv.x, uv.y,
uv.x, uv.y,
uv.x, uv.y
];
g.setAttribute("center", new THREE.Float32BufferAttribute(centers, 3));
g.setAttribute("bUv", new THREE.Float32BufferAttribute(uvs, 2));
geoms.push(g);
}
const g = BufferGeometryUtils.mergeBufferGeometries(geoms);
if(params.mapPoints.showSphereToHideBackSide)params.mapPoints.showBackMap=false;
let sideOfMap=(params.mapPoints.showBackMap)?THREE.DoubleSide:THREE.FrontSide;
if(!params.mapPoints.showBackMap && params.mapPoints.showSphereToHideBackSide){ // Add sphere hide
let isTransparent=true;
if(params.mapPoints.hiddenSphereOpacity===undefined || params.mapPoints.hiddenSphereOpacity === 1)isTransparent=false
scene.add(
new THREE.Mesh(
new THREE.IcosahedronBufferGeometry(rad-.005,16),
new THREE.MeshBasicMaterial({
color:params.mapPoints.hiddenShpereColor || '#000000',
transparent:isTransparent,
opacity:params.mapPoints.hiddenSphereOpacity || 1,
})
)
)
}
const m = new THREE.MeshBasicMaterial({
color: new THREE.Color(params.mapPoints.base),
side: sideOfMap,
transparent:true,
onBeforeCompile: shader => {
shader.uniforms.impacts = uniforms.impacts;
shader.uniforms.maxSize = uniforms.maxSize;
shader.uniforms.minSize = uniforms.minSize;
shader.uniforms.waveHeight = uniforms.waveHeight;
shader.uniforms.scaling = uniforms.scaling;
shader.uniforms.gradInner = uniforms.gradInner;
shader.uniforms.gradOuter = uniforms.gradOuter;
shader.uniforms.tex = {value: tex};
shader.vertexShader = `
struct impact {
vec3 impactPosition;
float impactMaxRadius;
float impactRatio;
};
uniform impact impacts[${maxImpactAmount}];
uniform sampler2D tex;
uniform float maxSize;
uniform float minSize;
uniform float waveHeight;
uniform float scaling;
attribute vec3 center;
attribute vec2 bUv;
varying float vFinalStep;
varying float vMap;
${shader.vertexShader}
`.replace(
`#include <begin_vertex>`,
`#include <begin_vertex>
float finalStep = 0.0;
for (int i = 0; i < ${maxImpactAmount};i++){
float dist = distance(center, impacts[i].impactPosition);
float curRadius = impacts[i].impactMaxRadius * impacts[i].impactRatio;
float sstep = smoothstep(0., curRadius, dist) - smoothstep(curRadius - (.25 * impacts[i].impactRatio ), curRadius, dist);
sstep *= 1. - impacts[i].impactRatio;
finalStep += sstep;
}
finalStep = clamp(finalStep, 0., 1.);
vFinalStep = finalStep;
float map = texture(tex, bUv).g;
vMap = map;
float pSize = map < 0.5 ? maxSize : minSize;
float scale = scaling;
transformed = (position - center) * pSize * mix(1., scale * 1.25, finalStep) + center; // scale on wave
transformed += normal * finalStep * waveHeight; // lift on wave
`
);
shader.fragmentShader = `
uniform vec3 gradInner;
uniform vec3 gradOuter;
varying float vFinalStep;
varying float vMap;
${shader.fragmentShader}
`.replace(
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`// shaping the point, pretty much from The Book of Shaders
vec2 hUv = (vUv - .1);
int N = 8;
float a = atan(hUv.x,hUv.y);
float r = PI2/float(N);
float d = cos(floor(.5+a/r)*r-a)*length(hUv);
float f = cos(PI / float(N)) * .5;
//if (d > f) discard;
if (length(vUv - ${params.mapPoints.sizeOfPoints}) > ${params.mapPoints.sizeOfPoints}) discard;
vec3 grad = mix(gradInner, gradOuter, clamp( d / f, 0., 1.)); // gradient
vec3 diffuseMap = diffuse * ((vMap > .5) ? ${params.mapPoints.opacityOfOceanPoints} : 1.);
vec3 col = mix(diffuseMap, grad, vFinalStep); // color on wave
float opct=(vMap > .5)?${params.mapPoints.opacityOfOceanPoints}:1.;
vec4 diffuseColor = vec4( col , opct );
`);
}
});
m.defines = {"USE_UV":""};
o = new THREE.Mesh(g, m);
trails.forEach(t => group.add(t));
group.add(o);
isMapLoaded=!isMapLoaded
})
})()
function makeTrail(idx,color,lineWidth){ // Creation of lines
const pts = new Array(100 * 3).fill(0);
const g = new THREE.BufferGeometry();
g.setAttribute("position", new THREE.Float32BufferAttribute(pts, 3));
const m = new THREE.LineDashedMaterial({
color: color || params.colors.lineColor,
linewidth: lineWidth || 1,
transparent: true,
onBeforeCompile: shader => {
shader.uniforms.actionRatio = impacts[idx].trailRatio;
shader.uniforms.lineLength = impacts[idx].trailLength;
shader.fragmentShader = `
uniform float actionRatio;
uniform float lineLength;
${shader.fragmentShader}
`.replace(
`if ( mod( vLineDistance, totalSize ) > dashSize ) {
discard;
}`,
` float halfDash = dashSize * .5;
float currPos = (lineLength + dashSize) * actionRatio;
float d = (vLineDistance + halfDash) - currPos;
if (abs(d) > halfDash ) discard;
float grad = ((vLineDistance + halfDash) - (currPos - halfDash)) / halfDash;
`
)
.replace(
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`vec4 diffuseColor = vec4( diffuse, grad );`
);
}
});
const l = new THREE.Line(g, m);
l.userData.idx = idx;
if(impacts[idx])setPath(l, impacts[idx].prevPosition, impacts[idx].impactPosition, 1);
trails.push(l);
}
// based on https://jsfiddle.net/prisoner849/fu59aved/
function setPath(l, startPoint, endPoint, peakHeight, cycles) {
const pos = l.geometry.attributes.position;
const division = pos.count - 1;
const cycle = cycles || 1;
const peak = peakHeight || 1;
const radius = startPoint.length();
const angle = startPoint.angleTo(endPoint);
const arcLength = radius * angle;
const diameterMinor = arcLength / Math.PI;
const radiusMinor = (diameterMinor * 0.5) / cycle;
const peakRatio = peak / diameterMinor;
const radiusMajor = startPoint.length() + radiusMinor;
const basisMajor = new THREE.Vector3().copy(startPoint).setLength(radiusMajor);
const basisMinor = new THREE.Vector3().copy(startPoint).negate().setLength(radiusMinor);
// triangle (start, end, center)
const tri = new THREE.Triangle(startPoint, endPoint, new THREE.Vector3());
const nrm = new THREE.Vector3(); // normal
tri.getNormal(nrm);
// rotate startPoint around normal
const v3Major = new THREE.Vector3();
const v3Minor = new THREE.Vector3();
const v3Inter = new THREE.Vector3();
const vFinal = new THREE.Vector3();
for (let i = 0; i <= division; i++) {
const divisionRatio = i / division;
const angleValue = angle * divisionRatio;
v3Major.copy(basisMajor).applyAxisAngle(nrm, angleValue);
v3Minor.copy(basisMinor).applyAxisAngle(nrm, angleValue + Math.PI * 2 * divisionRatio * cycle);
v3Inter.addVectors(v3Major, v3Minor);
const newLength = ((v3Inter.length() - radius) * peakRatio) + radius;
vFinal.copy(v3Inter).setLength(newLength);
pos.setXYZ(i, vFinal.x, vFinal.y, vFinal.z);
}
pos.needsUpdate = true;
l.computeLineDistances();
l.geometry.attributes.lineDistance.needsUpdate = true;
impacts[l.userData.idx].trailLength.value = l.geometry.attributes.lineDistance.array[99];
l.material.dashSize = 7
}
function cTv(coordObj={lat:48.5125,lon:2.2055}){//coordinates to vector | Default: Paris
const parisSpherical = {
lat: THREE.Math.degToRad(90 - coordObj.lat),
lon: THREE.Math.degToRad(coordObj.lon)
};
const radius = 5;// corresponds to the radius of the planet map
const vector=new THREE.Vector3().setFromSphericalCoords(
radius,
parisSpherical.lat,
parisSpherical.lon
);
return vector
}
scene.add(group) // Add a group that rotates: the map of the planet, "boom", lines, sticks
window.addEventListener( 'resize', onWindowResize )
renderer.setAnimationLoop( () => {
TWEEN.update()
tweenGroup.update()
group.rotation.y += 0.001
renderer.render(scene, camera)
})
//Fix to compute canvas width/height
setTimeout(()=>{onWindowResize()},1)
function onWindowResize() {
const sizes = {width: parseInt(window.getComputedStyle(ca_nvas).width), height: parseInt(window.getComputedStyle(ca_nvas).height)};
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize( sizes.width , sizes.height );
}
Расшифровка временных меток видео:
00:00 Welcome aboard
01:16 Общая информация
01:40 Особенности линий в WEBGL (OpenGL) на Linux/windows
02:43 Изменения по сравнению с пред. уроком
03:49 Размер render'а теперь зависит от размера canvas
07:07 Что будет ещё на планете?
08:56 Поясняю особенности размера render от canvas
16:55 Как узнать, какой цвет?
21:58 Новые данные в массиве для планеты, каждой линии, "бум", + палочки
27:40 Всё в THREEJS измеряется в метрах
35:12 Немного отладки кода...
40:41 Что ещё было добавлено/именено в коде?
40:51 Поясняю градиентный шейдер с прозрачностью ThreeJS (WEBGL)
53:28 Позиционирование цилиндра, относительно центра планеты
1:11:07 Особенности появления цилиндра
1:14:15 Появление остального кода
1:20:57 Что ещё необходимо добавить?
1:21:46 Fix для определения размера canvas
1:28:20 See you soon