为了对GMM-HMM在语音识别上的应用有个宏观认识,花了些时间读了下HTK(用htk完成简单的孤立词识别)的部分源码,对该算法总算有了点大概认识,达到了预期我想要的。不得不说,网络上关于语音识别的通俗易懂教程太少,都是各种公式满天飞,很少有说具体细节的,当然了,那需要有实战经验才行。下面总结以下几点,对其有个宏观印象即可(以孤立词识别为例)。

  一、每个单词的读音都对应一个HMM模型,大家都知道HMM模型中有个状态集S,那么每个状态用什么来表示呢,数字?向量?矩阵?其实这个状态集中的状态没有具体的数学要求,只是一个名称而已,你可以用’1’, ’2’, ‘3’…表示,也可以用’a’, ‘b’, ’c ’表示。另外每个HMM模型中到底该用多少个状态,是通过先验知识人为设定的。

  二、HMM的每一个状态都对应有一个观察值,这个观察值可以是一个实数,也可以是个向量,且每个状态对应的观察值的维度应该相同。假设现在有一个单词的音频文件,首先需要将其进行采样得到数字信息(A/D转换),然后分帧进行MFCC特征提取,假设每一帧音频对应的MFCC特征长度为39,则每个音频文件就转换成了N个MFCC向量(不同音频文件对应的N可能不同),这就成了一个序列,而在训练HMM模型的参数时(比如用Baum-Welch算法),每次输入到HMM中的数据要求就是一个观测值序列。这时,每个状态对应的观测值为39维的向量,因为向量中元素的取值是连续的,需要用多维密度函数来模拟,通常情况下用的是多维高斯函数。在GMM-HMM体系中,这个拟合函数是用K个多维高斯混合得到的。假设知道了每个状态对应的K个多维高斯的所有参数,则该GMM生成该状态上某一个观察向量(一帧音频的MFCC系数)的概率就可以求出来了。

  三、对每个单词建立一个HMM模型,需要用到该单词的训练样本,这些训练样本是提前标注好的,即每个样本对应一段音频,该音频只包含这个单词的读音。当有了该单词的多个训练样本后,就用这些样本结合Baum-Welch算法和EM算法来训练出GMM-HMM的所有参数,这些参数包括初始状态的概率向量,状态之间的转移矩阵,每个状态对应的观察矩阵(这里对应的是GMM,即每个状态对应的K个高斯的权值,每个高斯的均值向量和方差矩阵)。

  四、在识别阶段,输入一段音频,如果该音频含有多个单词,则可以手动先将其分割开(考虑的是最简单的方法),然后提取每个单词的音频MFCC特征序列,将该序列输入到每个HMM模型(已提前训练好的)中,采用前向算法求出每个HMM模型生成该序列的概率,最后取最大概率对应的那个模型,而那个模型所表示的单词就是我们识别的结果。

  五、在建立声学模型时,可以用Deep Learning的方法来代替GMM-HMM中的GMM,因为GMM模拟任意函数的功能取决于混合高斯函数的个数,所以具有一定的局限性,属于浅层模型。而Deep Network可以模拟任意的函数,因而表达能力更强。注意,这里用来代替GMM的Deep Nets模型要求是产生式模型,比如DBN,DBM等,因为在训练HMM-DL网络时,需要用到HMM的某个状态产生一个样本的概率。

  六、GMM-HMM在具体实现起来还是相当复杂的。

  七、一般涉及到时间序列时才会使用HMM,比如这里音频中的语音识别,视频中的行为识别等。如果我们用GMM-HMM对静态的图片分类,因为这里没涉及到时间信息,所以HMM的状态数可设为1,那么此时的GMM-HMM算法就退化成GMM算法了。

  MFCC:

  MFCC的matlab实现教程可参考:张智星老师的网页教程mfcc. 最基本的12维特征。

