因为该死的Unity不支持WebGL的麦克风,所以只能向网页借力,用网页原生的navigator.getUserMedia录音,然后传音频流给Unity进行转AudioClip播放。

还有一点非常重要:能有同事借力就直接问,厚着脸皮上,我自己闷头两天带加班,不如同事谭老哥加起来提供帮助的俩小时,很感谢他,虽然是他们该做的,但我一直没提出,而且我方向错了

版本:

Unity:2021.3.6f1

Github库:UnityWebGLMicrophone

相关代码

Unity端的.cs  .jslibWebGL端的.js.

.jslib

WebGLRecorder.jslib

这个需要放在UnityPlugins文件夹下

mergeInto(LibraryManager.library, {
StartRecord: function(){
StartRecord();
},
StopRecord: function(){
StopRecord();
},
});

.cs

WAV.cs

public class WAV
{
static float bytesToFloat(byte firstByte, byte secondByte)
{
short s = (short)((secondByte << 8) | firstByte);
return s / 32768.0F;
} static int bytesToInt(byte[] bytes, int offset = 0)
{
int value = 0;
for (int i = 0; i < 4; i++)
{
value |= ((int)bytes[offset + i]) << (i * 8);
}
return value;
} public float[] LeftChannel { get; internal set; }
public float[] RightChannel { get; internal set; }
public int ChannelCount { get; internal set; }
public int SampleCount { get; internal set; }
public int Frequency { get; internal set; } public WAV(byte[] wav)
{
ChannelCount = wav[22]; Frequency = bytesToInt(wav, 24); int pos = 12; while (!(wav[pos] == 100 && wav[pos + 1] == 97 && wav[pos + 2] == 116 && wav[pos + 3] == 97))
{
pos += 4;
int chunkSize = wav[pos] + wav[pos + 1] * 256 + wav[pos + 2] * 65536 + wav[pos + 3] * 16777216;
pos += 4 + chunkSize;
}
pos += 8; SampleCount = (wav.Length - pos) / 2;
if (ChannelCount == 2) SampleCount /= 2; LeftChannel = new float[SampleCount];
if (ChannelCount == 2) RightChannel = new float[SampleCount];
else RightChannel = null; int i = 0; int maxInput = wav.Length - (RightChannel == null ? 1 : 3);
while ((i < SampleCount) && (pos < maxInput))
{
LeftChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
pos += 2;
if (ChannelCount == 2)
{
RightChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
pos += 2;
}
i++;
} } public override string ToString()
{
return string.Format("[WAV: LeftChannel={0}, RightChannel={1}, ChannelCount={2}, SampleCount={3}, Frequency={4}]", LeftChannel, RightChannel, ChannelCount, SampleCount, Frequency);
} }

