Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

유저의 눈을 훔치는 인터렉티브 웹페이지 - I/O Extended 2023 Inche...

유저의 눈을 훔치는 인터렉티브 웹페이지 - I/O Extended 2023 Incheon 윤창현

Google I/O Extended 2023 Incheon - Web - 윤창현
유저의 눈을 훔치는 인터렉티브 웹페이지 (WebGL, Three.js, GSAP)

Changhyeon Yoon

August 26, 2023
Tweet

More Decks by Changhyeon Yoon

Other Decks in Technology

Transcript

  1. LIKELION NFT Division - Frontend Engineer Granter - Software Engineer

    (Frontend, DevOps) Microsoft Learn Student Ambassadors Google Developer Student Clubs - Hanyang Univ. 1st Lead Next.js Korea User Group - Organizer AWS Korea User Group - Frontend, Beginners, Pangyo Changhyeon Yoon Introduction ChanghyeonYoon yooniverse.dev Changhyeon Yoon
  2. 9

  3. 10

  4. 11

  5. 사용자가 원하는 그림을 화면에 표시하는 것을 담당하는 라이브러리 C언어 기반

    GPU를 이용한 하드웨어 가속화를 통해 렌더링을 하며 주로 데스크탑 어플리케이션 등에 사용 Graphics Library OpenGL 자바스크립트를 사용. HTML Canvas에 2D, 3D를 표현 WebGL Section 02
  6. WebGL™ is a cross-platform, royalty-free open web standard for a

    low-level 3D graphics API based on OpenGL ES, exposed to ECMAScript via the HTML5 Canvas element. Developers familiar with OpenGL ES 2.0 will recognize WebGL as a Shader-based API using GLSL, with constructs that are semantically similar to those of the underlying OpenGL ES API. It stays very close to the OpenGL ES specification, with some concessions made for what developers expect out of memory-managed languages such as JavaScript. WebGL 1.0 exposes the OpenGL ES 2.0 feature set; WebGL 2.0 exposes the OpenGL ES 3.0 API. Browser Support WebGL Section 02 IE 11+ iOS 8+
  7. 정점의 최종 위치를 설정 각 픽셀의 최종 색상을 설정 정점

    셰이더 프레그먼트 셰이더 셰이더 (Shader) Section 02
  8. // 버텍스 셰이더 attribute vec4 a_position; uniform mat4 u_matrix; //4x4

    변환 행렬 유니폼 void main() { gl_Position = u_matrix * a_position; } // 프래그먼트 셰이더 precision mediump float; void main() { gl_FragColor = vec4(1, 0, 0.5, 1); // 붉은 보라색을 반환합니다. }
  9. var primitiveType = gl.TRIANGLES; var offset = 0; var count

    = 9; gl.drawArrays(primitiveType, offset, count);
  10. //위치 데이터를 다루는 어트리뷰트 attribute vec4 a_position; //변환 행렬 유니폼

    uniform mat4 u_matrix; //각 위치에 대한 색값을 가지는 배어링 //프래그먼트 셰이더로 전달됨 varying vec4 v_color; void main() { //변환행렬에 위치를 곱한다. gl_Position = u_matrix * a_position; //클립공간을 색공간으로 변환 //클립공간은 -1.0 ~ 1.0의 범위인데, //이것을 0.0 ~ 1.0의 색공간 값으로 변경 v_color = gl_Position * 0.5 + 0.5; } precision mediump float; //버텍스 셰이더에서 받은 색값 varying vec4 v_color; void main() { gl_FragColor = v_color; }
  11. //위치 데이타를 GPU 버퍼에 공급 var positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,

    positionBuffer); //삼각형 각 꼭짓점의 좌표 var positions = [ 0, 0.7, -0.7, -0.7, 0.7, -0.7 ]; gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW );
  12. var matrixUniformLocation = gl.getUniformLocation(program, "u_matrix"); //프로그램에 변환행렬 설정(아래는 단위 행렬로

    아무 변환 안함) gl.uniformMatrix4fv(matrixUniformLocation, false, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]);
  13. //GPU에 등록된 버퍼의 위치 데이타를 //어떻게 위치 애트리뷰트 참조하는지 셋팅

    gl.enableVertexAttribArray(positionAttributeLocation); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); var size = 2; var type = gl.FLOAT; var normalize = false; var stride = 0; var offset = 0; gl.vertexAttribPointer( positionAttributeLocation, size, type, normalize, stride, offset );
  14. //렌더링 실시 var primitiveType = gl.TRIANGLES; var offset = 0;

    var count = positions.length / 2; gl.drawArrays(primitiveType, offset, count);
  15. 3D JavaScript Library Renderers: WebGL, <canvas>, <svg>, CSS3D / DOM,

    …etc Scenes, Cameras, Geometry, 3D Model Loaders, Lights, Materials, Shaders, Particles, Animation, Math Utilities three.js Section 02
  16. const scene = new THREE.Scene(); const aspect = window.innerWidth /

    window.innerHeight; const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); const div = document.getElementById("box"); div.appendChild(renderer.domElement);
  17. const render = () => { requestAnimationFrame(render); cube.rotation.x += 0.01;

    cube.rotation.y += 0.01; renderer.render(scene, camera); }; render();
  18. const group = new THREE.Group(); scene.add( group ); group.add( mesh1

    ); group.add( mesh2 ); mesh2.visible = false; group.remove( mesh2 ); group.children // mesh1 group.parent // scene
  19. - 시야각이 커질 수록 카메라가 담는 화면의 영역이 넓어짐 -

    보통 단위는 Radian을 사용 fov = field of view(시야 각) Section 06 aspect - fixel ratio : 가로/세로의 비율 near / far - 카메라가 볼수있는 최소, 최대 거리 - far는 near보다 항상 커야한다.
  20. 가장 기본적인 절두체(frustum)을 만드는카메라. 주로 2D 요소를 표현하기 위해 사용

    left, right, top, bottom, near, far 로 육면체를 정의하여 사용 PerspectiveCamera OrthographicCamera 카메라가 특정 대상을 중심으로 공전 별도 모듈이므로 따로 로드해야함. OrbitControls Section 06
  21. 자연광 반구광 AmbientLight HemisphereLight 직사광, 태양표현 DirectionalLight 한 점에서 뻗어나가는

    광원 빛이 원뿔처럼 퍼져나가는 광원 PointLight SpotLight 사각형태의 조명 RectAreaLight Section 07
  22. // normalized device coordinates mouse.x = ( event.clientX / window.innerWidth

    ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; raycaster = new THREE.Raycaster(); raycaster.setFromCamera( mouse, camera ); var intersects = raycaster.intersectObjects( scene.children ); INTERSECTED = intersects[ 0 ].object;
  23. // normalized device coordinates mouse.x = ( event.clientX / window.innerWidth

    ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; raycaster = new THREE.Raycaster(); raycaster.setFromCamera( mouse, camera ); var intersects = raycaster.intersectObjects( scene.children ); INTERSECTED = intersects[ 0 ].object;
  24. // normalized device coordinates mouse.x = ( event.clientX / window.innerWidth

    ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; raycaster = new THREE.Raycaster(); raycaster.setFromCamera( mouse, camera ); var intersects = raycaster.intersectObjects( scene.children ); INTERSECTED = intersects[ 0 ].object;
  25. function render() { theta += 0.1; camera.current.position.x = radius *

    Math.sin(THREE.Math.degToRad(theta)); camera.current.position.y = radius * Math.sin(THREE.Math.degToRad(theta)); camera.current.position.z = radius * Math.cos(THREE.Math.degToRad(theta)); camera.current.lookAt(scene.position); // find intersections const vector = new THREE.Vector3(mouse.current.x, mouse.current.y, 1).unproject(camera.current); raycaster.set(camera.current.position, vector.sub(camera.current.position).normalize()); const intersects = raycaster.intersectObjects(scene.children); if (intersects.length > 0) { if (INTERSECTED.current != intersects[0].object) { if (INTERSECTED.current) INTERSECTED.current.material.emissive.setHex(INTERSECTED.current.currentHex); INTERSECTED.current = intersects[0].object; INTERSECTED.current.currentHex = INTERSECTED.current.material.emissive.getHex(); INTERSECTED.current.material.emissive.setHex(0xff0000); } } else { if (INTERSECTED.current) INTERSECTED.current.material.emissive.setHex(INTERSECTED.current.currentHex); INTERSECTED.current = null; } renderer.current.render(scene, camera.current); }
  26. const BackgroundGradient = (colorA, colorB) => { var mesh =

    new THREE.Mesh( new THREE.PlaneBufferGeometry(2, 2, 1, 1), new THREE.ShaderMaterial({ uniforms: { uColorA: { value: new THREE.Color(colorA) }, uColorB: { value: new THREE.Color(colorB) }, }, vertexShader: ` varying vec2 vUv; void main(){ vUv = uv; float depth = -1.; //or maybe 1. you can experiment gl_Position = vec4(position.xy, depth, 1.); } `, fragmentShader: ` varying vec2 vUv; uniform vec3 uColorA; uniform vec3 uColorB; void main(){ gl_FragColor = vec4( mix( uColorA, uColorB, vec3(vUv.y)), 1. ); } `, }), ); mesh.material.depthWrite = false; mesh.renderOrder = -99999; return mesh; };
  27. getRipMaterial(side) { const material = new THREE.ShaderMaterial({ defines: { HEIGHT:

    this.sheetSettings.height, WIDTH: this.sheetSettings.width / 2, FULL_WIDTH: this.sheetSettings.width, HEIGHT_SEGMENTS: this.sheetSettings.heightSegments, WIDTH_SEGMENTS: this.sheetSettings.widthSegments, }, uniforms: { uMap: { value: this.photoTexture }, uRip: { value: this.ripTexture }, uBorder: { value: this.borderTexture }, uRipSide: { value: this.sheetSettings[side].ripSide }, uTearWidth: { value: this.sheetSettings.tearWidth }, uWhiteThreshold: { value: this.sheetSettings.ripWhiteThreshold }, uTearAmount: { value: this.sheetSettings.tearAmount }, uTearOffset: { value: this.sheetSettings.tearOffset }, uUvOffset: { value: this.sheetSettings[side].uvOffset }, uTearXAngle: { value: this.sheetSettings[side].tearXAngle }, uTearYAngle: { value: this.sheetSettings[side].tearYAngle }, uTearZAngle: { value: this.sheetSettings[side].tearZAngle }, uTearXOffset: { value: this.sheetSettings[side].tearXOffset }, uXDirection: { value: this.sheetSettings[side].direction }, uShadeColor: { value: this.sheetSettings[side].shadeColor }, uShadeAmount: { value: this.sheetSettings[side].shadeAmount }, }, transparent: true, vertexShader: ripVertexShader, fragmentShader: ripFragmentShader, }); return material; }
  28. remove() { this.interactive = false; this.destroyCallback(); const removeAnimation = gsap.timeline({

    defaults: { duration: 1, ease: "power2.in" }, onComplete: () => this.destroyMe(), }); removeAnimation.to(this.sheetSettings, { tearAmount: 1.5 + Math.random() * 1.5, ease: "power2.out", onUpdate: () => this.updateUniforms(), }); removeAnimation.to(this.group.position, { z: 1 }, 0); this.sides.forEach((side) => { removeAnimation.to( side.mesh.position, { y: -3 + Math.random() * -3, x: (2 + Math.random() * 3) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0, ); removeAnimation.to( side.mesh.rotation, { z: (-2 + Math.random() * -3) * (this.sheetSettings[side.id].ripSide - 0.5) }, 0, ); }); }
  29. Resources WebGL Fundamentals - https://webglfundamentals.org Three.js Example - https://threejs.org/examples Three.js

    Intro - https://www.youtube.com/watch?v=6eLl8yQnxHQ GSAP Docs - https://greensock.com/ React Demo - https://interactive.changhyeon.net Source Code - https://github.com/ChanghyeonYoon/interactive