function mfcc=frame2mfcc(frame, fs, filterNum, mfccNum, plotOpt)
% frame2mfcc: Frame to MFCC conversion.
% Usage: mfcc=frame2mfcc(frame, fs, filterNum, mfccNum, plotOpt)
%
% For example:
% waveFile='what_movies_have_you_seen_recently.wav';
% [y, fs, nbits]=wavReadInt(waveFile);
% startIndex=;
% frameSize=;
% frame=y(startIndex:startIndex+frameSize-);
% frame2mfcc(frame, fs, , , ); % Roger Jang if nargin<, selfdemo; return; end
if nargin<, fs=; end
if nargin<, filterNum=; end
if nargin<, mfccNum=; end
if nargin<, plotOpt=; end frameSize=length(frame);
% ====== Preemphasis should be done at wave level
%a=0.95;
%frame2 = filter([, -a], , frame);
frame2=frame;
% ====== Hamming windowing
frame3=frame2.*hamming(frameSize);
% ====== FFT
[fftMag, fftPhase, fftFreq, fftPowerDb]=fftOneSide(frame3, fs);
% ====== Triangular band-pass filter bank
triFilterBankPrm=getTriFilterBankPrm(fs, filterNum); % Get parameters for triangular band-pass filter bank
% Triangular bandpass filter.
for i=:filterNum
tbfCoef(i)=dot(fftPowerDb, trimf(fftFreq, triFilterBankPrm(:,i)));%得到filterNum个滤波系数
end
% ====== DCT
mfcc=zeros(mfccNum, ); %DCT变换的前后个数也没有变
for i=:mfccNum
coef = cos((pi/filterNum)*i*((:filterNum)-0.5))'; %mfcc中的前mfccNum个系数
mfcc(i) = sum(coef.*tbfCoef');%直接按照DCT公式
end
% ====== Log energy
%logEnergy=*log10(sum(frame.*frame));
%mfcc=[logEnergy; mfcc]; if plotOpt
subplot(,,);
plot(frame, '.-');
set(gca, 'xlim', [-inf inf]);
title('Input frame');
subplot(,,);
plot(mfcc, '.-');
set(gca, 'xlim', [-inf inf]);
title('MFCC vector');
end % ====== trimf.m (from fuzzy toolbox)
function y = trimf(x, prm) %由频率的横坐标算出三角形内的纵坐标,~
a = prm(); b = prm(); c = prm();
y = zeros(size(x));
% Left and right shoulders (y = )
index = find(x <= a | c <= x);
y(index) = zeros(size(index)); %只考虑三角波内的量
% Left slope
if (a ~= b)
index = find(a < x & x < b);
y(index) = (x(index)-a)/(b-a);
end
% right slope
if (b ~= c)
index = find(b < x & x < c);
y(index) = (c-x(index))/(c-b);
end
% Center (y = )
index = find(x == b);
y(index) = ones(size(index)); % ====== Self demo
function selfdemo
waveFile='what_movies_have_you_seen_recently.wav';
[y, fs, nbits]=wavReadInt(waveFile);
startIndex=;
frameSize=;
frame=y(startIndex:startIndex+frameSize-);
feval(mfilename, frame, fs, , , );

  ZCR:

  过0检测,用于判断每一帧中过零点的数量情况,最简单的版本可参考:zeros cross rate.

waveFile='csNthu.wav';
frameSize=;
overlap=;
[y, fs, nbits]=wavread(waveFile);
frameMat=enframe(y, frameSize, overlap);
frameNum=size(frameMat, );
for i=:frameNum
frameMat(:,i)=frameMat(:,i)-mean(frameMat(:,i)); % mean justification
end
zcr=sum(frameMat(:end-, :).*frameMat(:end, :)<);
sampleTime=(:length(y))/fs;
frameTime=((:frameNum-)*(frameSize-overlap)+0.5*frameSize)/fs;
subplot(,,); plot(sampleTime, y); ylabel('Amplitude'); title(waveFile);
subplot(,,); plot(frameTime, zcr, '.-');
xlabel('Time (sec)'); ylabel('Count'); title('ZCR');

  EPD:

  端点检测,检测声音的起始点和终止点,可参考:EPD in Time Domain,在时域中的最简单检测方法。

waveFile='sunday.wav';
[wave, fs, nbits] = wavread(waveFile);
frameSize = ;
overlap = ; wave=wave-mean(wave); % zero-mean substraction
frameMat=buffer2(wave, frameSize, overlap); % frame blocking,每一列代表一帧
frameNum=size(frameMat, ); % no. of frames
volume=frame2volume(frameMat); % volume,求每一帧的能量,绝对值或者平方和,volume为行向量
volumeTh1=max(volume)*0.1; % volume threshold
volumeTh2=median(volume)*0.1; % volume threshold
volumeTh3=min(volume)*; % volume threshold
volumeTh4=volume()*; % volume threshold
index1 = find(volume>volumeTh1); %找出volume大于阈值的那些帧序号
index2 = find(volume>volumeTh2);
index3 = find(volume>volumeTh3);
index4 = find(volume>volumeTh4);
%frame2sampleIndex()为从帧序号找到样本点的序号(即每一个采样点的序号)
%endPointX长度为2,包含了起点和终点的样本点序号
endPoint1=frame2sampleIndex([index1(), index1(end)], frameSize, overlap);
endPoint2=frame2sampleIndex([index2(), index2(end)], frameSize, overlap);
endPoint3=frame2sampleIndex([index3(), index3(end)], frameSize, overlap);
endPoint4=frame2sampleIndex([index4(), index4(end)], frameSize, overlap); subplot(,,);
time=(:length(wave))/fs;
plot(time, wave);
ylabel('Amplitude'); title('Waveform');
axis([-inf inf - ]);
line(time(endPoint1( ))*[ ], [-, ], 'color', 'm');%标起点终点线
line(time(endPoint2( ))*[ ], [-, ], 'color', 'g');
line(time(endPoint3( ))*[ ], [-, ], 'color', 'k');
line(time(endPoint4( ))*[ ], [-, ], 'color', 'r');
line(time(endPoint1(end))*[ ], [-, ], 'color', 'm');
line(time(endPoint2(end))*[ ], [-, ], 'color', 'g');
line(time(endPoint3(end))*[ ], [-, ], 'color', 'k');
line(time(endPoint4(end))*[ ], [-, ], 'color', 'r');
legend('Waveform', 'Boundaries by threshold 1', 'Boundaries by threshold 2', 'Boundaries by threshold 3', 'Boundaries by threshold 4'); subplot(,,);
frameTime=frame2sampleIndex(:frameNum, frameSize, overlap);
plot(frameTime, volume, '.-');
ylabel('Sum of Abs.'); title('Volume');
axis tight;
line([min(frameTime), max(frameTime)], volumeTh1*[ ], 'color', 'm');
line([min(frameTime), max(frameTime)], volumeTh2*[ ], 'color', 'g');
line([min(frameTime), max(frameTime)], volumeTh3*[ ], 'color', 'k');
line([min(frameTime), max(frameTime)], volumeTh4*[ ], 'color', 'r');
legend('Volume', 'Threshold 1', 'Threshold 2', 'Threshold 3', 'Threshold 4');

   GMM: 

   GMM用在拟合数据分布上,本质上是先假设样本的概率分布为GMM,然后用多个样本去学习这些GMM的参数。GMM建模在语音中可用于某个单词的发音,某个人的音色等。其训练过程可参考:speaker recognition.

function [M, V, W, logProb] = gmmTrain(data, gaussianNum, dispOpt)
% gmmTrain: Parameter training for gaussian mixture model (GMM)
% Usage: function [M, V, W, logProb] = gmm(data, gaussianNum, dispOpt)
% data: dim x dataNum matrix where each column is a data point
% gaussianNum: No. of Gaussians or initial centers
% dispOpt: Option for displaying info during training
% M: dim x meanNum matrix where each column is a mean vector
% V: x gaussianNum vector where each element is a variance for a Gaussian
% W: x gaussianNum vector where each element is a weighting factor for a Gaussian % Roger Jang if nargin==, selfdemo; return; end
if nargin<, dispOpt=; end maxLoopCount = ; % Max. iteration
minImprove = 1e-; % Min. improvement
minVariance = 1e-; % Min. variance
logProb = zeros(maxLoopCount, ); % Array for objective function
[dim, dataNum] = size(data); % Set initial parameters
% Set initial M
%M = data(+floor(rand(gaussianNum,)*dataNum),:); % Randomly select several data points as the centers
if length(gaussianNum)==,
% Using vqKmeans to find initial centers
fprintf('Start KMEANS to find the initial mu...\n');
% M = vqKmeansMex(data, gaussianNum, );
M = vqKmeans(data, gaussianNum, ); %利用聚类的方法求均值,聚成gaussianNum类
% M = vqLBG(data, gaussianNum, );
fprintf('Start GMM training...\n');
if any(any(~isfinite(M))); keyboard; end
else
% gaussianNum is in fact the initial centers
M = gaussianNum;
gaussianNum = size(M, );
end
% Set initial V as the distance to the nearest center
if gaussianNum==
V=;
else
distance=pairwiseSqrDist(M);%pairwiseSqrDist是dll
%distance=pairwiseSqrDist2(M); distance(:(gaussianNum+):gaussianNum^)=inf; % Diagonal elements are inf
[V, index]=min(distance); % Initial variance for each Gaussian
end
% Set initial W
W = ones(, gaussianNum)/gaussianNum; % Weight for each Gaussian,初始化时是均分权值 if dispOpt & dim==, displayGmm(M, V, data); end
for i = :maxLoopCount %开始迭代训练参数,EM算法
% Expectation step:
% P(i,j) is the probability of data(:,j) to the i-th Gaussian
% Prob为每个样本在GMM下的概率
[prob, P]=gmmEval(data, M, V, W);
logProb(i)=sum(log(prob)); %所有样本的联合概率
if dispOpt
fprintf('i = %d, log prob. = %f\n',i-, logProb(i));
end
PW = diag(W)*P;
BETA=PW./(ones(gaussianNum,)*sum(PW)); % BETA(i,j) is beta_i(x_j)
sumBETA=sum(BETA,); % Maximization step: eqns (2.96) to (2.98) from Bishop p.:
M = (data*BETA')./(ones(dim,1)*sumBETA'); DISTSQ = pairwiseSqrDist(M, data); % Distance of M to data
%DISTSQ = pairwiseSqrDist2(M, data); % Distance of M to data V = max((sum(BETA.*DISTSQ, )./sumBETA)/dim, minVariance); % (2.97)
W = (/dataNum)*sumBETA; % (2.98) if dispOpt & dim==, displayGmm(M, V, data); end
if i>, if logProb(i)-logProb(i-)<minImprove, break; end; end
end
[prob, P]=gmmEval(data, M, V, W);
logProb(i)=sum(log(prob));
fprintf('Iteration count = %d, log prob. = %f\n',i, logProb(i));
logProb(i+:maxLoopCount) = []; % ====== Self Demo ======
function selfdemo
%[data, gaussianNum] = dcdata();
data = rand(,);
gaussianNum = ;
data=data';
plotOpt=;
[M, V, W, lp] = feval(mfilename, data, gaussianNum, plotOpt); pointNum = ;
x = linspace(min(data(,:)), max(data(,:)), pointNum);
y = linspace(min(data(,:)), max(data(,:)), pointNum);
[xx, yy] = meshgrid(x, y);
data = [xx(:) yy(:)]';
z = gmmEval(data, M, V, W);
zz = reshape(z, pointNum, pointNum);
figure; mesh(xx, yy, zz); axis tight; box on; rotate3d on
figure; contour(xx, yy, zz, ); axis image % ====== Other subfunctions ======
function displayGmm(M, V, data)
% Display function for EM algorithm
figureH=findobj(, 'tag', mfilename);
if isempty(figureH)
figureH=figure;
set(figureH, 'tag', mfilename);
colordef black
plot(data(,:), data(,:),'.r'); axis image
theta=linspace(-pi, pi, );
x=cos(theta); y=sin(theta);
sigma=sqrt(V);
for i=:length(sigma)
circleH(i)=line(x*sigma(i)+M(,i), y*sigma(i)+M(,i), 'color', 'y');
end
set(circleH, 'tag', 'circleH', 'erasemode', 'xor');
else
circleH=findobj(figureH, 'tag', 'circleH');
theta=linspace(-pi, pi, );
x=cos(theta); y=sin(theta);
sigma=sqrt(V);
for i=:length(sigma)
set(circleH(i), 'xdata', x*sigma(i)+M(,i), 'ydata', y*sigma(i)+M(,i));
end
drawnow
end

  Speaker identification:

   给N个人的语音资料,用GMM可以训练这N个人的声音模型,然后给定一段语音,判断该语音与这N个人中哪个最相似。方法是求出该语音在N个GMM模型下的概率,选出概率最大的那个。可参考:speaker recognition.

function [recogRate, confusionMatrix, speakerData]=speakerIdentify(speakerData, speakerGmm, useIntGmm)
% speakerIdentify: speaker identification using GMM parameters
% Usage: [recogRate, confusionMatrix, speakerData]=speakerIdentify(speakerData, speakerGmm, useIntGmm)
% speakerData: structure array generated by speakerDataRead.m
% speakerGmm: speakerGmm(i).gmmPrm is the GMM parameters for speaker i.
% useIntGmm: use fixed-point GMM % Roger Jang, , if nargin<, useIntGmm=; end % ====== Speaker identification using GMM parameters
speakerNum=length(speakerData);
for i=:speakerNum
% fprintf('%d/%d: Recognizing wave files by %s\n', i, speakerNum, speakerData(i).name);
for j=:length(speakerData(i).sentence)
% fprintf('\tSentece %d...\n', j);
frameNum=size(speakerData(i).sentence(j).fea, );
logProb=zeros(speakerNum, frameNum); %logProb(i,m)表示第i个人第j个句子中第m帧在GMM模型下的log概率
%找出一个句子,看它属于哪个speaker
for k=:speakerNum,
% fprintf('\t\tSpeaker %d...\n', k);
% logProb(k, :)=gmmEval(speakerData(i).sentence(j).fea, speakerGmm(k).gmmPrm);
if ~useIntGmm
% logProb(k, :)=gmmEvalMex(speakerData(i).sentence(j).fea, gmm(k).mean, gmm(k).covariance, gmm(k).weight);
logProb(k, :)=gmmEval(speakerData(i).sentence(j).fea, speakerGmm(k).gmmPrm);
else
% logProb(k, :)=gmmEvalIntMex(speakerData(i).sentence(j).fea, gmm(k).mean, gmm(k).covariance, gmm(k).weight);
logProb(k, :)=gmmEvalIntMex(speakerData(i).sentence(j).fea, speakerGmm(i).gmmPrm);
end
end
cumLogProb=sum(logProb, );
[maxProb, index]=max(cumLogProb);
speakerData(i).sentence(j).predictedSpeaker=index; %找出身份
speakerData(i).sentence(j).logProb=logProb;
end
end % ====== Compute confusion matrix and recognition rate
confusionMatrix=zeros(speakerNum);
for i=:speakerNum,
predictedSpeaker=[speakerData(i).sentence.predictedSpeaker];
[index, count]=elementCount(predictedSpeaker);
confusionMatrix(i, index)=count;
end
recogRate=sum(diag(confusionMatrix))/sum(sum(confusionMatrix));

  GMM-HMM:

  训练阶段:给出HMM的k个状态,每个状态下的观察样本的生成可以用一个概率分布来拟合,这里是采用GMM拟合的。其实,可以把GMM-HMM整体看成是一个生成模型。给定该模型的5个初始参数(结合随机和训练样本获得),启动EM算法的E步:获得训练样本分布,即计算训练样本在各个状态下的概率。M步:用这些训练样本重新评估那5个参数。

  测试阶段:(以孤立词识别为例)给定每个词发音的frame矩阵,取出某一个GMM-HMM模型,算出该发音每一帧数据在取出的GMM-HMM模型各个state下的概率,结合模型的转移概率和初始概率,获得对应的clique tree,可用图模型的方法inference出生成该语音的概率。比较多个GMM-HMM模型,取最大概率的模型对应的词。

   参考资料:

机器学习&数据挖掘笔记_13(用htk完成简单的孤立词识别)

http://htk.eng.cam.ac.uk/extensions/

张智星老师的网页教程mfcc.

机器学习&数据挖掘笔记_14(GMM-HMM语音识别简单理解)的更多相关文章

  1. 机器学习&数据挖掘笔记(常见面试之机器学习算法思想简单梳理)

    机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 前言: 找工作时( ...

  2. [转]机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理)

    机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 转自http://www.cnblogs.com/tornadomeet/p/3395593.html 前言: 找工作时(I ...

  3. 机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理)

    前言: 找工作时(IT行业),除了常见的软件开发以外,机器学习岗位也可以当作是一个选择,不少计算机方向的研究生都会接触这个,如果你的研究方向是机器学习/数据挖掘之类,且又对其非常感兴趣的话,可以考虑考 ...

  4. 机器学习&数据挖掘笔记_25(PGM练习九:HMM用于分类)

    前言: 本次实验是用EM来学习HMM中的参数,并用学好了的HMM对一些kinect数据进行动作分类.实验内容请参考coursera课程:Probabilistic Graphical Models 中 ...

  5. 机器学习&数据挖掘笔记_13(用htk完成简单的孤立词识别)

    最近在看图模型中著名的HMM算法,对应的一些理论公式也能看懂个大概,就是不太明白怎样在一个具体的机器学习问题(比如分类,回归)中使用HMM,特别是一些有关状态变量.观察变量和实际问题中变量的对应关系, ...

  6. 机器学习&数据挖掘笔记_15(关于凸优化的一些简单概念)

    没有系统学过数学优化,但是机器学习中又常用到这些工具和技巧,机器学习中最常见的优化当属凸优化了,这些可以参考Ng的教学资料:http://cs229.stanford.edu/section/cs22 ...

  7. 机器学习&数据挖掘笔记_12(对Conjugate Gradient 优化的简单理解)

    数学优化方法在机器学习算法中至关重要,本篇博客主要来简单介绍下Conjugate Gradient(共轭梯度法,以下简称CG)算法,内容是参考的文献为:An Introduction to the C ...

  8. 机器学习&数据挖掘笔记_19(PGM练习三:马尔科夫网络在OCR上的简单应用)

    前言: 接着coursera课程:Probabilistic Graphical Models上的实验3,本次实验是利用马尔科夫网络(CRF模型)来完成单词的OCR识别,每个单词由多个字母组合,每个字 ...

  9. 机器学习&数据挖掘笔记_18(PGM练习二:贝叶斯网络在遗传图谱在的应用)

    前言: 这是coursera课程:Probabilistic Graphical Models上的第二个实验,主要是用贝叶斯网络对基因遗传问题进行一些计算.具体实验内容可参考实验指导教材:bayes ...

