因为该死的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. 基于SqlSugar的开发框架循序渐进介绍(20)-- 在基于UniApp+Vue的移动端实现多条件查询的处理

    在做一些常规应用的时候,我们往往需要确定条件的内容,以便在后台进行区分的进行精确查询,在移动端,由于受限于屏幕界面的情况,一般会对多个指定的条件进行模糊的搜索,而这个搜索的处理,也是和前者强类型的条件 ...

  2. 第2-2-4章 常见组件与中台化-常用组件服务介绍-分布式ID-附Snowflake雪花算法的代码实现

    目录 2.3 分布式ID 2.3.1 功能概述 2.3.2 应用场景 2.3.3 使用说明 2.3.4 项目截图 2.3.5 Snowflake雪花算法的代码实现 2.3 分布式ID 2.3.1 功能 ...

  3. mysql忽略大小写配置

    #更改配置文件:vim /etc/my.cnf#添加此行在[mysqld]下lower_case_table_names=1​#重启服务systemctl restart mysqld  

  4. 【红队技巧】Windows存储的密码获取

    [红队技巧]Windows存储的密码获取 免责声明: 使用前提 支持版本 利用方式 参考: 免责声明: 本文章仅供学习和研究使用,严禁使用该文章内容对互联网其他应用进行非法操作,若将其用于非法目的,所 ...

  5. 2022春每日一题:Day 15

    题目:Balanced lineup 题目说的很清楚了,没有修改,直接RMQ,模板题. 代码: #include <cstdio> #include <cstdlib> #in ...

  6. 链接脚本(Linker Scripts)语法和规则解析(自官方手册)

    为了便于与英文原文对照学习与理解(部分翻译可能不准确),本文中的每个子章节标题和引用使用的都是官方手册英文原称.命令及命令行选项统一使用斜体书写.高频小节会用蓝色字体标出. 3 Linker Scri ...

  7. float16与float32转换

    // based on https://gist.github.com/martin-kallman/5049614 // float32 // Martin Kallman // // Fast h ...

  8. c++ 三种继承

    继承优先级:private>protect>public ​ 变量或函数函数本身的类型和继承方式,比较,取小的就是继承的访问性 ​ eg: protected x,通过private继承, ...

  9. 安装mySql 出现 one more product requirements have not been satisified

    安装mySql 出现 one more product requirements have not been satisified 原因是缺少一些依赖环境. 在弹出的对话框中点击 否. 然后点击执行, ...

  10. 【每日一题】【遍历orSet】2022年2月1日-NC66 两个链表的第一个公共结点

    描述输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空.(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的) 输入描述:输入分为是3段,第 ...