欢迎光临
我的个人博客网站

用 three.js 绘制三维带箭头线 (线内箭头)

在LineMaterial.js基础上修改的ArrowLineMaterial.js代码:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

/**  * @author WestLangley / http://github.com/WestLangley  *  * parameters = {  *  color: <hex>,  *  linewidth: <float>,  *  dashed: <boolean>,  *  dashScale: <float>,  *  dashSize: <float>,  *  gapSize: <float>,  *  resolution: <Vector2>, // to be set by renderer  * }  */  import {     ShaderLib,     ShaderMaterial,     UniformsLib,     UniformsUtils,     Vector2 } from "../build/three.module.js";  UniformsLib.line = {      linewidth: { value: 1 },     resolution: { value: new Vector2(1, 1) },     dashScale: { value: 1 },     dashSize: { value: 1 },     gapSize: { value: 1 } // todo FIX - maybe change to totalSize  };  ShaderLib['line'] = {      uniforms: UniformsUtils.merge([         UniformsLib.common,         UniformsLib.fog,         UniformsLib.line     ]),      vertexShader:         `         #include <common>         #include <color_pars_vertex>         #include <fog_pars_vertex>         #include <logdepthbuf_pars_vertex>         #include <clipping_planes_pars_vertex>          uniform float linewidth;         uniform vec2 resolution;          attribute vec3 instanceStart;         attribute vec3 instanceEnd;          attribute vec3 instanceColorStart;         attribute vec3 instanceColorEnd;          varying vec2 vUv;          varying float lineLength;          #ifdef USE_DASH              uniform float dashScale;             attribute float instanceDistanceStart;             attribute float instanceDistanceEnd;             varying float vLineDistance;          #endif          void trimSegment( const in vec4 start, inout vec4 end ) {              // trim end segment so it terminates between the camera plane and the near plane              // conservative estimate of the near plane             float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column             float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column             float nearEstimate = - 0.5 * b / a;              float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );              end.xyz = mix( start.xyz, end.xyz, alpha );          }          void main() {              #ifdef USE_COLOR                  vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;              #endif              #ifdef USE_DASH                  vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;              #endif              float aspect = resolution.x / resolution.y;              vUv = uv;              // camera space             vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );             vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );              // special case for perspective projection, and segments that terminate either in, or behind, the camera plane             // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space             // but we need to perform ndc-space calculations in the shader, so we must address this issue directly             // perhaps there is a more elegant solution -- WestLangley              bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column              if ( perspective ) {                  if ( start.z < 0.0 && end.z >= 0.0 ) {                      trimSegment( start, end );                  } else if ( end.z < 0.0 && start.z >= 0.0 ) {                      trimSegment( end, start );                  }              }              // clip space             vec4 clipStart = projectionMatrix * start;             vec4 clipEnd = projectionMatrix * end;              // ndc space             vec2 ndcStart = clipStart.xy / clipStart.w;             vec2 ndcEnd = clipEnd.xy / clipEnd.w;              // direction             vec2 dir = ndcEnd - ndcStart;              // account for clip-space aspect ratio             dir.x *= aspect;             dir = normalize( dir );              // perpendicular to dir             vec2 offset = vec2( dir.y, - dir.x );              // undo aspect ratio adjustment             dir.x /= aspect;             offset.x /= aspect;              // sign flip             if ( position.x < 0.0 ) offset *= - 1.0;              // endcaps             if ( position.y < 0.0 ) {                  offset += - dir;              } else if ( position.y > 1.0 ) {                  offset += dir;              }              // adjust for linewidth             offset *= linewidth;              // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...             offset /= resolution.y;              // select end             vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;              // back to clip space             offset *= clip.w;              clip.xy += offset;              gl_Position = clip;              vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation              //lineLength = distance(ndcStart, ndcEnd);             lineLength = distance(ndcStart, ndcEnd) * (1.57 + abs(atan(dir.x / dir.y))) / 2.0;             //lineLength = distance(clipStart.xyz, clipEnd.xyz);             //lineLength = distance(start.xyz, end.xyz);              #include <logdepthbuf_vertex>             #include <clipping_planes_vertex>             #include <fog_vertex>          }         `,      fragmentShader:         `         uniform vec3 diffuse;         uniform float opacity;         uniform sampler2D map;          varying float lineLength;          #ifdef USE_DASH              uniform float dashSize;             uniform float gapSize;          #endif          varying float vLineDistance;          #include <common>         #include <color_pars_fragment>         #include <fog_pars_fragment>         #include <logdepthbuf_pars_fragment>         #include <clipping_planes_pars_fragment>          varying vec2 vUv;          void main() {              #include <clipping_planes_fragment>              #ifdef USE_DASH                  if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps                  if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX              #endif              if ( abs( vUv.y ) > 1.0 ) {                  float a = vUv.x;                 float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;                 float len2 = a * a + b * b;                  if ( len2 > 1.0 ) discard;             }              vec4 diffuseColor = vec4( diffuse, opacity );              #include <logdepthbuf_fragment>             #include <color_fragment>              vec4 c;              if ( abs( vUv.y ) > 1.0 ) {                 c = vec4(diffuseColor.rgb, diffuseColor.a);              } else {                 vec2 rpt = vec2(0.5, 1.0);                                  rpt.y *= lineLength * 5.0;                 //rpt.y *= lineLength / 500.0;                  rpt.y = floor(rpt.y + 0.5);                 if(rpt.y < 1.0) { rpt.y = 1.0; }                 if(rpt.y > 5.0) { rpt.y = 5.0; }                 c = vec4(1.0, 1.0, 1.0, 1.0);                  c *= texture2D( map, vUv * rpt );             }                          gl_FragColor = c;              //#include <premultiplied_alpha_fragment>             //#include <tonemapping_fragment>             //#include <encodings_fragment>             //#include <fog_fragment>          }         ` };  var ArrowLineMaterial = function (parameters) {      ShaderMaterial.call(this, {          type: 'ArrowLineMaterial',          uniforms: Object.assign({}, UniformsUtils.clone(ShaderLib['line'].uniforms), {             map: { value: null },         }),          vertexShader: ShaderLib['line'].vertexShader,         fragmentShader: ShaderLib['line'].fragmentShader,          clipping: true // required for clipping support      });      this.dashed = false;      Object.defineProperties(this, {          map: {              enumerable: true,              get: function () {                  return this.uniforms.map.value;              },              set: function (value) {                  this.uniforms.map.value = value;              }          },          color: {              enumerable: true,              get: function () {                  return this.uniforms.diffuse.value;              },              set: function (value) {                  this.uniforms.diffuse.value = value;              }          },          linewidth: {              enumerable: true,              get: function () {                  return this.uniforms.linewidth.value;              },              set: function (value) {                  this.uniforms.linewidth.value = value;              }          },          dashScale: {              enumerable: true,              get: function () {                  return this.uniforms.dashScale.value;              },              set: function (value) {                  this.uniforms.dashScale.value = value;              }          },          dashSize: {              enumerable: true,              get: function () {                  return this.uniforms.dashSize.value;              },              set: function (value) {                  this.uniforms.dashSize.value = value;              }          },          gapSize: {              enumerable: true,              get: function () {                  return this.uniforms.gapSize.value;              },              set: function (value) {                  this.uniforms.gapSize.value = value;              }          },          resolution: {              enumerable: true,              get: function () {                  return this.uniforms.resolution.value;              },              set: function (value) {                  this.uniforms.resolution.value.copy(value);              }          }      });      this.setValues(parameters);  };  ArrowLineMaterial.prototype = Object.create(ShaderMaterial.prototype); ArrowLineMaterial.prototype.constructor = ArrowLineMaterial;  ArrowLineMaterial.prototype.isLineMaterial = true;   export { ArrowLineMaterial };

View Code

ArrowLineMaterial.js中主要修改部分:

在顶点着色器中定义变量:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

varying float lineLength;

View Code

在顶点着色器中计算一下线的长度:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

lineLength = distance(ndcStart, ndcEnd) * (1.57 + abs(atan(dir.x / dir.y))) / 2.0;

View Code

在片元着色器中定义变量:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

uniform sampler2D map; varying float lineLength;

View Code

在片元着色器中贴图:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

vec4 c;  if ( abs( vUv.y ) > 1.0 ) {     c = vec4(diffuseColor.rgb, diffuseColor.a);  } else {     vec2 rpt = vec2(0.5, 1.0);          rpt.y *= lineLength * 5.0;     //rpt.y *= lineLength / 500.0;      rpt.y = floor(rpt.y + 0.5);     if(rpt.y < 1.0) { rpt.y = 1.0; }     if(rpt.y > 5.0) { rpt.y = 5.0; }     c = vec4(1.0, 1.0, 1.0, 1.0);      c *= texture2D( map, vUv * rpt ); }  gl_FragColor = c;

View Code

在片元着色器中注释掉下面几行,使线的颜色和canvas中设置的颜色一致:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

//#include <premultiplied_alpha_fragment> //#include <tonemapping_fragment> //#include <encodings_fragment> //#include <fog_fragment>

View Code

CanvasDraw.js代码:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

/**  * canvas绘图  */  let CanvasDraw = function () {      /**      * 画文本和气泡      */     this.drawText = function (THREE, renderer, text, width) {         let canvas = document.createElement("canvas");         let ctx = canvas.getContext('2d');          canvas.width = width * 2;         canvas.height = width * 2;          this.drawBubble(ctx, width - 10, width - 65, width, 45, 6, "#00c864");          //设置文字         ctx.fillStyle = "#ffffff";         ctx.font = '32px 宋体';         ctx.fillText(text, width - 10 + 12, width - 65 + 34);          let canvasTexture = new THREE.CanvasTexture(canvas);         canvasTexture.magFilter = THREE.NearestFilter;         canvasTexture.minFilter = THREE.NearestFilter;          let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();         canvasTexture.anisotropy = maxAnisotropy;          return canvasTexture;     }      /**      * 画箭头      */     this.drawArrow = function (THREE, renderer, width, height) {         let canvas = document.createElement("canvas");         let ctx = canvas.getContext('2d');          canvas.width = width;         canvas.height = height;          ctx.save();          ctx.translate(0, 0);          //this.drawRoundRectPath(ctx, width, height, 0);          //ctx.fillStyle = "#ffff00";         //ctx.fill();          this.drawArrowBorder(ctx, 2, 0, 0, 4, 100, 50, 0, 96, 2, 100, 300, 50);         ctx.fillStyle = "#ffffff";         ctx.fill();          ctx.restore();          let canvasTexture = new THREE.CanvasTexture(canvas);         canvasTexture.magFilter = THREE.NearestFilter;         canvasTexture.minFilter = THREE.NearestFilter;          let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();         canvasTexture.anisotropy = maxAnisotropy;          return canvasTexture;     }      /**      * 画线内箭头      */     this.drawArrow3 = function (THREE, renderer, width, height, color) {         let canvas = document.createElement("canvas");         let ctx = canvas.getContext('2d');          canvas.width = width;         canvas.height = height;          ctx.save();          ctx.translate(0, 0);          this.drawRoundRectPath(ctx, width, height, 0);          ctx.fillStyle = color;         ctx.fill();          this.drawArrowBorder(ctx, 0, 350, 0, 400, 50, 450, 100, 400, 100, 350, 50, 400);         ctx.fillStyle = "#ffffff";         ctx.fill();          ctx.restore();          let canvasTexture = new THREE.CanvasTexture(canvas);         canvasTexture.magFilter = THREE.NearestFilter;         canvasTexture.minFilter = THREE.NearestFilter;         canvasTexture.wrapS = THREE.RepeatWrapping;         canvasTexture.wrapT = THREE.RepeatWrapping;          let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();         canvasTexture.anisotropy = maxAnisotropy;          return canvasTexture;     }      /**      * 画气泡      */     this.drawBubble = function (ctx, x, y, width, height, radius, fillColor) {         ctx.save();          ctx.translate(x, y);          this.drawRoundRectPath(ctx, width, height, radius);          ctx.fillStyle = fillColor || "#000";         ctx.fill();          this.drawTriangle(ctx, 20, height, 40, height, 10, 65);         ctx.fillStyle = fillColor || "#000";         ctx.fill();          ctx.restore();     }      /**      * 画三角形      */     this.drawTriangle = function (ctx, x1, y1, x2, y2, x3, y3) {         ctx.beginPath();          ctx.moveTo(x1, y1);         ctx.lineTo(x2, y2);         ctx.lineTo(x3, y3);          ctx.closePath();     }      /**      * 画箭头边框      */     this.drawArrowBorder = function (ctx, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5, x6, y6) {         ctx.beginPath();          ctx.moveTo(x1, y1);         ctx.lineTo(x2, y2);         ctx.lineTo(x3, y3);         ctx.lineTo(x4, y4);         ctx.lineTo(x5, y5);         ctx.lineTo(x6, y6);          ctx.closePath();     }      /**      * 画圆角矩形      */     this.drawRoundRectPath = function (ctx, width, height, radius) {         ctx.beginPath(0);          //从右下角顺时针绘制,弧度从0到1/2PI           ctx.arc(width - radius, height - radius, radius, 0, Math.PI / 2);          //矩形下边线           ctx.lineTo(radius, height);          //左下角圆弧,弧度从1/2PI到PI           ctx.arc(radius, height - radius, radius, Math.PI / 2, Math.PI);          //矩形左边线           ctx.lineTo(0, radius);          //左上角圆弧,弧度从PI到3/2PI           ctx.arc(radius, radius, radius, Math.PI, Math.PI * 3 / 2);          //上边线           ctx.lineTo(width - radius, 0);          //右上角圆弧           ctx.arc(width - radius, radius, radius, Math.PI * 3 / 2, Math.PI * 2);          //右边线           ctx.lineTo(width, height - radius);          ctx.closePath();     }      /**      * 画圆      */     this.drawCircle = function (THREE, renderer, width, height, radius, fillColor) {         let canvas = document.createElement("canvas");         let ctx = canvas.getContext('2d');          canvas.width = width;         canvas.height = height;          ctx.save();          ctx.beginPath(0);          ctx.arc(width / 2, height / 2, radius, 0, 2 * Math.PI);          ctx.closePath();          ctx.fillStyle = fillColor || "#000";         ctx.fill();          ctx.restore();          let texture = new THREE.CanvasTexture(canvas);         texture.needsUpdate = true;          texture.magFilter = THREE.NearestFilter;         texture.minFilter = THREE.NearestFilter;          let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();         texture.anisotropy = maxAnisotropy;          return texture;     }  }  CanvasDraw.prototype.constructor = CanvasDraw;  export { CanvasDraw }

View Code

DrawPath2.js代码:

用 three.js 绘制三维带箭头线 (线内箭头)用 three.js 绘制三维带箭头线 (线内箭头)

/**  * 绘制路线  */  import * as THREE from '../build/three.module.js';  import { Line2 } from '../js/lines/Line2.js'; import { LineGeometry } from '../js/lines/LineGeometry.js';  import { CanvasDraw } from '../js.my/CanvasDraw.js'; import { ArrowLineMaterial } from '../js.my/ArrowLineMaterial.js';  import { Utils } from '../js.my/Utils.js'; import { Msg } from '../js.my/Msg.js';  let DrawPath2 = function () {      let _self = this;      let _canvasDraw = new CanvasDraw();     let utils = new Utils();     let msg = new Msg();      this._isDrawing = false;     this._path = [];     this._lines = [];     this.color = '#00F300';      this._depthTest = true;     this._hide = false;      let _side = 0;      let viewerContainerId = '#threeCanvas';     let viewerContainer = $(viewerContainerId)[0];      let objects;     let camera;     let turn;     let scene;      this.config = function (objects_, camera_, scene_, turn_) {         objects = objects_;         camera = camera_;         turn = turn_;         scene = scene_;          this._oldDistance = 1;         this._oldCameraPos = { x: camera.position.x, y: camera.position.y, z: camera.position.z }     }      this.start = function () {         if (!this._isDrawing) {             this._isDrawing = true;             viewerContainer.addEventListener('click', ray);             viewerContainer.addEventListener('mousedown', mousedown);             viewerContainer.addEventListener('mouseup', mouseup);         }     }      this.stop = function () {         if (this._isDrawing) {             this._isDrawing = false;             viewerContainer.removeEventListener('click', ray);             viewerContainer.removeEventListener('mousedown', mousedown);             viewerContainer.removeEventListener('mouseup', mouseup);         }     }      function mousedown(params) {         this._mousedownPosition = { x: camera.position.x, y: camera.position.y, z: camera.position.z }     }      function mouseup(params) {         this._mouseupPosition = { x: camera.position.x, y: camera.position.y, z: camera.position.z }     }      function ray(e) {         turn.unFocusButton();          let raycaster = createRaycaster(e.clientX, e.clientY);         let objs = [];         objects.all.map(object => {             if (object.material.visible) {                 objs.push(object);             }         });         let intersects = raycaster.intersectObjects(objs);         if (intersects.length > 0) {             let point = intersects[0].point;              let distance = utils.distance(this._mousedownPosition.x, this._mousedownPosition.y, this._mousedownPosition.z, this._mouseupPosition.x, this._mouseupPosition.y, this._mouseupPosition.z);              if (distance < 5) {                 _self._path.push({ x: point.x, y: point.y + 50, z: point.z });                  if (_self._path.length > 1) {                     let point1 = _self._path[_self._path.length - 2];                     let point2 = _self._path[_self._path.length - 1];                      drawLine(point1, point2);                 }             }         }     }      function createRaycaster(clientX, clientY) {         let x = (clientX / $(viewerContainerId).width()) * 2 - 1;         let y = -(clientY / $(viewerContainerId).height()) * 2 + 1;          let standardVector = new THREE.Vector3(x, y, 0.5);          let worldVector = standardVector.unproject(camera);          let ray = worldVector.sub(camera.position).normalize();          let raycaster = new THREE.Raycaster(camera.position, ray);          return raycaster;     }      this.refresh = function () {      }      function drawLine(point1, point2) {         let n = Math.round(utils.distance(point1.x, point1.y, point1.z, point2.x, point2.y, point2.z) / 500);         if (n < 1) n = 1;         for (let i = 0; i < n; i++) {             let p1 = {};             p1.x = point1.x + (point2.x - point1.x) / n * i;             p1.y = point1.y + (point2.y - point1.y) / n * i;             p1.z = point1.z + (point2.z - point1.z) / n * i;              let p2 = {};             p2.x = point1.x + (point2.x - point1.x) / n * (i + 1);             p2.y = point1.y + (point2.y - point1.y) / n * (i + 1);             p2.z = point1.z + (point2.z - point1.z) / n * (i + 1);              drawLine2(p1, p2);         }     }      function drawLine2(point1, point2) {         const positions = [];          positions.push(point1.x / 50, point1.y / 50, point1.z / 50);         positions.push(point2.x / 50, point2.y / 50, point2.z / 50);          let geometry = new LineGeometry();         geometry.setPositions(positions);          geometry.setColors([             parseInt(_self.color.substr(1, 2), 16) / 256,             parseInt(_self.color.substr(3, 2), 16) / 256,             parseInt(_self.color.substr(5, 2), 16) / 256,             parseInt(_self.color.substr(1, 2), 16) / 256,             parseInt(_self.color.substr(3, 2), 16) / 256,             parseInt(_self.color.substr(5, 2), 16) / 256         ]);          let canvasTexture = _canvasDraw.drawArrow3(THREE, renderer, 100, 800, _self.color); //箭头          let matLine = new ArrowLineMaterial({             map: canvasTexture,             color: new THREE.Color(0xffffff),             linewidth: 0.005, // in world units with size attenuation, pixels otherwise             dashed: false,             depthTest: _self._depthTest,             side: _side,             vertexColors: THREE.VertexColors,             resolution: new THREE.Vector2(1, $(viewerContainerId).height() / $(viewerContainerId).width())         });          let line = new Line2(geometry, matLine);         line.computeLineDistances();         line.scale.set(50, 50, 50);          scene.add(line);         _self._lines.push(line);     }      this.setDepthTest = function (bl) {         if (bl) {             _self._depthTest = true;             this._lines.map(line => {                 line.material.depthTest = true;                 line.material.side = 0;             });         } else {             _self._depthTest = false;             this._lines.map(line => {                 line.material.depthTest = false;                 line.material.side = THREE.DoubleSide;             });         }     }      this.getPath = function () {         return this._path;     }      this.hide = function () {         this._lines.map(line => scene.remove(line));         this._hide = true;     }      this.show = function () {         this._lines.map(line => scene.add(line));         this._hide = false;     }      this.isShow = function () {         return !this._hide;     }      this.create = function (path, color) {         _self.color = color;         _self._path = path;          if (_self._path.length > 1) {             for (let i = 0; i < _self._path.length - 1; i++) {                 let point1 = _self._path[i];                 let point2 = _self._path[i + 1];                  drawLine(point1, point2);             }         }     }      this.getDepthTest = function () {         return _self._depthTest;     }      this.undo = function () {         scene.remove(this._lines[this._lines.length - 1]);         _self._path.splice(this._path.length - 1, 1);         _self._lines.splice(this._lines.length - 1, 1);     }  }  DrawPath2.prototype.constructor = DrawPath2;  export { DrawPath2 }

View Code

效果图:

用 three.js 绘制三维带箭头线 (线内箭头)

 

缺陷:

2.5D视角观察,看着还行,但是把相机拉近观察,箭头就会变形。凑合着用。

箭头贴图变形或者箭头显示不全,原因我猜可能是因为在场景中,线的远离相机的一端,在标准设备坐标系中比较细,线的靠近相机的一端,在标准设备坐标系中比较粗,但为了使线的粗细一样,靠近相机的一端被裁剪了,所以箭头可能会显示不全。

不管是MeshLine还是three.js的Line2,这个带宽度的线,和三维场景中的三维模型是有区别的,无论场景拉近还是拉远,线的宽度不变,而三维模型场景拉远变小,拉近变大。

Drawing lines is hard, drawing arrow lines is even harder!

用 three.js 绘制三维带箭头线 (线内箭头)

参考文章:

https://www.cnblogs.com/dojo-lzz/p/9219290.html

https://blog.csdn.net/Amesteur/article/details/95964526

 

 

赞(0) 打赏
未经允许不得转载:张拓的天空 » 用 three.js 绘制三维带箭头线 (线内箭头)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

专业的IT技术经验分享 更专业 更方便

联系我们本站主机

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