SignalManager.cs

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.UI; public class SignalManager : MonoBehaviour
{
public Button StartRecorder;
public Button EndRecorder;
AudioSource m_audioSource;
void Start()
{
m_audioSource = gameObject.AddComponent<AudioSource>();
StartRecorder.onClick.AddListener(StartRecorderFunc);
EndRecorder.onClick.AddListener(EndRecorderFunc);
} #region UnityToJs
[DllImport("__Internal")]
private static extern void StartRecord();
[DllImport("__Internal")]
private static extern void StopRecord();
void StartRecorderFunc()
{
StartRecord();
}
void EndRecorderFunc()
{
StopRecord();
}
#endregion #region JsToUnity
#region Data
/// <summary>
///需获取数据的数目
/// </summary>
private int m_valuePartCount = 0;
/// <summary>
/// 获取的数据数目
/// </summary>
private int m_getDataLength = 0;
/// <summary>
/// 获取的数据长度
/// </summary>
private int m_audioLength = 0;
/// <summary>
/// 获取的数据
/// </summary>
private string[] m_audioData = null; /// <summary>
/// 当前音频
/// </summary>
public static AudioClip m_audioClip = null; /// <summary>
/// 音频片段存放列表
/// </summary>
private List<byte[]> m_audioClipDataList; /// <summary>
/// 片段结束标记
/// </summary>
private string m_currentRecorderSign;
/// <summary>
/// 音频频率
/// </summary>
private int m_audioFrequency; /// <summary>
/// 单次最大录制时间
/// </summary>
private const int maxRecordTime = 30;
#endregion public void GetAudioData(string _audioDataString)
{
if (_audioDataString.Contains("Head"))
{
string[] _headValue = _audioDataString.Split('|');
m_valuePartCount = int.Parse(_headValue[1]);
m_audioLength = int.Parse(_headValue[2]);
m_currentRecorderSign = _headValue[3];
m_audioData = new string[m_valuePartCount];
m_getDataLength = 0;
Debug.Log("接收数据头:" + m_valuePartCount + " " + m_audioLength);
}
else if (_audioDataString.Contains("Part"))
{
string[] _headValue = _audioDataString.Split('|');
int _dataIndex = int.Parse(_headValue[1]);
m_audioData[_dataIndex] = _headValue[2];
m_getDataLength++;
if (m_getDataLength == m_valuePartCount)
{
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < m_audioData.Length; i++)
{
stringBuilder.Append(m_audioData[i]);
}
string _audioDataValue = stringBuilder.ToString();
Debug.Log("接收长度:" + _audioDataValue.Length + " 需接收长度:" + m_audioLength);
int _index = _audioDataValue.LastIndexOf(',');
string _value = _audioDataValue.Substring(_index + 1, _audioDataValue.Length - _index - 1);
byte[] data = Convert.FromBase64String(_value);
Debug.Log("已接收长度 :" + data.Length); if (m_currentRecorderSign == "end")
{
int _audioLength = data.Length;
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
_audioLength += m_audioClipDataList[i].Length;
}
byte[] _audioData = new byte[_audioLength];
Debug.Log("总长度 :" + _audioLength);
int _audioIndex = 0;
data.CopyTo(_audioData, _audioIndex);
_audioIndex += data.Length;
Debug.Log("已赋值0:" + _audioIndex);
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
m_audioClipDataList[i].CopyTo(_audioData, _audioIndex);
_audioIndex += m_audioClipDataList[i].Length;
Debug.Log("已赋值 :" + _audioIndex);
} WAV wav = new WAV(_audioData);
AudioClip _audioClip = AudioClip.Create("TestWAV", wav.SampleCount, 1, wav.Frequency, false);
_audioClip.SetData(wav.LeftChannel, 0); m_audioClip = _audioClip;
Debug.Log("音频设置成功,已设置到unity。" + m_audioClip.length + " " + m_audioClip.name); m_audioSource.clip = m_audioClip;
m_audioSource.Play(); m_audioClipDataList.Clear();
}
else
m_audioClipDataList.Add(data); m_audioData = null;
}
}
}
#endregion
}

.js

AddToIndex.js

这个脚本中的内容直接增加到index.html文件中

    <script src="./recorder.wav.min.js"></script>

      // 全局录音实例
let RecorderIns = null;
//全局Unity实例 (全局找 unityInstance , 然后等于它就行)
let UnityIns = null; // 初始化 , 记得调用
function initRecord(opt = {}) {
let defaultOpt = {
serviceCode: "asr_aword",
audioFormat: "wav",
sampleRate: 16000,
sampleBit: 16,
audioChannels: 1,
bitRate: 96000,
audioData: null,
punctuation: "true",
model: null,
intermediateResult: null,
maxStartSilence: null,
maxEndSilence: null,
}; let options = Object.assign({}, defaultOpt, opt); let sampleRate = options.sampleRate || 8000;
let bitRate = parseInt(options.bitRate / 1000) || 16;
if (RecorderIns) {
RecorderIns.close();
} RecorderIns = Recorder({
type: "wav",
sampleRate: sampleRate,
bitRate: bitRate,
onProcess(buffers, powerLevel, bufferDuration, bufferSampleRate) {
// 60秒时长限制
const LEN = 59 * 1000;
if (bufferDuration > LEN) {
RecorderIns.recStop();
}
},
});
RecorderIns.open(
() => {
// 打开麦克风授权获得相关资源
console.log("打开麦克风成功");
},
(msg, isUserNotAllow) => {
// 用户拒绝未授权或不支持
console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg);
}
);
} // 开始
function StartRecord() {
RecorderIns.start();
}
// 结束
function StopRecord() {
RecorderIns.stop(
(blob, duration) => {
console.log(
blob,
window.URL.createObjectURL(blob),
"时长:" + duration + "ms"
);
sendWavData(blob)
},
(msg) => {
console.log("录音失败:" + msg);
}
);
} // 切片像unity发送音频数据
function sendWavData(blob) {
var reader = new FileReader();
reader.onload = function (e) {
var _value = reader.result;
var _partLength = 8192;
var _length = parseInt(_value.length / _partLength);
if (_length * _partLength < _value.length) _length += 1;
var _head = "Head|" + _length.toString() + "|" + _value.length.toString() + "|end" ;
// 发送数据头
UnityIns.SendMessage("SignalManager", "GetAudioData", _head);
for (var i = 0; i < _length; i++) {
var _sendValue = "";
if (i < _length - 1) {
_sendValue = _value.substr(i * _partLength, _partLength);
} else {
_sendValue = _value.substr(
i * _partLength,
_value.length - i * _partLength
);
}
_sendValue = "Part|" + i.toString() + "|" + _sendValue;
// 发送分片数据
UnityIns.SendMessage("SignalManager", "GetAudioData", _sendValue);
}
_value = null;
};
reader.readAsDataURL(blob);
}