随机推荐

  1. CentOS7使用阿里云镜像安装Mongodb

    一.概述 近日要在新的CentOS系统上安装MongoDB,某度结果后直接从Mongo官网直接获得3.2版本的下载链接,结果在下载时发觉速度慢的可怜.迫于无奈,只能找国内的镜像下载.切换国内的安装源后 ...

  2. android更换工具链

    欢迎转载opendevkit文章, 文章原始地址: http://www.opendevkit.com/?e=73 android编译系统是跟随android源码一起发布的,使用了gcc编译器,也就是 ...

  3. fallacies of distributed computing

    The network is reliable. Latency is zero. Bandwidth is infinite. The network is secure. Topology doe ...

  4. Transactional replication-如何跳过一个事务

    在transactional replication, 经常会遇到数据同步延迟的情况.有时候这些延迟是由于在publication中执行了一个更新,例如update ta set col=? Wher ...

  5. C#基于Socket的简单聊天室实践

    序:实现一个基于Socket的简易的聊天室,实现的思路如下: 程序的结构:多个客户端+一个服务端,客户端都是向服务端发送消息,然后服务端转发给所有的客户端,这样形成一个简单的聊天室功能. 实现的细节: ...

  6. ASP.Net MVC的ViewBag一个坑,不要跳进去

    如鹏的学习管理系统是使用ASP.net MVC 5开发的,今天一个新版本发布后网站出现一个Bug,学生在下拉列表中选中的项再加载显示的时候发现仍然没被选中.详细一点说吧:假如有这样一个Action: ...

  7. 打开mysql时,提示 1040,Too many connections

    打开mysql时,提示 1040,Too many connections,这样就无法打开数据库,看不了表里边的内容了. 出现这个问题的原因是,同时对数据库的连接数过大,mysql默认的最大连接数是1 ...

  8. 如何做到在虚拟数据库和真实数据库之间自由切换?【低调赠送:QQ高仿版GG 4.4 最新源码】

    记得以前在公司上班时,有时候白天的活没干完,我就会把工作带回家晚上加班继续做.但是,我们开发用的数据库是部署在公司局网内部的一台服务器上的,在家里是肯定连不上这台机器的.在家里没有数据库,服务端就跑不 ...

  9. sourcesafe.light 开源项目启动

    sourcesafe.light 源于一个2D独立砖块沙盒游戏. 在这个游戏的设计中碰到了一个瓶颈:这个游戏想把玩家变成一个个neo,在矩阵世界中没有什么不可以修改. 这个游戏要跨平台,玩家的修改操作 ...

  10. [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之网格Meshes

    [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之网格Meshes 本篇分享一下第5个已完工的视频,即<beginner Graphics – ...