recorder.wav.min.js

这个直接创建脚本放到index.html同级目录下

/*
录音
https://github.com/xiangyuecn/Recorder
src: recorder-core.js,engine/wav.js
*/
!function(h){"use strict";var d=function(){},A=function(e){return new t(e)};A.IsOpen=function(){var e=A.Stream;if(e){var t=e.getTracks&&e.getTracks()||e.audioTracks||[],n=t[0];if(n){var r=n.readyState;return"live"==r||r==n.LIVE}}return!1},A.BufferSize=4096,A.Destroy=function(){for(var e in F("Recorder Destroy"),g(),n)n[e]()};var n={};A.BindDestroy=function(e,t){n[e]=t},A.Support=function(){var e=h.AudioContext;if(e||(e=h.webkitAudioContext),!e)return!1;var t=navigator.mediaDevices||{};return t.getUserMedia||(t=navigator).getUserMedia||(t.getUserMedia=t.webkitGetUserMedia||t.mozGetUserMedia||t.msGetUserMedia),!!t.getUserMedia&&(A.Scope=t,A.Ctx&&"closed"!=A.Ctx.state||(A.Ctx=new e,A.BindDestroy("Ctx",function(){var e=A.Ctx;e&&e.close&&(e.close(),A.Ctx=0)})),!0)};var S=function(e){var t=(e=e||A).BufferSize||A.BufferSize,n=A.Ctx,r=e.Stream,a=r._m=n.createMediaStreamSource(r),o=r._p=(n.createScriptProcessor||n.createJavaScriptNode).call(n,t,1,1);a.connect(o),o.connect(n.destination);var f=r._call;o.onaudioprocess=function(e){for(var t in f){for(var n=e.inputBuffer.getChannelData(0),r=n.length,a=new Int16Array(r),o=0,s=0;s<r;s++){var i=Math.max(-1,Math.min(1,n[s]));i=i<0?32768*i:32767*i,a[s]=i,o+=Math.abs(i)}for(var c in f)f[c](a,o);return}}},g=function(e){var t=(e=e||A)==A,n=e.Stream;if(n&&(n._m&&(n._m.disconnect(),n._p.disconnect(),n._p.onaudioprocess=n._p=n._m=null),t)){for(var r=n.getTracks&&n.getTracks()||n.audioTracks||[],a=0;a<r.length;a++){var o=r[a];o.stop&&o.stop()}n.stop&&n.stop()}e.Stream=0};A.SampleData=function(e,t,n,r,a){r||(r={});var o=r.index||0,s=r.offset||0,i=r.frameNext||[];a||(a={});var c=a.frameSize||1;a.frameType&&(c="mp3"==a.frameType?1152:1);for(var f=0,u=o;u<e.length;u++)f+=e[u].length;f=Math.max(0,f-Math.floor(s));var l=t/n;1<l?f=Math.floor(f/l):(l=1,n=t),f+=i.length;for(var v=new Int16Array(f),p=0,u=0;u<i.length;u++)v[p]=i[u],p++;for(var m=e.length;o<m;o++){for(var h=e[o],u=s,d=h.length;u<d;){var S=Math.floor(u),g=Math.ceil(u),_=u-S,y=h[S],I=g<d?h[g]:(e[o+1]||[y])[0]||0;v[p]=y+(I-y)*_,p++,u+=l}s=u-d}i=null;var M=v.length%c;if(0<M){var x=2*(v.length-M);i=new Int16Array(v.buffer.slice(x)),v=new Int16Array(v.buffer.slice(0,x))}return{index:o,offset:s,frameNext:i,sampleRate:n,data:v}},A.PowerLevel=function(e,t){var n=e/t||0;return n<1251?Math.round(n/1250*10):Math.round(Math.min(100,Math.max(0,100*(1+Math.log(n/1e4)/Math.log(10)))))};var F=function(e,t){var n=new Date,r=("0"+n.getMinutes()).substr(-2)+":"+("0"+n.getSeconds()).substr(-2)+"."+("00"+n.getMilliseconds()).substr(-3),a=["["+r+" Recorder]"+e],o=arguments,s=2,i=console.log;for("number"==typeof t?i=1==t?console.error:3==t?console.warn:i:s=1;s<o.length;s++)a.push(o[s]);i.apply(console,a)};A.CLog=F;var r=0;function t(e){this.id=++r,A.Traffic&&A.Traffic();var t={type:"mp3",bitRate:16,sampleRate:16e3,onProcess:d};for(var n in e)t[n]=e[n];this.set=t,this._S=9,this.Sync={O:9,C:9}}A.Sync={O:9,C:9},A.prototype=t.prototype={_streamStore:function(){return this.set.sourceStream?this:A},open:function(e,n){var t=this,r=t._streamStore();e=e||d;var a=function(e,t){F("录音open失败:"+e+",isUserNotAllow:"+(t=!!t),1),n&&n(e,t)},o=function(){F("open成功"),e(),t._SO=0},s=r.Sync,i=++s.O,c=s.C;t._O=t._O_=i,t._SO=t._S;var f=function(){if(c!=s.C||!t._O){var e="open被取消";return i==s.O?t.close():e="open被中断",a(e),!0}},u=t.envCheck({envName:"H5",canProcess:!0});if(u)a("不能录音:"+u);else if(t.set.sourceStream){if(!A.Support())return void a("不支持此浏览器从流中获取录音");g(r),t.Stream=t.set.sourceStream,t.Stream._call={};try{S(r)}catch(e){return void a("从流中打开录音失败:"+e.message)}o()}else{var l=function(e,t){try{h.top.a}catch(e){return void a('无权录音(跨域,请尝试给iframe添加麦克风访问策略,如allow="camera;microphone")')}/Permission|Allow/i.test(e)?a("用户拒绝了录音权限",!0):!1===h.isSecureContext?a("无权录音(需https)"):/Found/i.test(e)?a(t+",无可用麦克风"):a(t)};if(A.IsOpen())o();else if(A.Support()){var v=function(e){(A.Stream=e)._call={},f()||setTimeout(function(){f()||(A.IsOpen()?(S(),o()):a("录音功能无效:无音频流"))},100)},p=function(e){var t=e.name||e.message||e.code+":"+e;F("请求录音权限错误",1,e),l(t,"无法录音:"+t)},m=A.Scope.getUserMedia({audio:t.set.audioTrackSet||!0},v,p);m&&m.then&&m.then(v)[e&&"catch"](p)}else l("","此浏览器不支持录音")}},close:function(e){e=e||d;var t=this._streamStore();this._stop();var n=t.Sync;if(this._O=0,this._O_!=n.O)return F("close被忽略",3),void e();n.C++,g(t),F("close"),e()},mock:function(e,t){var n=this;return n._stop(),n.isMock=1,n.mockEnvInfo=null,n.buffers=[e],n.recSize=e.length,n.srcSampleRate=t,n},envCheck:function(e){var t,n=this.set;return t||(this[n.type+"_envCheck"]?t=this[n.type+"_envCheck"](e,n):n.takeoffEncodeChunk&&(t=n.type+"类型不支持设置takeoffEncodeChunk")),t||""},envStart:function(e,t){var n=this,r=n.set;if(n.isMock=e?1:0,n.mockEnvInfo=e,n.buffers=[],n.recSize=0,n.envInLast=0,n.envInFirst=0,n.envInFix=0,n.envInFixTs=[],r.sampleRate=Math.min(t,r.sampleRate),n.srcSampleRate=t,n.engineCtx=0,n[r.type+"_start"]){var a=n.engineCtx=n[r.type+"_start"](r);a&&(a.pcmDatas=[],a.pcmSize=0)}},envResume:function(){this.envInFixTs=[]},envIn:function(e,t){var a=this,o=a.set,s=a.engineCtx,n=a.srcSampleRate,r=e.length,i=A.PowerLevel(t,r),c=a.buffers,f=c.length;c.push(e);var u=c,l=f,v=Date.now(),p=Math.round(r/n*1e3);a.envInLast=v,1==a.buffers.length&&(a.envInFirst=v-p);var m=a.envInFixTs;m.splice(0,0,{t:v,d:p});for(var h=v,d=0,S=0;S<m.length;S++){var g=m[S];if(3e3<v-g.t){m.length=S;break}h=g.t,d+=g.d}var _=m[1],y=v-h;if(y/3<y-d&&(_&&1e3<y||6<=m.length)){var I=v-_.t-p;if(p/5<I){var M=!o.disableEnvInFix;if(F("["+v+"]"+(M?"":"未")+"补偿"+I+"ms",3),a.envInFix+=I,M){var x=new Int16Array(I*n/1e3);r+=x.length,c.push(x)}}}var k=a.recSize,C=r,w=k+C;if(a.recSize=w,s){var R=A.SampleData(c,n,o.sampleRate,s.chunkInfo);s.chunkInfo=R,w=(k=s.pcmSize)+(C=R.data.length),s.pcmSize=w,c=s.pcmDatas,f=c.length,c.push(R.data),n=R.sampleRate}var b=Math.round(w/n*1e3),T=c.length,z=u.length,D=function(){for(var e=O?0:-C,t=null==c[0],n=f;n<T;n++){var r=c[n];null==r?t=1:(e+=r.length,s&&r.length&&a[o.type+"_encode"](s,r))}if(t&&s)for(n=l,u[0]&&(n=0);n<z;n++)u[n]=null;t&&(e=O?C:0,c[0]=null),s?s.pcmSize+=e:a.recSize+=e},O=o.onProcess(c,i,b,n,f,D);if(!0===O){var U=0;for(S=f;S<T;S++)null==c[S]?U=1:c[S]=new Int16Array(0);U?F("未进入异步前不能清除buffers",3):s?s.pcmSize-=C:a.recSize-=C}else D()},start:function(){var e=this,t=A.Ctx,n=1;if(e.set.sourceStream?e.Stream||(n=0):A.IsOpen()||(n=0),n)if(F("开始录音"),e._stop(),e.state=0,e.envStart(null,t.sampleRate),e._SO&&e._SO+1!=e._S)F("start被中断",3);else{e._SO=0;var r=function(){e.state=1,e.resume()};"suspended"==t.state?t.resume().then(function(){F("ctx resume"),r()}):r()}else F("未open",1)},pause:function(){this.state&&(this.state=2,F("pause"),delete this._streamStore().Stream._call[this.id])},resume:function(){var n=this;n.state&&(n.state=1,F("resume"),n.envResume(),n._streamStore().Stream._call[n.id]=function(e,t){1==n.state&&n.envIn(e,t)})},_stop:function(e){var t=this,n=t.set;t.isMock||t._S++,t.state&&(t.pause(),t.state=0),!e&&t[n.type+"_stop"]&&(t[n.type+"_stop"](t.engineCtx),t.engineCtx=0)},stop:function(n,t,e){var r,a=this,o=a.set;F("Stop "+(a.envInLast?a.envInLast-a.envInFirst+"ms 补"+a.envInFix+"ms":"-"));var s=function(){a._stop(),e&&a.close()},i=function(e){F("结束录音失败:"+e,1),t&&t(e),s()},c=function(e,t){if(F("结束录音 编码"+(Date.now()-r)+"ms 音频"+t+"ms/"+e.size+"b"),o.takeoffEncodeChunk)F("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据",3);else if(e.size<Math.max(100,t/2))return void i("生成的"+o.type+"无效");n&&n(e,t),s()};if(!a.isMock){if(!a.state)return void i("未开始录音");a._stop(!0)}var f=a.recSize;if(f)if(a.buffers[0])if(a[o.type]){if(a.isMock){var u=a.envCheck(a.mockEnvInfo||{envName:"mock",canProcess:!1});if(u)return void i("录音错误:"+u)}var l=a.engineCtx;if(a[o.type+"_complete"]&&l){var v=Math.round(l.pcmSize/o.sampleRate*1e3);return r=Date.now(),void a[o.type+"_complete"](l,function(e){c(e,v)},i)}r=Date.now();var p=A.SampleData(a.buffers,a.srcSampleRate,o.sampleRate);o.sampleRate=p.sampleRate;var m=p.data;v=Math.round(m.length/o.sampleRate*1e3),F("采样"+f+"->"+m.length+" 花:"+(Date.now()-r)+"ms"),setTimeout(function(){r=Date.now(),a[o.type](m,function(e){c(e,v)},function(e){i(e)})})}else i("未加载"+o.type+"编码器");else i("音频被释放");else i("未采集到录音")}},h.Recorder&&h.Recorder.Destroy(),(h.Recorder=A).LM="2021-08-03 20:01:03",A.TrafficImgUrl="",A.Traffic=function(){var e=A.TrafficImgUrl;if(e){var t=A.Traffic,n=location.href.replace(/#.*/,"");if(0==e.indexOf("//")&&(e=/^https:/i.test(n)?"https:"+e:"http:"+e),!t[n]){t[n]=1;var r=new Image;r.src=e,F("Traffic Analysis Image: Recorder.TrafficImgUrl="+A.TrafficImgUrl)}}}}(window),"function"==typeof define&&define.amd&&define(function(){return Recorder}),"object"==typeof module&&module.exports&&(module.exports=Recorder),function(){"use strict";Recorder.prototype.enc_wav={stable:!0,testmsg:"支持位数8位、16位(填在比特率里面),采样率取值无限制"},Recorder.prototype.wav=function(e,t,n){var r=this.set,a=e.length,o=r.sampleRate,s=8==r.bitRate?8:16,i=a*(s/8),c=new ArrayBuffer(44+i),f=new DataView(c),u=0,l=function(e){for(var t=0;t<e.length;t++,u++)f.setUint8(u,e.charCodeAt(t))},v=function(e){f.setUint16(u,e,!0),u+=2},p=function(e){f.setUint32(u,e,!0),u+=4};if(l("RIFF"),p(36+i),l("WAVE"),l("fmt "),p(16),v(1),v(1),p(o),p(o*(s/8)),v(s/8),v(s),l("data"),p(i),8==s)for(var m=0;m<a;m++,u++){var h=128+(e[m]>>8);f.setInt8(u,h,!0)}else for(m=0;m<a;m++,u+=2)f.setInt16(u,e[m],!0);t(new Blob([f.buffer],{type:"audio/wav"}))}}();

参考链接:

Recorder用于html5录音  (recorder-core.js,engine/wav.js

Unity WebGL基于js通信实现网页录音  (sendWAVData.js,GetAudioData.cs,WAV.cs


希望大家:点赞,留言,关注咯~    

唠家常

  • Xiaohei.Wang(Wenhao)的今日分享结束啦,小伙伴们你们get到了么,你们有没有更好的办法呢,可以评论区留言分享,也可以加我的QQ:841298494 (记得备注),大家一起进步。

今日无推荐

Unity-WebGL基于JS实现网页录音的更多相关文章

  1. 基于js的网页换肤(不需要刷新整个页面,只需替换css文件)

    1. [代码][JS]代码    <HTML><HEAD><link ID="skin" rel="stylesheet" typ ...

  2. 一款基于TweenMax.js的网页幻灯片

    之前介绍了好多网页幻灯片.今天给大家带来一款基于TweenMax.js的网页幻灯片.这款幻灯片以不规则的碎片百叶窗的形式切换.切换效果非常漂亮.一起看下效果图: 在线预览   源码下载 实现的代码. ...

  3. 关于 Unity WebGL 的探索(一)

    到今天为止,项目已经上线一个多月了,目前稳定运行,各种 bug 也是有的.至少得到了苹果的两次推荐和 TapTap 一次首页推荐,也算是结项后第一时间对我们项目的一个肯定. 出于各种各样的可描述和不可 ...

  4. 使用WebGL + Three.js制作动画场景

    使用WebGL + Three.js制作动画场景 3D图像,技术,打造产品,还有互联网:这些只是我爱好的一小部分. 现在,感谢WebGL的出现-一个新的JavaScriptAPI,它可以在不依赖任何插 ...

  5. Unity WebGL 窗口自适应

    unity 打包好WebGL后,用文本编辑器编辑打包生成的 index.html 文件 在生成的html里面修改代码     <script type="text/javascript ...

  6. 从零开始实现基于微信JS-SDK的录音与语音评价功能

    最近接受了一个新的需求,希望制作一个基于微信的英语语音评价页面.即点击录音按钮,用户录音说出预设的英文,根据用户的发音给出对应的评价.以下是简单的Demo: ![](reecode/qrcode.pn ...

  7. Breach - HTML5 时代,基于 JS 编写的浏览器

    Breach 是一款属于 HTML5 时代的开源浏览器项目,,完全用 Javascript 编写的.免费.模块化.易于扩展.这个浏览器中的一切都是模块,Web 应用程序在其自己的进程运行.通过选择合适 ...

  8. 图解WebGL&Three.js工作原理

    “哥,你又来啦?”“是啊,我随便逛逛.”“别介啊……给我20分钟,成不?”“5分钟吧,我很忙的.”“不行,20分钟,不然我真很难跟你讲清楚.”“好吧……”“行,那进来吧,咱好好聊聊” 一.我们讲什么? ...

  9. WebGL&Three.js工作原理

    一.我们讲什么? 我们讲两个东西:1.WebGL背后的工作原理是什么?2.以Three.js为例,讲述框架在背后扮演什么样的角色? 二.我们为什么要了解原理? 我们假定你对WebGL已经有一定了解,或 ...

随机推荐

  1. fastjson反序列化漏洞历史CVE学习整理

    fastjson 1.2.24反序列化漏洞复现 先写一个正常的使用 fastjson的web服务 我们使用 springboot创建 主要是pom.xml 里面要添加fastjson fastjson ...

  2. Python基础之模块:2、包的使用和软件开发目录规范及常用内置模块

    目录 一.包的使用 1.什么是包 2.包的具体使用 1.常规导入 2.直接导入包名 二.编程思想转变 1.面条阶段 2.函数阶段 3.模块阶段 三.软件目录开发规范 1.bin 2.conf 3.co ...

  3. Codeforces Round #781(C. Tree Infection)

    Codeforces Round #781 C. Tree Infection time limit per test 1 second memory limit per test 256 megab ...

  4. 【深入浅出 Yarn 架构与实现】2-1 Yarn 基础库概述

    了解 Yarn 基础库是后面阅读 Yarn 源码的基础,本节对 Yarn 基础库做总体的介绍.并对其中使用的第三方库 Protocol Buffers 和 Avro 是什么.怎么用做简要的介绍. 一. ...

  5. hwlog--logger.go

    // Copyright(C) 2021. Huawei Technologies Co.,Ltd. All rights reserved.// Package hwlog provides the ...

  6. Dive into TensorFlow系列(2)- 解析TF核心抽象op算子

    本文作者:李杰 TF计算图从逻辑层来讲,由op与tensor构成.op是项点代表计算单元,tensor是边代表op之间流动的数据内容,两者配合以数据流图的形式来表达计算图.那么op对应的物理层实现是什 ...

  7. 原来用 MySQL 也可以做全文检索

    我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态. 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面. 有朋友聊到他们的系统中要接入全 ...

  8. windows error LNK2019

    温馨提示,请使用ctrl+F进行快速查找 ws2_32.lib error LNK2001: 无法解析的外部符号 __imp_htons error LNK2001: 无法解析的外部符号 __imp_ ...

  9. C++动态链接MySQL库

    C++链接MySQL库 库安装目录 CMakeList cmake_minimum_required(VERSION 3.22) project(MySQLConnectionPool) includ ...

  10. On Java 8读书笔记

    第一章 什么是对象 1.1 抽象的历程 "对象":问题空间中的元素及其解决方案空间中的具体呈现. 理念即是通过添加各种新的对象,可以将程序改编为一种描述问题的语言. 对象是具有状态 ...